dialecto-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +196 -0
- data/action.yml +83 -0
- data/exe/dialecto-rails +6 -0
- data/lib/dialecto-rails.rb +1 -0
- data/lib/dialecto_rails/config.rb +206 -0
- data/lib/dialecto_rails/errors.rb +21 -0
- data/lib/dialecto_rails/in_context/args.rb +37 -0
- data/lib/dialecto_rails/in_context/assets/overlay.js +2784 -0
- data/lib/dialecto_rails/in_context/backend_hook.rb +39 -0
- data/lib/dialecto_rails/in_context/csp.rb +72 -0
- data/lib/dialecto_rails/in_context/git_context.rb +103 -0
- data/lib/dialecto_rails/in_context/key_index.rb +128 -0
- data/lib/dialecto_rails/in_context/launch_token.rb +78 -0
- data/lib/dialecto_rails/in_context/live_middleware.rb +307 -0
- data/lib/dialecto_rails/in_context/live_runtime.rb +53 -0
- data/lib/dialecto_rails/in_context/live_store.rb +14 -0
- data/lib/dialecto_rails/in_context/marker.rb +106 -0
- data/lib/dialecto_rails/in_context/marking.rb +122 -0
- data/lib/dialecto_rails/in_context/middleware.rb +202 -0
- data/lib/dialecto_rails/in_context/overrides.rb +61 -0
- data/lib/dialecto_rails/in_context/railtie_stub.rb +76 -0
- data/lib/dialecto_rails/in_context/runtime.rb +52 -0
- data/lib/dialecto_rails/in_context/sanitizer.rb +19 -0
- data/lib/dialecto_rails/in_context/store.rb +41 -0
- data/lib/dialecto_rails/scan/catalogs.rb +46 -0
- data/lib/dialecto_rails/scan/cli.rb +339 -0
- data/lib/dialecto_rails/scan/client.rb +117 -0
- data/lib/dialecto_rails/scan/envelope.rb +100 -0
- data/lib/dialecto_rails/scan/git.rb +68 -0
- data/lib/dialecto_rails/scan/worktree.rb +41 -0
- data/lib/dialecto_rails/unmarked.rb +42 -0
- data/lib/dialecto_rails/usages/call_args.rb +78 -0
- data/lib/dialecto_rails/usages/catalog_keys.rb +67 -0
- data/lib/dialecto_rails/usages/check.rb +70 -0
- data/lib/dialecto_rails/usages/haml_extractor.rb +260 -0
- data/lib/dialecto_rails/usages/keys.rb +206 -0
- data/lib/dialecto_rails/usages/known_wrappers.rb +31 -0
- data/lib/dialecto_rails/usages/lazy_scope.rb +52 -0
- data/lib/dialecto_rails/usages/parse_check.rb +75 -0
- data/lib/dialecto_rails/usages/read_by_name.json +28 -0
- data/lib/dialecto_rails/usages/record.rb +91 -0
- data/lib/dialecto_rails/usages/ruby_visitor.rb +628 -0
- data/lib/dialecto_rails/usages/scanner.rb +241 -0
- data/lib/dialecto_rails/usages/summary.rb +191 -0
- data/lib/dialecto_rails/usages/templates.rb +45 -0
- data/lib/dialecto_rails/usages/wrappers.rb +336 -0
- data/lib/dialecto_rails/version.rb +3 -0
- data/lib/dialecto_rails.rb +14 -0
- metadata +119 -0
|
@@ -0,0 +1,2784 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// js/in_context/protocol.js
|
|
3
|
+
var SOURCE = "dialecto";
|
|
4
|
+
var VERSION = 1;
|
|
5
|
+
var MAX_STRING = 2e4;
|
|
6
|
+
var MAX_EDITS = 5e3;
|
|
7
|
+
var MAX_FORMS = 10;
|
|
8
|
+
var isString = (value) => typeof value === "string" && value.length <= MAX_STRING;
|
|
9
|
+
var isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
var identity = (m) => isString(m.domain) && isString(m.key) && isString(m.locale) && m.key !== "";
|
|
11
|
+
function gettextFields(raw) {
|
|
12
|
+
if (raw.forms === void 0) return {};
|
|
13
|
+
const { forms, msgid_plural: plural = null, from_forms: fromForms } = raw;
|
|
14
|
+
const listOk = (list) => Array.isArray(list) && list.length > 0 && list.length <= MAX_FORMS && list.every(isString);
|
|
15
|
+
if (!listOk(forms) || !(plural === null || isString(plural))) return null;
|
|
16
|
+
if (fromForms !== void 0 && !listOk(fromForms)) return null;
|
|
17
|
+
return { forms: [...forms], msgid_plural: plural, ...fromForms === void 0 ? {} : { from_forms: [...fromForms] } };
|
|
18
|
+
}
|
|
19
|
+
function normalizeEdit(raw) {
|
|
20
|
+
if (!isPlainObject(raw) || !identity(raw) || !isString(raw.from) || !isString(raw.to)) return null;
|
|
21
|
+
const gettext = gettextFields(raw);
|
|
22
|
+
if (!gettext) return null;
|
|
23
|
+
return { domain: raw.domain, key: raw.key, locale: raw.locale, from: raw.from, to: raw.to, ...gettext };
|
|
24
|
+
}
|
|
25
|
+
function normalizeEdits(list) {
|
|
26
|
+
if (!Array.isArray(list) || list.length > MAX_EDITS) return null;
|
|
27
|
+
const edits = [];
|
|
28
|
+
for (const item of list) {
|
|
29
|
+
const edit = normalizeEdit(item);
|
|
30
|
+
if (!edit) return null;
|
|
31
|
+
edits.push(edit);
|
|
32
|
+
}
|
|
33
|
+
return edits;
|
|
34
|
+
}
|
|
35
|
+
function parseIncoming(data) {
|
|
36
|
+
if (!isPlainObject(data) || data.source !== SOURCE || data.v !== VERSION || typeof data.type !== "string") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
switch (data.type) {
|
|
40
|
+
case "ready": {
|
|
41
|
+
const edits = data.edits === void 0 ? [] : normalizeEdits(data.edits);
|
|
42
|
+
if (!edits) return null;
|
|
43
|
+
return { type: "ready", signed_in: data.signed_in === true, edits };
|
|
44
|
+
}
|
|
45
|
+
case "preview": {
|
|
46
|
+
const edit = normalizeEdit(data);
|
|
47
|
+
return edit ? { type: "preview", ...edit } : null;
|
|
48
|
+
}
|
|
49
|
+
case "preview_clear":
|
|
50
|
+
return identity(data) ? { type: "preview_clear", domain: data.domain, key: data.key, locale: data.locale } : null;
|
|
51
|
+
case "previews_reset": {
|
|
52
|
+
const edits = normalizeEdits(data.edits);
|
|
53
|
+
return edits ? { type: "previews_reset", edits } : null;
|
|
54
|
+
}
|
|
55
|
+
case "highlight":
|
|
56
|
+
if (data.domain === null) return { type: "highlight", domain: null, key: null, scroll: false };
|
|
57
|
+
if (!isString(data.domain) || !isString(data.key) || data.key === "") return null;
|
|
58
|
+
return { type: "highlight", domain: data.domain, key: data.key, scroll: data.scroll === true };
|
|
59
|
+
case "pick_mode":
|
|
60
|
+
return typeof data.on === "boolean" ? { type: "pick_mode", on: data.on } : null;
|
|
61
|
+
case "save_files": {
|
|
62
|
+
const edits = normalizeEdits(data.edits);
|
|
63
|
+
return edits && edits.length > 0 && edits.every((edit) => edit.forms === void 0) ? { type: "save_files", edits: edits.map(({ domain, key, locale, from, to }) => ({ domain, key, locale, from, to })) } : null;
|
|
64
|
+
}
|
|
65
|
+
// The account's shortcut for this user ("" = none); the overlay judges whether it is usable.
|
|
66
|
+
case "shortcut":
|
|
67
|
+
return isString(data.key) ? { type: "shortcut", key: data.key } : null;
|
|
68
|
+
case "close":
|
|
69
|
+
case "reload":
|
|
70
|
+
case "toggle":
|
|
71
|
+
return { type: data.type };
|
|
72
|
+
default:
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
var envelope = (type, payload = {}) => ({ source: SOURCE, v: VERSION, type, ...payload });
|
|
77
|
+
var hello = (page, site, toggleKey = "", baseKey = "") => envelope("hello", {
|
|
78
|
+
page,
|
|
79
|
+
site,
|
|
80
|
+
...toggleKey ? { toggle_key: toggleKey } : {},
|
|
81
|
+
...baseKey && baseKey !== toggleKey ? { base_key: baseKey } : {}
|
|
82
|
+
});
|
|
83
|
+
var pageStrings = (strings) => envelope("page_strings", { strings });
|
|
84
|
+
var pick = (domain, key) => envelope("pick", { domain, key });
|
|
85
|
+
var refOf = (ref) => ({ domain: ref.domain, key: ref.key, locale: ref.locale });
|
|
86
|
+
function filesSaved(result) {
|
|
87
|
+
if (!isPlainObject(result) || !Array.isArray(result.saved)) {
|
|
88
|
+
return envelope("files_saved", { error: typeof result?.error === "string" ? result.error : "unreachable" });
|
|
89
|
+
}
|
|
90
|
+
const refs = (list) => Array.isArray(list) ? list.filter((ref) => isPlainObject(ref) && identity(ref)).slice(0, MAX_EDITS) : [];
|
|
91
|
+
return envelope("files_saved", {
|
|
92
|
+
saved: refs(result.saved).map(refOf),
|
|
93
|
+
refused: refs(result.refused).map((ref) => ({ ...refOf(ref), reason: isString(ref.reason) ? ref.reason : "refused" })),
|
|
94
|
+
files: Array.isArray(result.files) ? result.files.filter(isString).slice(0, 100) : [],
|
|
95
|
+
scanned: result.scan?.ok === true
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
var previewStatus = ({ domain, key, locale, applied, unsupported, reason, text: text3 }) => envelope("preview_status", {
|
|
99
|
+
domain,
|
|
100
|
+
key,
|
|
101
|
+
locale,
|
|
102
|
+
applied,
|
|
103
|
+
unsupported,
|
|
104
|
+
...reason ? { reason } : {},
|
|
105
|
+
...typeof text3 === "string" ? { text: text3 } : {}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// js/in_context/bridge.js
|
|
109
|
+
var HELLO_INTERVAL_MS = 500;
|
|
110
|
+
var HELLO_ATTEMPTS = 20;
|
|
111
|
+
var SNAPSHOT_DEBOUNCE_MS = 150;
|
|
112
|
+
function createBridge({ origin, frameWindow, page, site, strings, onMessage, onTimeout, toggleKey = () => "", baseKey = () => "" }) {
|
|
113
|
+
let ready = false;
|
|
114
|
+
let helloTimer = 0;
|
|
115
|
+
let attempts = 0;
|
|
116
|
+
let snapshotTimer = 0;
|
|
117
|
+
let lastSnapshot = null;
|
|
118
|
+
const post = (message) => {
|
|
119
|
+
const target = frameWindow();
|
|
120
|
+
if (target) target.postMessage(message, origin);
|
|
121
|
+
};
|
|
122
|
+
function sendSnapshot(force) {
|
|
123
|
+
if (!ready) return;
|
|
124
|
+
const rows = strings();
|
|
125
|
+
const serialized = JSON.stringify(rows);
|
|
126
|
+
if (!force && serialized === lastSnapshot) return;
|
|
127
|
+
lastSnapshot = serialized;
|
|
128
|
+
post(pageStrings(rows));
|
|
129
|
+
}
|
|
130
|
+
function stopHandshake() {
|
|
131
|
+
clearInterval(helloTimer);
|
|
132
|
+
helloTimer = 0;
|
|
133
|
+
}
|
|
134
|
+
function handle(event) {
|
|
135
|
+
if (event.origin !== origin || !event.source || event.source !== frameWindow()) return;
|
|
136
|
+
const message = parseIncoming(event.data);
|
|
137
|
+
if (!message) return;
|
|
138
|
+
if (message.type === "ready") {
|
|
139
|
+
ready = true;
|
|
140
|
+
stopHandshake();
|
|
141
|
+
sendSnapshot(true);
|
|
142
|
+
}
|
|
143
|
+
onMessage(message);
|
|
144
|
+
}
|
|
145
|
+
window.addEventListener("message", handle);
|
|
146
|
+
return {
|
|
147
|
+
/** Send `hello` now and every 500ms until `ready` (about 10s). */
|
|
148
|
+
startHandshake() {
|
|
149
|
+
stopHandshake();
|
|
150
|
+
ready = false;
|
|
151
|
+
lastSnapshot = null;
|
|
152
|
+
attempts = 0;
|
|
153
|
+
const tick = () => {
|
|
154
|
+
if (ready) return;
|
|
155
|
+
if (attempts >= HELLO_ATTEMPTS) {
|
|
156
|
+
stopHandshake();
|
|
157
|
+
onTimeout();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
attempts += 1;
|
|
161
|
+
post(hello(page(), site(), toggleKey(), baseKey()));
|
|
162
|
+
};
|
|
163
|
+
helloTimer = setInterval(tick, HELLO_INTERVAL_MS);
|
|
164
|
+
tick();
|
|
165
|
+
},
|
|
166
|
+
/** Debounced, de-duplicated `page_strings`. */
|
|
167
|
+
requestSnapshot() {
|
|
168
|
+
if (!ready) return;
|
|
169
|
+
clearTimeout(snapshotTimer);
|
|
170
|
+
snapshotTimer = setTimeout(() => sendSnapshot(false), SNAPSHOT_DEBOUNCE_MS);
|
|
171
|
+
},
|
|
172
|
+
sendPick: (domain, key) => post(pick(domain, key)),
|
|
173
|
+
sendPreviewStatus: (status) => post(previewStatus(status)),
|
|
174
|
+
sendFilesSaved: (result) => post(filesSaved(result)),
|
|
175
|
+
isReady: () => ready,
|
|
176
|
+
destroy() {
|
|
177
|
+
stopHandshake();
|
|
178
|
+
clearTimeout(snapshotTimer);
|
|
179
|
+
window.removeEventListener("message", handle);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// js/in_context/codec.js
|
|
185
|
+
var OPEN = String.fromCodePoint(8290);
|
|
186
|
+
var HEADER_END = String.fromCodePoint(8291);
|
|
187
|
+
var CLOSE = String.fromCodePoint(8292);
|
|
188
|
+
var DIGITS = [8204, 8205, 8288, 8289].map((cp) => String.fromCodePoint(cp));
|
|
189
|
+
var FIELD_SEP = String.fromCodePoint(31);
|
|
190
|
+
var RECORD_SEP = String.fromCodePoint(30);
|
|
191
|
+
var DIGIT_VALUE = new Map(DIGITS.map((char, value) => [char, value]));
|
|
192
|
+
var encoder = new TextEncoder();
|
|
193
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
194
|
+
function readArgs(json) {
|
|
195
|
+
if (json === null) return null;
|
|
196
|
+
try {
|
|
197
|
+
const args = JSON.parse(json);
|
|
198
|
+
return args !== null && typeof args === "object" && !Array.isArray(args) ? args : null;
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function readHeader(str, at) {
|
|
204
|
+
let i = at + 1;
|
|
205
|
+
const bytes = [];
|
|
206
|
+
let acc = 0;
|
|
207
|
+
let count = 0;
|
|
208
|
+
for (; i < str.length; i++) {
|
|
209
|
+
const value = DIGIT_VALUE.get(str[i]);
|
|
210
|
+
if (value === void 0) break;
|
|
211
|
+
acc = acc << 2 | value;
|
|
212
|
+
count++;
|
|
213
|
+
if (count % 4 === 0) {
|
|
214
|
+
bytes.push(acc);
|
|
215
|
+
acc = 0;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (str[i] !== HEADER_END || count === 0 || count % 4 !== 0) return null;
|
|
219
|
+
let payload;
|
|
220
|
+
try {
|
|
221
|
+
payload = decoder.decode(Uint8Array.from(bytes));
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const record = payload.lastIndexOf(RECORD_SEP);
|
|
226
|
+
const identity2 = record < 0 ? payload : payload.slice(0, record);
|
|
227
|
+
const first = identity2.indexOf(FIELD_SEP);
|
|
228
|
+
const last = identity2.lastIndexOf(FIELD_SEP);
|
|
229
|
+
if (first < 0 || first === last) return null;
|
|
230
|
+
return {
|
|
231
|
+
domain: identity2.slice(0, first),
|
|
232
|
+
key: identity2.slice(first + 1, last),
|
|
233
|
+
locale: identity2.slice(last + 1),
|
|
234
|
+
args: readArgs(record < 0 ? null : payload.slice(record + 1)),
|
|
235
|
+
next: i + 1
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function parseSegments(str) {
|
|
239
|
+
const roots = [];
|
|
240
|
+
const stack = [];
|
|
241
|
+
const attach = (segment) => {
|
|
242
|
+
(stack.length ? stack[stack.length - 1].children : roots).push(segment);
|
|
243
|
+
};
|
|
244
|
+
for (let i = 0; i < str.length; i++) {
|
|
245
|
+
const char = str[i];
|
|
246
|
+
if (char === OPEN) {
|
|
247
|
+
const header = readHeader(str, i);
|
|
248
|
+
if (!header) continue;
|
|
249
|
+
stack.push({
|
|
250
|
+
domain: header.domain,
|
|
251
|
+
key: header.key,
|
|
252
|
+
locale: header.locale,
|
|
253
|
+
args: header.args,
|
|
254
|
+
start: i,
|
|
255
|
+
innerStart: header.next,
|
|
256
|
+
innerEnd: str.length,
|
|
257
|
+
end: str.length,
|
|
258
|
+
open: true,
|
|
259
|
+
children: []
|
|
260
|
+
});
|
|
261
|
+
i = header.next - 1;
|
|
262
|
+
} else if (char === CLOSE && stack.length) {
|
|
263
|
+
const segment = stack.pop();
|
|
264
|
+
segment.innerEnd = i;
|
|
265
|
+
segment.end = i + 1;
|
|
266
|
+
segment.open = false;
|
|
267
|
+
attach(segment);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
while (stack.length) attach(stack.pop());
|
|
271
|
+
return roots;
|
|
272
|
+
}
|
|
273
|
+
function flattenSegments(segments) {
|
|
274
|
+
const out = [];
|
|
275
|
+
const visit = (list) => {
|
|
276
|
+
for (const segment of list) {
|
|
277
|
+
out.push(segment);
|
|
278
|
+
visit(segment.children);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
visit(segments);
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
function stripMarks(str) {
|
|
285
|
+
if (!str || str.indexOf(OPEN) < 0 && str.indexOf(CLOSE) < 0 && str.indexOf(HEADER_END) < 0) return str ?? "";
|
|
286
|
+
let out = "";
|
|
287
|
+
for (let i = 0; i < str.length; i++) {
|
|
288
|
+
const char = str[i];
|
|
289
|
+
if (char === OPEN) {
|
|
290
|
+
const header = readHeader(str, i);
|
|
291
|
+
if (header) i = header.next - 1;
|
|
292
|
+
} else if (char !== CLOSE && char !== HEADER_END) {
|
|
293
|
+
out += char;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
function hasMarks(str) {
|
|
299
|
+
return typeof str === "string" && str.indexOf(OPEN) >= 0;
|
|
300
|
+
}
|
|
301
|
+
var KEY_SEP = String.fromCodePoint(9247);
|
|
302
|
+
var keyId = (domain, key, locale) => domain + KEY_SEP + key + KEY_SEP + locale;
|
|
303
|
+
var CONTEXT_SEP = String.fromCodePoint(4);
|
|
304
|
+
function splitKey(key) {
|
|
305
|
+
const at = key.indexOf(CONTEXT_SEP);
|
|
306
|
+
return at < 0 ? { msgctxt: null, msgid: key } : { msgctxt: key.slice(0, at), msgid: key.slice(at + 1) };
|
|
307
|
+
}
|
|
308
|
+
function segmentText(str, segment) {
|
|
309
|
+
return stripMarks(str.slice(segment.innerStart, segment.innerEnd));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// js/in_context/controls.js
|
|
313
|
+
var LABEL_INPUTS = /* @__PURE__ */ new Set(["button", "submit", "reset", "image"]);
|
|
314
|
+
function isDataControl(el) {
|
|
315
|
+
if (el.localName === "textarea") return true;
|
|
316
|
+
return el.localName === "input" && !LABEL_INPUTS.has(String(el.type || "text").toLowerCase());
|
|
317
|
+
}
|
|
318
|
+
function markedStrings(value) {
|
|
319
|
+
return flattenSegments(parseSegments(value)).map((segment) => ({
|
|
320
|
+
domain: segment.domain,
|
|
321
|
+
key: segment.key,
|
|
322
|
+
locale: segment.locale,
|
|
323
|
+
segment,
|
|
324
|
+
text: segmentText(value, segment)
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
function stripFormData(formData) {
|
|
328
|
+
const entries = [...formData.entries()];
|
|
329
|
+
if (!entries.some(([, value]) => typeof value === "string" && stripMarks(value) !== value)) return false;
|
|
330
|
+
for (const name of new Set(entries.map(([name2]) => name2))) formData.delete(name);
|
|
331
|
+
for (const [name, value] of entries) formData.append(name, typeof value === "string" ? stripMarks(value) : value);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// js/in_context/slots.js
|
|
336
|
+
var records = /* @__PURE__ */ new WeakMap();
|
|
337
|
+
function textSlot(node, kind = "text") {
|
|
338
|
+
return {
|
|
339
|
+
type: "text",
|
|
340
|
+
kind,
|
|
341
|
+
node,
|
|
342
|
+
read: () => node.nodeValue ?? "",
|
|
343
|
+
write: (value) => {
|
|
344
|
+
node.nodeValue = value;
|
|
345
|
+
},
|
|
346
|
+
element: () => node.parentElement,
|
|
347
|
+
isConnected: () => node.isConnected
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function attributeSlot(el, attr, kind = "attribute") {
|
|
351
|
+
return {
|
|
352
|
+
type: "attribute",
|
|
353
|
+
kind,
|
|
354
|
+
el,
|
|
355
|
+
attr,
|
|
356
|
+
read: () => el.getAttribute(attr) ?? "",
|
|
357
|
+
write: (value) => el.setAttribute(attr, value),
|
|
358
|
+
element: () => el,
|
|
359
|
+
isConnected: () => el.isConnected
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function baseValue(slot) {
|
|
363
|
+
const current = slot.read();
|
|
364
|
+
const record = records.get(slot);
|
|
365
|
+
return record && record.written === current ? record.base : current;
|
|
366
|
+
}
|
|
367
|
+
function isOwnWrite(slot) {
|
|
368
|
+
const record = records.get(slot);
|
|
369
|
+
return Boolean(record) && record.written === slot.read();
|
|
370
|
+
}
|
|
371
|
+
function setSlotValue(slot, base, value) {
|
|
372
|
+
const current = slot.read();
|
|
373
|
+
if (value === base) {
|
|
374
|
+
if (current !== base) slot.write(base);
|
|
375
|
+
records.delete(slot);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (current !== value) slot.write(value);
|
|
379
|
+
records.set(slot, { base, written: value });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// js/in_context/scanner.js
|
|
383
|
+
var SKIP_PARENTS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "textarea"]);
|
|
384
|
+
var ALWAYS_ATTRS = ["placeholder", "title", "alt", "aria-label", "aria-description"];
|
|
385
|
+
var OBSERVED_ATTRS = [...ALWAYS_ATTRS, "value", "content"];
|
|
386
|
+
var ATTR_SELECTOR = [
|
|
387
|
+
...ALWAYS_ATTRS.map((attr) => `[${attr}]`),
|
|
388
|
+
"button[value]",
|
|
389
|
+
'input[type="button"][value]',
|
|
390
|
+
'input[type="submit"][value]',
|
|
391
|
+
"meta[content]"
|
|
392
|
+
].join(",");
|
|
393
|
+
var META_NAME = /^(description|og:|twitter:)/;
|
|
394
|
+
var CONTROL_SELECTOR = "input, textarea";
|
|
395
|
+
var visibilityOptions = { checkOpacity: true, checkVisibilityCSS: true };
|
|
396
|
+
function relevantAttributes(el) {
|
|
397
|
+
const attrs = ALWAYS_ATTRS.filter((attr) => el.hasAttribute(attr));
|
|
398
|
+
const tag = el.localName;
|
|
399
|
+
if (tag === "input" && el.hasAttribute("value") && /^(button|submit)$/i.test(el.getAttribute("type") ?? "")) {
|
|
400
|
+
attrs.unshift("value");
|
|
401
|
+
}
|
|
402
|
+
if (tag === "button" && el.hasAttribute("value")) attrs.push("value");
|
|
403
|
+
if (tag === "meta" && el.hasAttribute("content")) {
|
|
404
|
+
const name = (el.getAttribute("name") ?? el.getAttribute("property") ?? "").toLowerCase();
|
|
405
|
+
if (META_NAME.test(name)) attrs.push("content");
|
|
406
|
+
}
|
|
407
|
+
return attrs;
|
|
408
|
+
}
|
|
409
|
+
function textKind(node) {
|
|
410
|
+
const parent = node.parentElement;
|
|
411
|
+
return parent && parent.localName === "title" && parent.closest("head") ? "title" : "text";
|
|
412
|
+
}
|
|
413
|
+
function attributeKind(el, attr) {
|
|
414
|
+
return el.localName === "meta" && attr === "content" ? "meta" : "attribute";
|
|
415
|
+
}
|
|
416
|
+
function visible(el) {
|
|
417
|
+
if (!el) return false;
|
|
418
|
+
return typeof el.checkVisibility === "function" ? el.checkVisibility(visibilityOptions) : true;
|
|
419
|
+
}
|
|
420
|
+
function intersects(rect, viewport) {
|
|
421
|
+
return rect.width + rect.height > 0 && rect.bottom > viewport.top && rect.top < viewport.bottom && rect.right > viewport.left && rect.left < viewport.right;
|
|
422
|
+
}
|
|
423
|
+
function createScanner({ isIgnored = () => false, viewport, onChange = () => {
|
|
424
|
+
} }) {
|
|
425
|
+
const slots = /* @__PURE__ */ new Set();
|
|
426
|
+
const textSlots = /* @__PURE__ */ new Map();
|
|
427
|
+
const attributeSlots = /* @__PURE__ */ new Map();
|
|
428
|
+
const controls = /* @__PURE__ */ new Map();
|
|
429
|
+
let controlsChanged = false;
|
|
430
|
+
let observer = null;
|
|
431
|
+
let pending = null;
|
|
432
|
+
let frame = 0;
|
|
433
|
+
const skipText = (node) => {
|
|
434
|
+
const parent = node.parentElement;
|
|
435
|
+
return !parent || SKIP_PARENTS.has(parent.localName) || isIgnored(parent);
|
|
436
|
+
};
|
|
437
|
+
function scanControl(el) {
|
|
438
|
+
if (isIgnored(el) || !isDataControl(el)) return false;
|
|
439
|
+
const value = el.value;
|
|
440
|
+
if (typeof value !== "string" || !hasMarks(value)) return false;
|
|
441
|
+
const slot = { type: "control", kind: "attribute", el, element: () => el, isConnected: () => el.isConnected };
|
|
442
|
+
slot.occurrences = markedStrings(value).map((found) => ({ ...found, kind: "attribute", slot, onScreen: false }));
|
|
443
|
+
controls.set(el, slot);
|
|
444
|
+
controlsChanged = true;
|
|
445
|
+
el.value = stripMarks(value);
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
function build(slot) {
|
|
449
|
+
const value = slot.read();
|
|
450
|
+
if (!hasMarks(value)) return false;
|
|
451
|
+
const live = flattenSegments(parseSegments(value));
|
|
452
|
+
const base = baseValue(slot);
|
|
453
|
+
const baseFlat = base === value ? live : flattenSegments(parseSegments(base));
|
|
454
|
+
slot.occurrences = live.map((segment, index) => {
|
|
455
|
+
const source = baseFlat.length === live.length ? baseFlat[index] : segment;
|
|
456
|
+
const from = baseFlat.length === live.length ? base : value;
|
|
457
|
+
return {
|
|
458
|
+
domain: segment.domain,
|
|
459
|
+
key: segment.key,
|
|
460
|
+
locale: segment.locale,
|
|
461
|
+
kind: slot.kind,
|
|
462
|
+
slot,
|
|
463
|
+
segment,
|
|
464
|
+
text: segmentText(from, source),
|
|
465
|
+
onScreen: false
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
function drop(slot) {
|
|
471
|
+
slots.delete(slot);
|
|
472
|
+
if (slot.type === "text") {
|
|
473
|
+
textSlots.delete(slot.node);
|
|
474
|
+
} else {
|
|
475
|
+
const map = attributeSlots.get(slot.el);
|
|
476
|
+
if (map) {
|
|
477
|
+
map.delete(slot.attr);
|
|
478
|
+
if (!map.size) attributeSlots.delete(slot.el);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function textSlotFor(node) {
|
|
483
|
+
let slot = textSlots.get(node);
|
|
484
|
+
if (!slot) {
|
|
485
|
+
slot = textSlot(node, textKind(node));
|
|
486
|
+
textSlots.set(node, slot);
|
|
487
|
+
}
|
|
488
|
+
return slot;
|
|
489
|
+
}
|
|
490
|
+
function attributeSlotFor(el, attr) {
|
|
491
|
+
let map = attributeSlots.get(el);
|
|
492
|
+
if (!map) attributeSlots.set(el, map = /* @__PURE__ */ new Map());
|
|
493
|
+
let slot = map.get(attr);
|
|
494
|
+
if (!slot) {
|
|
495
|
+
slot = attributeSlot(el, attr, attributeKind(el, attr));
|
|
496
|
+
map.set(attr, slot);
|
|
497
|
+
}
|
|
498
|
+
return slot;
|
|
499
|
+
}
|
|
500
|
+
function refresh(slot) {
|
|
501
|
+
if (!slot.isConnected() || !build(slot)) {
|
|
502
|
+
if (slots.has(slot)) drop(slot);
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
slots.add(slot);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
function scanText(node, changed) {
|
|
509
|
+
if (node.parentElement?.localName === "textarea") {
|
|
510
|
+
scanControl(node.parentElement);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (skipText(node)) return;
|
|
514
|
+
const known = textSlots.get(node);
|
|
515
|
+
if (!hasMarks(node.nodeValue ?? "")) {
|
|
516
|
+
if (known && slots.has(known)) {
|
|
517
|
+
drop(known);
|
|
518
|
+
changed.add(known);
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const slot = textSlotFor(node);
|
|
523
|
+
if (refresh(slot)) changed.add(slot);
|
|
524
|
+
}
|
|
525
|
+
function scanElementAttributes(el, changed) {
|
|
526
|
+
if (isIgnored(el)) return;
|
|
527
|
+
scanControl(el);
|
|
528
|
+
const wanted = new Set(relevantAttributes(el));
|
|
529
|
+
for (const attr of wanted) {
|
|
530
|
+
const value = el.getAttribute(attr) ?? "";
|
|
531
|
+
const known = attributeSlots.get(el)?.get(attr);
|
|
532
|
+
if (!value.includes(OPEN)) {
|
|
533
|
+
if (known && slots.has(known)) {
|
|
534
|
+
drop(known);
|
|
535
|
+
changed.add(known);
|
|
536
|
+
}
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const slot = attributeSlotFor(el, attr);
|
|
540
|
+
if (refresh(slot)) changed.add(slot);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function scanTree(root, changed) {
|
|
544
|
+
if (root.nodeType === Node.TEXT_NODE) {
|
|
545
|
+
scanText(root, changed);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_NODE) return;
|
|
549
|
+
if (root.nodeType === Node.ELEMENT_NODE && isIgnored(root)) return;
|
|
550
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
551
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) scanText(node, changed);
|
|
552
|
+
if (root.nodeType === Node.ELEMENT_NODE && root.matches(ATTR_SELECTOR)) scanElementAttributes(root, changed);
|
|
553
|
+
for (const el of root.querySelectorAll(ATTR_SELECTOR)) scanElementAttributes(el, changed);
|
|
554
|
+
if (root.nodeType === Node.ELEMENT_NODE && root.matches(CONTROL_SELECTOR)) scanControl(root);
|
|
555
|
+
for (const el of root.querySelectorAll(CONTROL_SELECTOR)) scanControl(el);
|
|
556
|
+
}
|
|
557
|
+
function prune(changed) {
|
|
558
|
+
for (const slot of [...slots]) {
|
|
559
|
+
if (!slot.isConnected()) {
|
|
560
|
+
drop(slot);
|
|
561
|
+
changed.add(slot);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
for (const el of [...controls.keys()]) {
|
|
565
|
+
if (!el.isConnected) {
|
|
566
|
+
controls.delete(el);
|
|
567
|
+
controlsChanged = true;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function queue() {
|
|
572
|
+
if (!frame) frame = requestAnimationFrame(flush);
|
|
573
|
+
}
|
|
574
|
+
function flush() {
|
|
575
|
+
frame = 0;
|
|
576
|
+
const work2 = pending;
|
|
577
|
+
pending = null;
|
|
578
|
+
if (!work2) return;
|
|
579
|
+
const changed = /* @__PURE__ */ new Set();
|
|
580
|
+
controlsChanged = false;
|
|
581
|
+
if (work2.removed) prune(changed);
|
|
582
|
+
for (const root of work2.trees) if (root.isConnected) scanTree(root, changed);
|
|
583
|
+
for (const node of work2.text) if (node.isConnected) scanText(node, changed);
|
|
584
|
+
for (const el of work2.attributes) if (el.isConnected) scanElementAttributes(el, changed);
|
|
585
|
+
if (changed.size || controlsChanged) onChange([...changed]);
|
|
586
|
+
}
|
|
587
|
+
function work() {
|
|
588
|
+
return pending ?? (pending = { trees: /* @__PURE__ */ new Set(), text: /* @__PURE__ */ new Set(), attributes: /* @__PURE__ */ new Set(), removed: false });
|
|
589
|
+
}
|
|
590
|
+
function onMutations(records2) {
|
|
591
|
+
for (const record of records2) {
|
|
592
|
+
if (record.type === "childList") {
|
|
593
|
+
if (record.removedNodes.length) work().removed = true;
|
|
594
|
+
for (const node of record.addedNodes) if (!isIgnored(node)) work().trees.add(node);
|
|
595
|
+
} else if (record.type === "characterData") {
|
|
596
|
+
const known = textSlots.get(record.target);
|
|
597
|
+
if (!(known && isOwnWrite(known))) work().text.add(record.target);
|
|
598
|
+
} else if (record.type === "attributes") {
|
|
599
|
+
const known = attributeSlots.get(record.target)?.get(record.attributeName);
|
|
600
|
+
if (!(known && isOwnWrite(known))) work().attributes.add(record.target);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (pending) queue();
|
|
604
|
+
}
|
|
605
|
+
function occurrences() {
|
|
606
|
+
const out = [];
|
|
607
|
+
for (const slot of slots) out.push(...slot.occurrences);
|
|
608
|
+
for (const slot of controls.values()) out.push(...slot.occurrences);
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
function rangeFor(occurrence) {
|
|
612
|
+
const { slot, segment } = occurrence;
|
|
613
|
+
const range = document.createRange();
|
|
614
|
+
try {
|
|
615
|
+
range.setStart(slot.node, segment.start);
|
|
616
|
+
range.setEnd(slot.node, segment.end);
|
|
617
|
+
} catch {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
return range;
|
|
621
|
+
}
|
|
622
|
+
function rects(occurrence) {
|
|
623
|
+
const { slot } = occurrence;
|
|
624
|
+
const el = slot.element();
|
|
625
|
+
if (!el || !visible(el)) return [];
|
|
626
|
+
if (slot.type === "text") {
|
|
627
|
+
const range = rangeFor(occurrence);
|
|
628
|
+
return range ? [...range.getClientRects()].filter((r) => r.width + r.height > 0) : [];
|
|
629
|
+
}
|
|
630
|
+
const box = el.getBoundingClientRect();
|
|
631
|
+
return box.width + box.height > 0 ? [box] : [];
|
|
632
|
+
}
|
|
633
|
+
function measure() {
|
|
634
|
+
const bounds = viewport();
|
|
635
|
+
let changed = false;
|
|
636
|
+
for (const occurrence of occurrences()) {
|
|
637
|
+
const onScreen = occurrence.kind === "title" || occurrence.kind === "meta" ? false : rects(occurrence).some((rect) => intersects(rect, bounds));
|
|
638
|
+
if (onScreen !== occurrence.onScreen) {
|
|
639
|
+
occurrence.onScreen = onScreen;
|
|
640
|
+
changed = true;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return changed;
|
|
644
|
+
}
|
|
645
|
+
function start() {
|
|
646
|
+
const changed = /* @__PURE__ */ new Set();
|
|
647
|
+
scanTree(document, changed);
|
|
648
|
+
observer = new MutationObserver(onMutations);
|
|
649
|
+
observer.observe(document.documentElement, {
|
|
650
|
+
childList: true,
|
|
651
|
+
characterData: true,
|
|
652
|
+
subtree: true,
|
|
653
|
+
attributes: true,
|
|
654
|
+
attributeFilter: OBSERVED_ATTRS
|
|
655
|
+
});
|
|
656
|
+
return [...changed];
|
|
657
|
+
}
|
|
658
|
+
function stop() {
|
|
659
|
+
observer?.disconnect();
|
|
660
|
+
observer = null;
|
|
661
|
+
if (frame) cancelAnimationFrame(frame);
|
|
662
|
+
frame = 0;
|
|
663
|
+
pending = null;
|
|
664
|
+
}
|
|
665
|
+
const textSlotOf = (node) => {
|
|
666
|
+
const slot = textSlots.get(node);
|
|
667
|
+
return slot && slots.has(slot) ? slot : null;
|
|
668
|
+
};
|
|
669
|
+
const attributeSlotsOf = (el) => [
|
|
670
|
+
...controls.has(el) ? [controls.get(el)] : [],
|
|
671
|
+
...[...attributeSlots.get(el)?.values() ?? []].filter((slot) => slots.has(slot))
|
|
672
|
+
];
|
|
673
|
+
return { start, stop, refresh, slots: () => slots, occurrences, rects, measure, textSlotOf, attributeSlotsOf };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// js/in_context/canvas.js
|
|
677
|
+
var MARKER = "__dialectoInContext";
|
|
678
|
+
var visibilityOptions2 = { checkOpacity: true, checkVisibilityCSS: true };
|
|
679
|
+
function installCanvas({ transform: transform2, onResults = () => {
|
|
680
|
+
}, onSeen = () => {
|
|
681
|
+
} }) {
|
|
682
|
+
const proto = typeof CanvasRenderingContext2D === "undefined" ? null : CanvasRenderingContext2D.prototype;
|
|
683
|
+
if (!proto || proto.fillText[MARKER]) return { installed: false, occurrences: () => [], rects: () => [], measure: () => false };
|
|
684
|
+
const original = {
|
|
685
|
+
fillText: proto.fillText,
|
|
686
|
+
strokeText: proto.strokeText,
|
|
687
|
+
measureText: proto.measureText,
|
|
688
|
+
clearRect: proto.clearRect
|
|
689
|
+
};
|
|
690
|
+
const canvases = /* @__PURE__ */ new Map();
|
|
691
|
+
const stateFor = (canvas) => {
|
|
692
|
+
let state = canvases.get(canvas);
|
|
693
|
+
if (!state) canvases.set(canvas, state = { generation: 0, seen: /* @__PURE__ */ new Map() });
|
|
694
|
+
return state;
|
|
695
|
+
};
|
|
696
|
+
function boxOf(ctx, text3, x, y) {
|
|
697
|
+
const metrics = original.measureText.call(ctx, text3);
|
|
698
|
+
const m = ctx.getTransform();
|
|
699
|
+
const xs = [x - metrics.actualBoundingBoxLeft, x + metrics.actualBoundingBoxRight];
|
|
700
|
+
const ys = [y - metrics.actualBoundingBoxAscent, y + metrics.actualBoundingBoxDescent];
|
|
701
|
+
const corners = xs.flatMap((cx) => ys.map((cy) => [m.a * cx + m.c * cy + m.e, m.b * cx + m.d * cy + m.f]));
|
|
702
|
+
const px = corners.map((c) => c[0]);
|
|
703
|
+
const py = corners.map((c) => c[1]);
|
|
704
|
+
return { left: Math.min(...px), top: Math.min(...py), right: Math.max(...px), bottom: Math.max(...py) };
|
|
705
|
+
}
|
|
706
|
+
function record(ctx, source, drawn, x, y) {
|
|
707
|
+
const canvas = ctx.canvas;
|
|
708
|
+
if (!(canvas instanceof HTMLCanvasElement)) return;
|
|
709
|
+
const state = stateFor(canvas);
|
|
710
|
+
let box = null;
|
|
711
|
+
try {
|
|
712
|
+
box = boxOf(ctx, drawn, Number(x) || 0, Number(y) || 0);
|
|
713
|
+
} catch {
|
|
714
|
+
box = null;
|
|
715
|
+
}
|
|
716
|
+
for (const segment of flattenSegments(parseSegments(source))) {
|
|
717
|
+
const id = keyId(segment.domain, segment.key, segment.locale);
|
|
718
|
+
const known = state.seen.get(id);
|
|
719
|
+
const revived = !known || known.generation < state.generation - 1;
|
|
720
|
+
state.seen.set(id, {
|
|
721
|
+
domain: segment.domain,
|
|
722
|
+
key: segment.key,
|
|
723
|
+
locale: segment.locale,
|
|
724
|
+
kind: "canvas",
|
|
725
|
+
text: segmentText(source, segment),
|
|
726
|
+
canvas,
|
|
727
|
+
box,
|
|
728
|
+
generation: state.generation,
|
|
729
|
+
onScreen: known?.onScreen ?? false
|
|
730
|
+
});
|
|
731
|
+
if (revived) onSeen();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function wrapDraw(name) {
|
|
735
|
+
const draw = original[name];
|
|
736
|
+
const wrapped = function(text3, x, y, ...rest) {
|
|
737
|
+
if (typeof text3 !== "string" || text3.indexOf(OPEN) < 0) return draw.call(this, text3, x, y, ...rest);
|
|
738
|
+
let drawn = text3;
|
|
739
|
+
try {
|
|
740
|
+
const { value, results } = transform2(text3);
|
|
741
|
+
drawn = stripMarks(value);
|
|
742
|
+
if (results.length) onResults(results);
|
|
743
|
+
record(this, text3, drawn, x, y);
|
|
744
|
+
} catch {
|
|
745
|
+
drawn = stripMarks(text3);
|
|
746
|
+
}
|
|
747
|
+
return draw.call(this, drawn, x, y, ...rest);
|
|
748
|
+
};
|
|
749
|
+
wrapped[MARKER] = true;
|
|
750
|
+
return wrapped;
|
|
751
|
+
}
|
|
752
|
+
proto.fillText = wrapDraw("fillText");
|
|
753
|
+
proto.strokeText = wrapDraw("strokeText");
|
|
754
|
+
proto.measureText = function(text3) {
|
|
755
|
+
if (typeof text3 !== "string" || text3.indexOf(OPEN) < 0) return original.measureText.call(this, text3);
|
|
756
|
+
try {
|
|
757
|
+
return original.measureText.call(this, stripMarks(transform2(text3).value));
|
|
758
|
+
} catch {
|
|
759
|
+
return original.measureText.call(this, stripMarks(text3));
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
proto.clearRect = function(x, y, w, h) {
|
|
763
|
+
const canvas = this.canvas;
|
|
764
|
+
if (canvases.has(canvas) && w >= canvas.width && h >= canvas.height) canvases.get(canvas).generation += 1;
|
|
765
|
+
return original.clearRect.call(this, x, y, w, h);
|
|
766
|
+
};
|
|
767
|
+
function entries() {
|
|
768
|
+
const out = [];
|
|
769
|
+
for (const [canvas, state] of canvases) {
|
|
770
|
+
if (!canvas.isConnected) {
|
|
771
|
+
canvases.delete(canvas);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
for (const [id, entry] of state.seen) {
|
|
775
|
+
if (entry.generation < state.generation - 1) state.seen.delete(id);
|
|
776
|
+
else out.push(entry);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return out;
|
|
780
|
+
}
|
|
781
|
+
function rects(entry) {
|
|
782
|
+
const { canvas, box } = entry;
|
|
783
|
+
const visible2 = canvas.checkVisibility ? canvas.checkVisibility(visibilityOptions2) : true;
|
|
784
|
+
if (!canvas.isConnected || !visible2) return [];
|
|
785
|
+
const frame = canvas.getBoundingClientRect();
|
|
786
|
+
if (!(frame.width + frame.height > 0)) return [];
|
|
787
|
+
if (!box || !canvas.width || !canvas.height) return [frame];
|
|
788
|
+
const sx = frame.width / canvas.width;
|
|
789
|
+
const sy = frame.height / canvas.height;
|
|
790
|
+
return [
|
|
791
|
+
{
|
|
792
|
+
left: frame.left + box.left * sx,
|
|
793
|
+
top: frame.top + box.top * sy,
|
|
794
|
+
right: frame.left + box.right * sx,
|
|
795
|
+
bottom: frame.top + box.bottom * sy,
|
|
796
|
+
width: (box.right - box.left) * sx,
|
|
797
|
+
height: (box.bottom - box.top) * sy
|
|
798
|
+
}
|
|
799
|
+
];
|
|
800
|
+
}
|
|
801
|
+
function measure(viewport) {
|
|
802
|
+
let changed = false;
|
|
803
|
+
for (const entry of entries()) {
|
|
804
|
+
const onScreen = rects(entry).some((rect) => intersects(rect, viewport));
|
|
805
|
+
if (onScreen !== entry.onScreen) {
|
|
806
|
+
entry.onScreen = onScreen;
|
|
807
|
+
changed = true;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return changed;
|
|
811
|
+
}
|
|
812
|
+
return { installed: true, occurrences: entries, rects, measure };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// js/in_context/click.js
|
|
816
|
+
var CONTROL_TAGS = /* @__PURE__ */ new Set([
|
|
817
|
+
"button",
|
|
818
|
+
"input",
|
|
819
|
+
"select",
|
|
820
|
+
"textarea",
|
|
821
|
+
"label",
|
|
822
|
+
"summary",
|
|
823
|
+
"option",
|
|
824
|
+
"audio",
|
|
825
|
+
"video",
|
|
826
|
+
"canvas",
|
|
827
|
+
"iframe",
|
|
828
|
+
"embed",
|
|
829
|
+
"object"
|
|
830
|
+
]);
|
|
831
|
+
var CONTROL_ROLES = /* @__PURE__ */ new Set([
|
|
832
|
+
"button",
|
|
833
|
+
"link",
|
|
834
|
+
"tab",
|
|
835
|
+
"menuitem",
|
|
836
|
+
"menuitemcheckbox",
|
|
837
|
+
"menuitemradio",
|
|
838
|
+
"option",
|
|
839
|
+
"checkbox",
|
|
840
|
+
"radio",
|
|
841
|
+
"switch",
|
|
842
|
+
"slider",
|
|
843
|
+
"combobox",
|
|
844
|
+
"treeitem"
|
|
845
|
+
]);
|
|
846
|
+
var CLICK_ATTRS = ["onclick", "phx-click", "x-on:click", "@click", "v-on:click"];
|
|
847
|
+
var SHOWN_ATTRS = /* @__PURE__ */ new Set(["placeholder", "value"]);
|
|
848
|
+
function isControl(el) {
|
|
849
|
+
const tag = el.localName;
|
|
850
|
+
if (CONTROL_TAGS.has(tag)) return true;
|
|
851
|
+
if (tag === "a" && el.hasAttribute("href")) return true;
|
|
852
|
+
if (CONTROL_ROLES.has(el.getAttribute("role"))) return true;
|
|
853
|
+
const editable = el.getAttribute("contenteditable");
|
|
854
|
+
if (editable !== null && editable !== "false") return true;
|
|
855
|
+
if (el.getAttribute("draggable") === "true") return true;
|
|
856
|
+
return CLICK_ATTRS.some((attr) => el.hasAttribute(attr));
|
|
857
|
+
}
|
|
858
|
+
var computedCursor = (el) => getComputedStyle(el).cursor;
|
|
859
|
+
function actsOnClick(el, cursorOf = computedCursor) {
|
|
860
|
+
if (!el) return false;
|
|
861
|
+
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
|
|
862
|
+
if (isControl(node)) return true;
|
|
863
|
+
}
|
|
864
|
+
return cursorOf(el) === "pointer";
|
|
865
|
+
}
|
|
866
|
+
function shownOnPage(occurrence) {
|
|
867
|
+
if (occurrence.kind === "canvas") return true;
|
|
868
|
+
const slot = occurrence.slot;
|
|
869
|
+
if (!slot) return false;
|
|
870
|
+
if (slot.type === "text") return slot.kind === "text";
|
|
871
|
+
if (slot.type === "control") return true;
|
|
872
|
+
return SHOWN_ATTRS.has(slot.attr);
|
|
873
|
+
}
|
|
874
|
+
var elementOf = (occurrence) => occurrence.kind === "canvas" ? occurrence.canvas : occurrence.slot?.element() ?? null;
|
|
875
|
+
|
|
876
|
+
// js/in_context/highlight.js
|
|
877
|
+
var TAB_HEIGHT = 22;
|
|
878
|
+
var TAB_DELAY = 220;
|
|
879
|
+
var CSS = `
|
|
880
|
+
.layer { position: fixed; inset: 0; pointer-events: none; z-index: 2147483646; overflow: hidden; }
|
|
881
|
+
.box {
|
|
882
|
+
position: absolute; left: 0; top: 0; display: none; box-sizing: border-box; border-radius: 6px;
|
|
883
|
+
background: rgba(62, 154, 209, 0.14);
|
|
884
|
+
box-shadow: 0 0 0 2px #2b7cad, 0 0 0 3.5px rgba(255, 255, 255, 0.85);
|
|
885
|
+
opacity: 0; transition: opacity 140ms ease-out;
|
|
886
|
+
}
|
|
887
|
+
.box.on { opacity: 1; }
|
|
888
|
+
.box.hover { background: rgba(28, 92, 134, 0.1); box-shadow: 0 0 0 2px #1c5c86, 0 0 0 3.5px rgba(255, 255, 255, 0.85); }
|
|
889
|
+
.box.reveal {
|
|
890
|
+
border-radius: 4px; background: rgba(62, 154, 209, 0.07);
|
|
891
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6); outline: 1.5px dashed rgba(28, 92, 134, 0.75); outline-offset: 0;
|
|
892
|
+
}
|
|
893
|
+
.box.pulse::after {
|
|
894
|
+
content: ""; position: absolute; inset: -2px; border-radius: inherit; border: 2px solid #3e9ad1;
|
|
895
|
+
animation: pulse 700ms ease-out 1 forwards;
|
|
896
|
+
}
|
|
897
|
+
@keyframes pulse { from { opacity: 0.9; transform: scale(1); } to { opacity: 0; transform: scale(1.06, 1.3); } }
|
|
898
|
+
.tab {
|
|
899
|
+
position: absolute; left: 0; top: 0; display: none; align-items: center; gap: 4px;
|
|
900
|
+
height: ${TAB_HEIGHT}px; margin: 0; padding: 0 8px 0 6px; border: 0; border-radius: 6px 6px 0 0;
|
|
901
|
+
background: #1c5c86; color: #fff; box-shadow: 0 -1.5px 0 1.5px rgba(255, 255, 255, 0.85);
|
|
902
|
+
font: 600 11.5px/1 system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: 0.01em;
|
|
903
|
+
pointer-events: auto; cursor: pointer; opacity: 0; transition: opacity 120ms ease-out, background-color 120ms;
|
|
904
|
+
}
|
|
905
|
+
.tab.below { border-radius: 0 0 6px 6px; box-shadow: 0 1.5px 0 1.5px rgba(255, 255, 255, 0.85); }
|
|
906
|
+
.tab.on { opacity: 1; }
|
|
907
|
+
.tab:hover { background: #164a6c; }
|
|
908
|
+
.tab svg { width: 12px; height: 12px; flex: none; }
|
|
909
|
+
@media (prefers-reduced-motion: reduce) {
|
|
910
|
+
.box, .tab { transition: none; }
|
|
911
|
+
.box.pulse::after { animation: none; display: none; }
|
|
912
|
+
}
|
|
913
|
+
`;
|
|
914
|
+
var PENCIL = '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M13.6 3.4a1.9 1.9 0 0 1 2.7 0l.3.3a1.9 1.9 0 0 1 0 2.7L7.3 15.7 3 17l1.3-4.3 9.3-9.3Z" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg>';
|
|
915
|
+
var PAD_X = 3;
|
|
916
|
+
var PAD_Y = 0;
|
|
917
|
+
function createHighlighter({ root, scanner, canvas, getViewport, onTab = () => {
|
|
918
|
+
} }) {
|
|
919
|
+
const style = document.createElement("style");
|
|
920
|
+
style.textContent = CSS;
|
|
921
|
+
const layer = document.createElement("div");
|
|
922
|
+
layer.className = "layer";
|
|
923
|
+
layer.setAttribute("aria-hidden", "true");
|
|
924
|
+
const tab = document.createElement("button");
|
|
925
|
+
tab.type = "button";
|
|
926
|
+
tab.className = "tab";
|
|
927
|
+
tab.tabIndex = -1;
|
|
928
|
+
tab.innerHTML = `${PENCIL}<span>Edit</span>`;
|
|
929
|
+
layer.append(tab);
|
|
930
|
+
root.append(style, layer);
|
|
931
|
+
const pool = [];
|
|
932
|
+
let target = null;
|
|
933
|
+
let hover = null;
|
|
934
|
+
let hoverTab = false;
|
|
935
|
+
let tabReady = false;
|
|
936
|
+
let tabTimer = 0;
|
|
937
|
+
let reveal = false;
|
|
938
|
+
let frame = 0;
|
|
939
|
+
let pulseNext = false;
|
|
940
|
+
tab.addEventListener("mousedown", (event) => event.preventDefault());
|
|
941
|
+
tab.addEventListener("click", (event) => {
|
|
942
|
+
event.preventDefault();
|
|
943
|
+
event.stopPropagation();
|
|
944
|
+
onTab();
|
|
945
|
+
});
|
|
946
|
+
const boxAt = (index) => {
|
|
947
|
+
while (pool.length <= index) {
|
|
948
|
+
const el = document.createElement("div");
|
|
949
|
+
el.className = "box";
|
|
950
|
+
layer.insertBefore(el, tab);
|
|
951
|
+
pool.push(el);
|
|
952
|
+
}
|
|
953
|
+
return pool[index];
|
|
954
|
+
};
|
|
955
|
+
function separateLines(rects) {
|
|
956
|
+
const lines = rects.map((r) => ({ left: r.left, right: r.right, top: r.top, bottom: r.bottom })).sort((a, b) => a.top - b.top);
|
|
957
|
+
for (let i = 1; i < lines.length; i++) {
|
|
958
|
+
const prev = lines[i - 1];
|
|
959
|
+
const line = lines[i];
|
|
960
|
+
if (line.top < prev.bottom && line.left < prev.right && line.right > prev.left) {
|
|
961
|
+
const mid = (line.top + prev.bottom) / 2;
|
|
962
|
+
prev.bottom = mid;
|
|
963
|
+
line.top = mid;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return lines.map((l) => ({ ...l, width: l.right - l.left, height: l.bottom - l.top }));
|
|
967
|
+
}
|
|
968
|
+
const rectsOf = (occurrence) => separateLines(occurrence.kind === "canvas" ? canvas.rects(occurrence) : scanner.rects(occurrence));
|
|
969
|
+
const all = () => [...scanner.occurrences(), ...canvas.occurrences()];
|
|
970
|
+
const matching = ({ domain, key }) => all().filter((occ) => occ.domain === domain && occ.key === key);
|
|
971
|
+
const isTarget = (occ) => target !== null && occ.domain === target.domain && occ.key === target.key;
|
|
972
|
+
function placeTab(rects, bounds) {
|
|
973
|
+
const show = hover && hoverTab && tabReady && rects.length > 0;
|
|
974
|
+
if (!show) {
|
|
975
|
+
tab.style.display = "none";
|
|
976
|
+
tab.classList.remove("on");
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const first = rects[0];
|
|
980
|
+
const lastLine = rects[rects.length - 1];
|
|
981
|
+
const below = first.top - PAD_Y - TAB_HEIGHT < bounds.top;
|
|
982
|
+
const fresh = tab.style.display !== "flex";
|
|
983
|
+
tab.style.display = "flex";
|
|
984
|
+
tab.classList.toggle("below", below);
|
|
985
|
+
const width = tab.offsetWidth;
|
|
986
|
+
const left = Math.min(Math.max(first.left - PAD_X - 2, bounds.left + 2), bounds.right - width - 2);
|
|
987
|
+
const top = below ? lastLine.bottom + PAD_Y : first.top - PAD_Y - TAB_HEIGHT;
|
|
988
|
+
tab.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
|
|
989
|
+
if (fresh) requestAnimationFrame(() => tab.classList.add("on"));
|
|
990
|
+
}
|
|
991
|
+
function paint() {
|
|
992
|
+
const bounds = getViewport();
|
|
993
|
+
const drawn = [];
|
|
994
|
+
if (reveal) {
|
|
995
|
+
for (const occurrence of all()) {
|
|
996
|
+
if (!occurrence.onScreen || occurrence === hover || isTarget(occurrence) || !shownOnPage(occurrence)) continue;
|
|
997
|
+
for (const rect of rectsOf(occurrence)) if (intersects(rect, bounds)) drawn.push({ rect, kind: "reveal" });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (target) {
|
|
1001
|
+
for (const occurrence of matching(target)) {
|
|
1002
|
+
for (const rect of rectsOf(occurrence)) if (intersects(rect, bounds)) drawn.push({ rect, kind: "target" });
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const hoverRects = hover ? rectsOf(hover).filter((rect) => intersects(rect, bounds)) : [];
|
|
1006
|
+
for (const rect of hoverRects) drawn.push({ rect, kind: "hover" });
|
|
1007
|
+
drawn.forEach(({ rect, kind }, index) => {
|
|
1008
|
+
const el = boxAt(index);
|
|
1009
|
+
const fresh = el.style.display !== "block";
|
|
1010
|
+
el.style.display = "block";
|
|
1011
|
+
el.style.transform = `translate(${Math.round(rect.left - PAD_X)}px, ${Math.round(rect.top - PAD_Y)}px)`;
|
|
1012
|
+
el.style.width = `${Math.round(rect.width + PAD_X * 2)}px`;
|
|
1013
|
+
el.style.height = `${Math.round(rect.height + PAD_Y * 2)}px`;
|
|
1014
|
+
el.classList.toggle("hover", kind === "hover");
|
|
1015
|
+
el.classList.toggle("reveal", kind === "reveal");
|
|
1016
|
+
if (fresh) requestAnimationFrame(() => el.classList.add("on"));
|
|
1017
|
+
if (pulseNext && kind === "target") {
|
|
1018
|
+
el.classList.remove("pulse");
|
|
1019
|
+
void el.offsetWidth;
|
|
1020
|
+
el.classList.add("pulse");
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
pulseNext = false;
|
|
1024
|
+
for (let i = drawn.length; i < pool.length; i++) {
|
|
1025
|
+
pool[i].style.display = "none";
|
|
1026
|
+
pool[i].classList.remove("on", "pulse");
|
|
1027
|
+
}
|
|
1028
|
+
placeTab(hoverRects, bounds);
|
|
1029
|
+
}
|
|
1030
|
+
const active = () => Boolean(target || hover || reveal);
|
|
1031
|
+
function tick() {
|
|
1032
|
+
frame = 0;
|
|
1033
|
+
paint();
|
|
1034
|
+
if (active()) frame = requestAnimationFrame(tick);
|
|
1035
|
+
}
|
|
1036
|
+
const schedule = () => {
|
|
1037
|
+
if (!frame) frame = requestAnimationFrame(tick);
|
|
1038
|
+
};
|
|
1039
|
+
function reducedMotion() {
|
|
1040
|
+
return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
|
|
1041
|
+
}
|
|
1042
|
+
const inView = (rect, bounds) => rect.top >= bounds.top && rect.bottom <= bounds.bottom && rect.left >= bounds.left && rect.right <= bounds.right;
|
|
1043
|
+
function scrollTo(occurrences) {
|
|
1044
|
+
const bounds = getViewport();
|
|
1045
|
+
if (occurrences.some((occ) => rectsOf(occ).some((rect) => inView(rect, bounds)))) return;
|
|
1046
|
+
const visible2 = occurrences.find((occ) => rectsOf(occ).length > 0) ?? occurrences[0];
|
|
1047
|
+
if (!visible2) return;
|
|
1048
|
+
const el = visible2.kind === "canvas" ? visible2.canvas : visible2.slot.element();
|
|
1049
|
+
el?.scrollIntoView({ block: "center", inline: "nearest", behavior: reducedMotion() ? "auto" : "smooth" });
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
/** Outline every occurrence of a message; `null` clears. */
|
|
1053
|
+
setTarget(next, { scroll = false } = {}) {
|
|
1054
|
+
const changed = !next !== !target || next && target && (next.domain !== target.domain || next.key !== target.key);
|
|
1055
|
+
target = next;
|
|
1056
|
+
if (next && changed) pulseNext = true;
|
|
1057
|
+
if (next && scroll) scrollTo(matching(next));
|
|
1058
|
+
if (!next) paint();
|
|
1059
|
+
schedule();
|
|
1060
|
+
},
|
|
1061
|
+
/** The string under the pointer; `tab` offers its Edit tab once the pointer rests. */
|
|
1062
|
+
setHover(occurrence, { tab: withTab = false } = {}) {
|
|
1063
|
+
if (occurrence === hover && withTab === hoverTab) return;
|
|
1064
|
+
if (!occurrence || !withTab) tabReady = false;
|
|
1065
|
+
if (tabTimer) clearTimeout(tabTimer);
|
|
1066
|
+
tabTimer = 0;
|
|
1067
|
+
hover = occurrence;
|
|
1068
|
+
hoverTab = withTab;
|
|
1069
|
+
if (hover && hoverTab && !tabReady) {
|
|
1070
|
+
tabTimer = setTimeout(() => {
|
|
1071
|
+
tabTimer = 0;
|
|
1072
|
+
tabReady = true;
|
|
1073
|
+
schedule();
|
|
1074
|
+
}, TAB_DELAY);
|
|
1075
|
+
}
|
|
1076
|
+
if (!active()) paint();
|
|
1077
|
+
schedule();
|
|
1078
|
+
},
|
|
1079
|
+
/** Faintly outline every editable string on screen. */
|
|
1080
|
+
setReveal(on) {
|
|
1081
|
+
if (on === reveal) return;
|
|
1082
|
+
reveal = on;
|
|
1083
|
+
if (!active()) paint();
|
|
1084
|
+
schedule();
|
|
1085
|
+
},
|
|
1086
|
+
/** True when the event happened on the Edit tab. */
|
|
1087
|
+
isTabEvent: (event) => event.composedPath?.().includes(tab) === true,
|
|
1088
|
+
schedule,
|
|
1089
|
+
isActive: active,
|
|
1090
|
+
destroy() {
|
|
1091
|
+
if (frame) cancelAnimationFrame(frame);
|
|
1092
|
+
if (tabTimer) clearTimeout(tabTimer);
|
|
1093
|
+
layer.remove();
|
|
1094
|
+
style.remove();
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// js/in_context/marking.js
|
|
1100
|
+
var HEARTBEAT_MS = 3e4;
|
|
1101
|
+
var RELOAD_GUARD_MS = 3e4;
|
|
1102
|
+
var RELOADED_KEY = "dialecto-in-context:marking-reload";
|
|
1103
|
+
function sessionStore() {
|
|
1104
|
+
try {
|
|
1105
|
+
return globalThis.sessionStorage ?? null;
|
|
1106
|
+
} catch {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function readReload(storage) {
|
|
1111
|
+
try {
|
|
1112
|
+
const value = JSON.parse(storage.getItem(RELOADED_KEY));
|
|
1113
|
+
return value && typeof value.url === "string" && typeof value.at === "number" ? value : null;
|
|
1114
|
+
} catch {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function writeReload(storage, value) {
|
|
1119
|
+
try {
|
|
1120
|
+
storage.setItem(RELOADED_KEY, JSON.stringify(value));
|
|
1121
|
+
return true;
|
|
1122
|
+
} catch {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function createMarking(endpoint, {
|
|
1127
|
+
hasMarkers = () => true,
|
|
1128
|
+
reload = () => globalThis.location?.reload(),
|
|
1129
|
+
fetchImpl = globalThis.fetch?.bind(globalThis),
|
|
1130
|
+
storage = sessionStore(),
|
|
1131
|
+
now = () => Date.now(),
|
|
1132
|
+
url = () => `${globalThis.location?.pathname ?? ""}${globalThis.location?.search ?? ""}`,
|
|
1133
|
+
timers = globalThis
|
|
1134
|
+
} = {}) {
|
|
1135
|
+
if (!endpoint || !fetchImpl) return { enabled: false, setOpen: async () => {
|
|
1136
|
+
}, isOpen: () => false };
|
|
1137
|
+
let open = false;
|
|
1138
|
+
let heartbeat = 0;
|
|
1139
|
+
async function send(on) {
|
|
1140
|
+
try {
|
|
1141
|
+
const response = await fetchImpl(endpoint, {
|
|
1142
|
+
method: "POST",
|
|
1143
|
+
headers: { "content-type": "application/json" },
|
|
1144
|
+
body: JSON.stringify({ on }),
|
|
1145
|
+
credentials: "same-origin",
|
|
1146
|
+
// Turning it off must arrive even when the page goes away right after.
|
|
1147
|
+
keepalive: !on
|
|
1148
|
+
});
|
|
1149
|
+
return Boolean(response?.ok);
|
|
1150
|
+
} catch {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function reloadOnce() {
|
|
1155
|
+
if (!storage) return false;
|
|
1156
|
+
const here = url();
|
|
1157
|
+
const last = readReload(storage);
|
|
1158
|
+
if (last && last.url === here && now() - last.at < RELOAD_GUARD_MS) return false;
|
|
1159
|
+
if (!writeReload(storage, { url: here, at: now() })) return false;
|
|
1160
|
+
reload();
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
enabled: true,
|
|
1165
|
+
isOpen: () => open,
|
|
1166
|
+
/** The editor opened or closed; resolves once the dev server answered. */
|
|
1167
|
+
async setOpen(next) {
|
|
1168
|
+
if (next === open) return;
|
|
1169
|
+
open = next;
|
|
1170
|
+
timers.clearInterval(heartbeat);
|
|
1171
|
+
heartbeat = 0;
|
|
1172
|
+
if (!next) {
|
|
1173
|
+
await send(false);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
heartbeat = timers.setInterval(() => send(true), HEARTBEAT_MS);
|
|
1177
|
+
if (await send(true) && open && !hasMarkers()) reloadOnce();
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// js/in_context/overrides.js
|
|
1183
|
+
var DEBOUNCE_MS = 400;
|
|
1184
|
+
var keyOf = (edit) => [edit.domain, edit.locale, edit.key].join("");
|
|
1185
|
+
function overrideOf({ domain, key, locale, to, forms, msgid_plural: plural }) {
|
|
1186
|
+
return Array.isArray(forms) && typeof plural === "string" ? { domain, key, locale, forms } : { domain, key, locale, to };
|
|
1187
|
+
}
|
|
1188
|
+
function createOverrideSync(endpoint, { fetchImpl = globalThis.fetch?.bind(globalThis) } = {}) {
|
|
1189
|
+
if (!endpoint || !fetchImpl) return { enabled: false, push() {
|
|
1190
|
+
} };
|
|
1191
|
+
let timer = 0;
|
|
1192
|
+
let lastSent = null;
|
|
1193
|
+
let queued = null;
|
|
1194
|
+
async function send(payload) {
|
|
1195
|
+
try {
|
|
1196
|
+
await fetchImpl(endpoint, {
|
|
1197
|
+
method: "POST",
|
|
1198
|
+
headers: { "content-type": "application/json" },
|
|
1199
|
+
body: payload,
|
|
1200
|
+
credentials: "same-origin",
|
|
1201
|
+
keepalive: true
|
|
1202
|
+
});
|
|
1203
|
+
lastSent = payload;
|
|
1204
|
+
} catch {
|
|
1205
|
+
lastSent = null;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
enabled: true,
|
|
1210
|
+
/** Debounced full replacement of the override set. */
|
|
1211
|
+
push(edits) {
|
|
1212
|
+
const list = edits.map(overrideOf).sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
|
|
1213
|
+
const payload = JSON.stringify({ edits: list });
|
|
1214
|
+
if (payload === lastSent && timer === 0) return;
|
|
1215
|
+
queued = payload;
|
|
1216
|
+
clearTimeout(timer);
|
|
1217
|
+
timer = setTimeout(() => {
|
|
1218
|
+
timer = 0;
|
|
1219
|
+
if (queued !== null && queued !== lastSent) send(queued);
|
|
1220
|
+
}, DEBOUNCE_MS);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// js/in_context/url.js
|
|
1226
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
1227
|
+
var isLoopback = (hostname) => LOOPBACK.has(hostname);
|
|
1228
|
+
function sidebarUrl(base, { site, project, locale } = {}) {
|
|
1229
|
+
const prefix = base.pathname.replace(/\/+$/, "");
|
|
1230
|
+
const params = new URLSearchParams();
|
|
1231
|
+
if (site) params.set("site", site);
|
|
1232
|
+
if (project) params.set("project", project);
|
|
1233
|
+
if (locale) params.set("locale", locale);
|
|
1234
|
+
const query = params.toString();
|
|
1235
|
+
return `${base.origin}${prefix}/in-context${query ? `?${query}` : ""}`;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// js/in_context/panel.js
|
|
1239
|
+
var PANEL_WIDTH = 380;
|
|
1240
|
+
var NARROW_BREAKPOINT = 720;
|
|
1241
|
+
var STORAGE_KEY = "dialecto-in-context:open";
|
|
1242
|
+
var DOCKED_CSS = `
|
|
1243
|
+
@media (min-width: ${NARROW_BREAKPOINT + 1}px) { html { margin-right: ${PANEL_WIDTH}px; } }
|
|
1244
|
+
`;
|
|
1245
|
+
var CSS2 = `
|
|
1246
|
+
:host { all: initial; }
|
|
1247
|
+
* { box-sizing: border-box; }
|
|
1248
|
+
.ui {
|
|
1249
|
+
--cream: #fbf4e9; --paper: #ffffff; --edge: #e4d4b8; --ink: #33291f; --ink2: #6a5e4b;
|
|
1250
|
+
--terra: #ba563e; --terra-dk: #ad4b35; --sky: #3e9ad1; --sea: #1c5c86; --danger: #b5402c; --danger-bg: #f7e2dc;
|
|
1251
|
+
--shadow: 0 8px 22px -6px rgba(28, 49, 63, 0.35), 0 2px 5px rgba(28, 49, 63, 0.18);
|
|
1252
|
+
font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--ink);
|
|
1253
|
+
}
|
|
1254
|
+
@media (prefers-color-scheme: dark) {
|
|
1255
|
+
.ui { --cream: #1b1610; --paper: #271f17; --edge: #463b28; --ink: #f3ecdb; --ink2: #cabb9f; --sea: #4f93c2;
|
|
1256
|
+
--danger: #e0795f; --danger-bg: #3a201a; }
|
|
1257
|
+
}
|
|
1258
|
+
button { font: inherit; cursor: pointer; }
|
|
1259
|
+
:focus-visible { outline: 3px solid var(--sky); outline-offset: 2px; }
|
|
1260
|
+
|
|
1261
|
+
.dock {
|
|
1262
|
+
position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;
|
|
1263
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
|
|
1264
|
+
animation: rise 260ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
1265
|
+
}
|
|
1266
|
+
.dock[hidden] { display: none; }
|
|
1267
|
+
.dock.left { right: auto; left: 16px; align-items: flex-start; }
|
|
1268
|
+
.dock.top { bottom: auto; top: 16px; }
|
|
1269
|
+
.dock-row { display: flex; gap: 8px; }
|
|
1270
|
+
.launcher, .stop {
|
|
1271
|
+
display: inline-flex; align-items: center; gap: 8px; height: 40px; border-radius: 999px;
|
|
1272
|
+
font-weight: 600; letter-spacing: 0.005em; box-shadow: var(--shadow);
|
|
1273
|
+
transition: background-color 120ms, border-color 120ms, transform 120ms, box-shadow 120ms;
|
|
1274
|
+
}
|
|
1275
|
+
.launcher { padding: 0 16px 0 13px; border: 0; background: var(--terra); color: #fff; }
|
|
1276
|
+
.launcher:hover { background: var(--terra-dk); transform: translateY(-1px); }
|
|
1277
|
+
.stop { padding: 0 16px; border: 1px solid var(--edge); background: var(--paper); color: var(--ink); }
|
|
1278
|
+
.stop:hover { border-color: var(--ink2); transform: translateY(-1px); }
|
|
1279
|
+
.stop:disabled { cursor: progress; opacity: 0.7; transform: none; }
|
|
1280
|
+
.launcher:active, .stop:active { transform: translateY(0); }
|
|
1281
|
+
.stop[hidden] { display: none; }
|
|
1282
|
+
.launcher svg { width: 16px; height: 16px; flex: none; }
|
|
1283
|
+
.stop-error {
|
|
1284
|
+
max-width: 280px; padding: 8px 12px; border-radius: 10px; background: var(--danger-bg); color: var(--danger);
|
|
1285
|
+
font-size: 13px; box-shadow: var(--shadow);
|
|
1286
|
+
}
|
|
1287
|
+
.stop-error[hidden] { display: none; }
|
|
1288
|
+
@keyframes rise { from { opacity: 0; transform: translateY(8px); } }
|
|
1289
|
+
|
|
1290
|
+
.panel {
|
|
1291
|
+
position: fixed; top: 0; right: 0; bottom: 0; z-index: 2147483647; display: flex; flex-direction: column;
|
|
1292
|
+
width: min(${PANEL_WIDTH}px, 100vw); background: var(--cream); border-left: 1px solid var(--edge);
|
|
1293
|
+
box-shadow: -14px 0 32px -18px rgba(28, 49, 63, 0.4);
|
|
1294
|
+
transform: translateX(105%); visibility: hidden;
|
|
1295
|
+
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0s linear 220ms;
|
|
1296
|
+
}
|
|
1297
|
+
.panel.open { transform: translateX(0); visibility: visible; transition-delay: 0s; }
|
|
1298
|
+
iframe { flex: 1; width: 100%; min-height: 0; border: 0; background: var(--cream); color-scheme: normal; }
|
|
1299
|
+
|
|
1300
|
+
.state {
|
|
1301
|
+
position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
1302
|
+
gap: 10px; padding: 32px 28px; text-align: center; background: var(--cream);
|
|
1303
|
+
}
|
|
1304
|
+
.state[hidden] { display: none; }
|
|
1305
|
+
.spinner {
|
|
1306
|
+
width: 22px; height: 22px; border-radius: 50%; border: 2.5px solid var(--edge); border-top-color: var(--sea);
|
|
1307
|
+
animation: spin 800ms linear infinite;
|
|
1308
|
+
}
|
|
1309
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1310
|
+
.muted { color: var(--ink2); }
|
|
1311
|
+
.badge {
|
|
1312
|
+
display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%;
|
|
1313
|
+
background: var(--danger-bg); color: var(--danger);
|
|
1314
|
+
}
|
|
1315
|
+
.badge svg { width: 22px; height: 22px; }
|
|
1316
|
+
h2 { margin: 4px 0 0; font-size: 17px; line-height: 1.3; font-weight: 650; }
|
|
1317
|
+
p { margin: 0; max-width: 32ch; }
|
|
1318
|
+
.hint { max-width: 36ch; font-size: 13px; color: var(--ink2); }
|
|
1319
|
+
code { font: 12.5px ui-monospace, "SF Mono", Menlo, monospace; padding: 1px 5px; border-radius: 4px;
|
|
1320
|
+
background: var(--paper); border: 1px solid var(--edge); overflow-wrap: anywhere; }
|
|
1321
|
+
.actions { display: flex; gap: 8px; margin-top: 8px; }
|
|
1322
|
+
.btn { height: 36px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--edge); background: var(--paper);
|
|
1323
|
+
color: var(--ink); font-weight: 600; }
|
|
1324
|
+
.btn:hover { border-color: var(--ink2); }
|
|
1325
|
+
.btn.primary { background: var(--terra); border-color: var(--terra); color: #fff; }
|
|
1326
|
+
.btn.primary:hover { background: var(--terra-dk); border-color: var(--terra-dk); }
|
|
1327
|
+
|
|
1328
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1329
|
+
.dock, .launcher, .stop, .panel { transition: none; animation: none; }
|
|
1330
|
+
.spinner { animation-duration: 2.4s; }
|
|
1331
|
+
}
|
|
1332
|
+
`;
|
|
1333
|
+
var PENCIL2 = '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M13.6 3.4a1.9 1.9 0 0 1 2.7 0l.3.3a1.9 1.9 0 0 1 0 2.7L7.3 15.7 3 17l1.3-4.3 9.3-9.3Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="m12 5 3 3" stroke="currentColor" stroke-width="1.7"/></svg>';
|
|
1334
|
+
var ALERT = '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 8v5m0 3.5h.01M10.3 3.9 2.6 17.2A2 2 0 0 0 4.3 20h15.4a2 2 0 0 0 1.7-2.8L13.7 3.9a2 2 0 0 0-3.4 0Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
1335
|
+
function readOpen() {
|
|
1336
|
+
try {
|
|
1337
|
+
return sessionStorage.getItem(STORAGE_KEY) === "1";
|
|
1338
|
+
} catch {
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function writeOpen(open) {
|
|
1343
|
+
try {
|
|
1344
|
+
sessionStorage.setItem(STORAGE_KEY, open ? "1" : "0");
|
|
1345
|
+
} catch {
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function createPanel({
|
|
1349
|
+
corner = "bottom-right",
|
|
1350
|
+
root,
|
|
1351
|
+
src,
|
|
1352
|
+
dialectoUrl,
|
|
1353
|
+
siteOrigin,
|
|
1354
|
+
onOpenChange = () => {
|
|
1355
|
+
},
|
|
1356
|
+
onFrameLoad = () => {
|
|
1357
|
+
},
|
|
1358
|
+
onRetry = () => {
|
|
1359
|
+
},
|
|
1360
|
+
onStop = null
|
|
1361
|
+
}) {
|
|
1362
|
+
const hostStyle = document.createElement("style");
|
|
1363
|
+
document.head.append(hostStyle);
|
|
1364
|
+
const style = document.createElement("style");
|
|
1365
|
+
style.textContent = CSS2;
|
|
1366
|
+
const ui = document.createElement("div");
|
|
1367
|
+
ui.className = "ui";
|
|
1368
|
+
ui.innerHTML = `
|
|
1369
|
+
<div class="dock">
|
|
1370
|
+
<p class="stop-error" role="alert" hidden>Couldn\u2019t stop editing. Try again.</p>
|
|
1371
|
+
<div class="dock-row">
|
|
1372
|
+
<button class="stop" type="button" hidden>Stop editing</button>
|
|
1373
|
+
<button class="launcher" type="button">${PENCIL2}<span>Edit text</span></button>
|
|
1374
|
+
</div>
|
|
1375
|
+
</div>
|
|
1376
|
+
<aside class="panel" role="complementary" aria-label="Dialecto editor" inert>
|
|
1377
|
+
<div class="state connecting" role="status">
|
|
1378
|
+
<div class="spinner"></div>
|
|
1379
|
+
<p class="muted">Connecting to Dialecto\u2026</p>
|
|
1380
|
+
</div>
|
|
1381
|
+
<div class="state failed" role="alert" hidden>
|
|
1382
|
+
<div class="badge">${ALERT}</div>
|
|
1383
|
+
<h2>Can\u2019t reach Dialecto</h2>
|
|
1384
|
+
<p class="muted">The editor didn\u2019t answer from <code class="url"></code></p>
|
|
1385
|
+
<p class="hint"></p>
|
|
1386
|
+
<div class="actions">
|
|
1387
|
+
<button class="btn primary retry" type="button">Try again</button>
|
|
1388
|
+
<button class="btn close" type="button">Close</button>
|
|
1389
|
+
</div>
|
|
1390
|
+
</div>
|
|
1391
|
+
</aside>`;
|
|
1392
|
+
root.append(style, ui);
|
|
1393
|
+
const dock = ui.querySelector(".dock");
|
|
1394
|
+
dock.classList.toggle("left", corner.endsWith("left"));
|
|
1395
|
+
dock.classList.toggle("top", corner.startsWith("top"));
|
|
1396
|
+
const launcher = ui.querySelector(".launcher");
|
|
1397
|
+
const stop = ui.querySelector(".stop");
|
|
1398
|
+
const stopError = ui.querySelector(".stop-error");
|
|
1399
|
+
const panel = ui.querySelector(".panel");
|
|
1400
|
+
const connecting = ui.querySelector(".connecting");
|
|
1401
|
+
const failed = ui.querySelector(".failed");
|
|
1402
|
+
ui.querySelector(".url").textContent = dialectoUrl;
|
|
1403
|
+
const code = (value) => Object.assign(document.createElement("code"), { textContent: value });
|
|
1404
|
+
ui.querySelector(".hint").append(
|
|
1405
|
+
...isLoopback(location.hostname) ? ["Check that Dialecto is running at ", code(dialectoUrl), "."] : ["Check that Dialecto is running, and that ", code(siteOrigin), " is one of the project\u2019s sites."]
|
|
1406
|
+
);
|
|
1407
|
+
let iframe = null;
|
|
1408
|
+
let open = false;
|
|
1409
|
+
function ensureFrame() {
|
|
1410
|
+
if (iframe) return;
|
|
1411
|
+
iframe = document.createElement("iframe");
|
|
1412
|
+
iframe.title = "Dialecto in-context editor";
|
|
1413
|
+
iframe.allow = "clipboard-write";
|
|
1414
|
+
iframe.referrerPolicy = "origin";
|
|
1415
|
+
iframe.addEventListener("load", () => onFrameLoad());
|
|
1416
|
+
iframe.src = src;
|
|
1417
|
+
panel.insertBefore(iframe, connecting);
|
|
1418
|
+
}
|
|
1419
|
+
function setOpen(next, { restored = false } = {}) {
|
|
1420
|
+
if (next === open) return;
|
|
1421
|
+
open = next;
|
|
1422
|
+
if (next) ensureFrame();
|
|
1423
|
+
panel.classList.toggle("open", next);
|
|
1424
|
+
panel.toggleAttribute("inert", !next);
|
|
1425
|
+
dock.hidden = next;
|
|
1426
|
+
hostStyle.textContent = next ? DOCKED_CSS : "";
|
|
1427
|
+
if (!restored) {
|
|
1428
|
+
writeOpen(next);
|
|
1429
|
+
(next ? iframe : launcher)?.focus({ preventScroll: true });
|
|
1430
|
+
}
|
|
1431
|
+
onOpenChange(next);
|
|
1432
|
+
}
|
|
1433
|
+
function showConnecting() {
|
|
1434
|
+
connecting.hidden = false;
|
|
1435
|
+
failed.hidden = true;
|
|
1436
|
+
}
|
|
1437
|
+
launcher.addEventListener("click", () => setOpen(true));
|
|
1438
|
+
if (typeof onStop === "function") {
|
|
1439
|
+
stop.hidden = false;
|
|
1440
|
+
stop.addEventListener("click", async () => {
|
|
1441
|
+
stop.disabled = true;
|
|
1442
|
+
stopError.hidden = true;
|
|
1443
|
+
stop.textContent = "Stopping\u2026";
|
|
1444
|
+
if (await onStop()) return;
|
|
1445
|
+
stop.disabled = false;
|
|
1446
|
+
stop.textContent = "Stop editing";
|
|
1447
|
+
stopError.hidden = false;
|
|
1448
|
+
stop.focus({ preventScroll: true });
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
ui.querySelector(".close").addEventListener("click", () => setOpen(false));
|
|
1452
|
+
ui.querySelector(".retry").addEventListener("click", () => {
|
|
1453
|
+
showConnecting();
|
|
1454
|
+
onRetry();
|
|
1455
|
+
if (iframe) iframe.src = src;
|
|
1456
|
+
});
|
|
1457
|
+
if (readOpen()) setOpen(true, { restored: true });
|
|
1458
|
+
return {
|
|
1459
|
+
open: () => setOpen(true),
|
|
1460
|
+
close: () => setOpen(false),
|
|
1461
|
+
isOpen: () => open,
|
|
1462
|
+
frameWindow: () => iframe?.contentWindow ?? null,
|
|
1463
|
+
showConnecting,
|
|
1464
|
+
/** Names the shortcut in the launcher's tooltip, once. */
|
|
1465
|
+
setShortcut(label) {
|
|
1466
|
+
launcher.title = label ? `Edit text (${label} shows or hides Dialecto)` : "Edit text";
|
|
1467
|
+
},
|
|
1468
|
+
showReady() {
|
|
1469
|
+
connecting.hidden = true;
|
|
1470
|
+
failed.hidden = true;
|
|
1471
|
+
},
|
|
1472
|
+
showError() {
|
|
1473
|
+
connecting.hidden = true;
|
|
1474
|
+
failed.hidden = false;
|
|
1475
|
+
if (open) failed.querySelector(".retry").focus({ preventScroll: true });
|
|
1476
|
+
},
|
|
1477
|
+
/** Right edge of the page area not covered by the sidebar. */
|
|
1478
|
+
usableRight: () => open && window.innerWidth > NARROW_BREAKPOINT ? window.innerWidth - PANEL_WIDTH : window.innerWidth,
|
|
1479
|
+
destroy() {
|
|
1480
|
+
hostStyle.remove();
|
|
1481
|
+
style.remove();
|
|
1482
|
+
ui.remove();
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// js/in_context/picker.js
|
|
1488
|
+
var PICK_CLASS = "dialecto-in-context-picking";
|
|
1489
|
+
var OPENS_ATTR = "data-dialecto-opens";
|
|
1490
|
+
var SLOP = 2;
|
|
1491
|
+
var DRAG = 4;
|
|
1492
|
+
var REVEAL_DELAY = 180;
|
|
1493
|
+
var CSS3 = `
|
|
1494
|
+
html.${PICK_CLASS}, html.${PICK_CLASS} * { cursor: crosshair !important; }
|
|
1495
|
+
[${OPENS_ATTR}], [${OPENS_ATTR}] * { cursor: pointer !important; }
|
|
1496
|
+
`;
|
|
1497
|
+
function caretFromPoint(x, y) {
|
|
1498
|
+
if (document.caretPositionFromPoint) {
|
|
1499
|
+
const position = document.caretPositionFromPoint(x, y);
|
|
1500
|
+
return position ? { node: position.offsetNode, offset: position.offset } : null;
|
|
1501
|
+
}
|
|
1502
|
+
if (document.caretRangeFromPoint) {
|
|
1503
|
+
const range = document.caretRangeFromPoint(x, y);
|
|
1504
|
+
return range ? { node: range.startContainer, offset: range.startOffset } : null;
|
|
1505
|
+
}
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
var contains = (rect, x, y) => x >= rect.left - SLOP && x <= rect.right + SLOP && y >= rect.top - SLOP && y <= rect.bottom + SLOP;
|
|
1509
|
+
var plainClick = (event) => event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey && event.detail <= 1;
|
|
1510
|
+
function createPicker({ scanner, canvas, highlighter, onPick, isInside, isEditing = () => false }) {
|
|
1511
|
+
let pickMode = false;
|
|
1512
|
+
let paused = false;
|
|
1513
|
+
let altReveal = false;
|
|
1514
|
+
let revealTimer = 0;
|
|
1515
|
+
let last = { x: -1, y: -1, target: null, altKey: false };
|
|
1516
|
+
let press = null;
|
|
1517
|
+
let hoverFrame = 0;
|
|
1518
|
+
let hover = null;
|
|
1519
|
+
let hoverOpens = false;
|
|
1520
|
+
let marked = null;
|
|
1521
|
+
const style = document.createElement("style");
|
|
1522
|
+
style.textContent = CSS3;
|
|
1523
|
+
document.head.append(style);
|
|
1524
|
+
function textOccurrenceAt(x, y) {
|
|
1525
|
+
const caret = caretFromPoint(x, y);
|
|
1526
|
+
if (!caret || caret.node?.nodeType !== Node.TEXT_NODE) return null;
|
|
1527
|
+
const slot = scanner.textSlotOf(caret.node);
|
|
1528
|
+
if (!slot) return null;
|
|
1529
|
+
const candidates = slot.occurrences.filter(({ segment }) => segment.start <= caret.offset && caret.offset <= segment.end).sort((a, b) => a.segment.end - a.segment.start - (b.segment.end - b.segment.start));
|
|
1530
|
+
return candidates.find((occ) => scanner.rects(occ).some((rect) => contains(rect, x, y))) ?? null;
|
|
1531
|
+
}
|
|
1532
|
+
function attributeOccurrenceAt(x, y, target, hidden) {
|
|
1533
|
+
for (let el = target; el && el.nodeType === Node.ELEMENT_NODE; el = el.parentElement) {
|
|
1534
|
+
for (const slot of scanner.attributeSlotsOf(el)) {
|
|
1535
|
+
const occ = slot.occurrences?.[0];
|
|
1536
|
+
if (!occ || !hidden && !shownOnPage(occ)) continue;
|
|
1537
|
+
if (scanner.rects(occ).some((rect) => contains(rect, x, y))) return occ;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
function canvasOccurrenceAt(x, y) {
|
|
1543
|
+
let best = null;
|
|
1544
|
+
let bestArea = Infinity;
|
|
1545
|
+
for (const entry of canvas.occurrences()) {
|
|
1546
|
+
for (const rect of canvas.rects(entry)) {
|
|
1547
|
+
const area = rect.width * rect.height;
|
|
1548
|
+
if (contains(rect, x, y) && area < bestArea) {
|
|
1549
|
+
best = entry;
|
|
1550
|
+
bestArea = area;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return best;
|
|
1555
|
+
}
|
|
1556
|
+
function textOccurrenceUnder(x, y) {
|
|
1557
|
+
let best = null;
|
|
1558
|
+
let bestArea = Infinity;
|
|
1559
|
+
for (const occ of scanner.occurrences()) {
|
|
1560
|
+
if (occ.slot?.type !== "text") continue;
|
|
1561
|
+
for (const rect of scanner.rects(occ)) {
|
|
1562
|
+
const area = rect.width * rect.height;
|
|
1563
|
+
if (area < bestArea && contains(rect, x, y)) {
|
|
1564
|
+
best = occ;
|
|
1565
|
+
bestArea = area;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return best;
|
|
1570
|
+
}
|
|
1571
|
+
function occurrenceAt(x, y, target, { hidden }) {
|
|
1572
|
+
if (target && isInside(target)) return null;
|
|
1573
|
+
const drawn = target instanceof HTMLCanvasElement ? canvasOccurrenceAt(x, y) : null;
|
|
1574
|
+
return textOccurrenceAt(x, y) ?? drawn ?? textOccurrenceUnder(x, y) ?? attributeOccurrenceAt(x, y, target, hidden);
|
|
1575
|
+
}
|
|
1576
|
+
const pickingAll = () => pickMode || altReveal || last.altKey;
|
|
1577
|
+
function mark(el) {
|
|
1578
|
+
if (marked === el) return;
|
|
1579
|
+
marked?.removeAttribute(OPENS_ATTR);
|
|
1580
|
+
marked = el;
|
|
1581
|
+
el?.setAttribute(OPENS_ATTR, "");
|
|
1582
|
+
}
|
|
1583
|
+
function clickOpens(occ) {
|
|
1584
|
+
mark(null);
|
|
1585
|
+
const el = elementOf(occ);
|
|
1586
|
+
return shownOnPage(occ) && !actsOnClick(el);
|
|
1587
|
+
}
|
|
1588
|
+
function setHover(occ) {
|
|
1589
|
+
if (occ === hover) return;
|
|
1590
|
+
hover = occ;
|
|
1591
|
+
hoverOpens = Boolean(occ) && !pickingAll() && clickOpens(occ);
|
|
1592
|
+
mark(hoverOpens ? elementOf(occ) : null);
|
|
1593
|
+
highlighter.setHover(occ, { tab: Boolean(occ) && !pickingAll() });
|
|
1594
|
+
}
|
|
1595
|
+
function updateHover() {
|
|
1596
|
+
hoverFrame = 0;
|
|
1597
|
+
const { x, y, target } = last;
|
|
1598
|
+
const all = pickingAll();
|
|
1599
|
+
setHover(target && (all || isEditing()) ? occurrenceAt(x, y, target, { hidden: all }) : null);
|
|
1600
|
+
}
|
|
1601
|
+
function scheduleHover() {
|
|
1602
|
+
if (!hoverFrame) hoverFrame = requestAnimationFrame(updateHover);
|
|
1603
|
+
}
|
|
1604
|
+
function clearHover() {
|
|
1605
|
+
if (hoverFrame) cancelAnimationFrame(hoverFrame);
|
|
1606
|
+
hoverFrame = 0;
|
|
1607
|
+
setHover(null);
|
|
1608
|
+
}
|
|
1609
|
+
function refresh() {
|
|
1610
|
+
highlighter.setReveal(pickMode || altReveal);
|
|
1611
|
+
const current = hover;
|
|
1612
|
+
clearHover();
|
|
1613
|
+
if (current || last.target) scheduleHover();
|
|
1614
|
+
}
|
|
1615
|
+
function setAltReveal(on) {
|
|
1616
|
+
if (revealTimer) clearTimeout(revealTimer);
|
|
1617
|
+
revealTimer = 0;
|
|
1618
|
+
if (on === altReveal) return;
|
|
1619
|
+
altReveal = on;
|
|
1620
|
+
refresh();
|
|
1621
|
+
}
|
|
1622
|
+
function setPickMode(on) {
|
|
1623
|
+
if (on === pickMode) return;
|
|
1624
|
+
pickMode = on;
|
|
1625
|
+
document.documentElement.classList.toggle(PICK_CLASS, on);
|
|
1626
|
+
refresh();
|
|
1627
|
+
}
|
|
1628
|
+
function pick2(occ) {
|
|
1629
|
+
onPick({ domain: occ.domain, key: occ.key });
|
|
1630
|
+
setPickMode(false);
|
|
1631
|
+
}
|
|
1632
|
+
function swallow(event) {
|
|
1633
|
+
event.preventDefault();
|
|
1634
|
+
event.stopPropagation();
|
|
1635
|
+
event.stopImmediatePropagation();
|
|
1636
|
+
}
|
|
1637
|
+
const dragged = (event) => press !== null && Math.hypot(event.clientX - press.x, event.clientY - press.y) > DRAG;
|
|
1638
|
+
const listeners = [
|
|
1639
|
+
[
|
|
1640
|
+
"mousemove",
|
|
1641
|
+
(event) => {
|
|
1642
|
+
if (highlighter.isTabEvent(event)) return;
|
|
1643
|
+
const wasAlt = last.altKey;
|
|
1644
|
+
last = { x: event.clientX, y: event.clientY, target: event.target, altKey: event.altKey };
|
|
1645
|
+
if (event.altKey && !event.ctrlKey && !event.metaKey) setAltReveal(true);
|
|
1646
|
+
else if (wasAlt || altReveal || revealTimer) setAltReveal(false);
|
|
1647
|
+
scheduleHover();
|
|
1648
|
+
}
|
|
1649
|
+
],
|
|
1650
|
+
[
|
|
1651
|
+
"keydown",
|
|
1652
|
+
(event) => {
|
|
1653
|
+
if (event.key === "Alt") {
|
|
1654
|
+
if (!event.ctrlKey && !event.metaKey && !event.shiftKey && !altReveal && !revealTimer) {
|
|
1655
|
+
revealTimer = setTimeout(() => setAltReveal(true), REVEAL_DELAY);
|
|
1656
|
+
}
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (altReveal || revealTimer) setAltReveal(false);
|
|
1660
|
+
if (event.key === "Escape" && pickMode) {
|
|
1661
|
+
setPickMode(false);
|
|
1662
|
+
event.stopPropagation();
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
],
|
|
1666
|
+
[
|
|
1667
|
+
"keyup",
|
|
1668
|
+
(event) => {
|
|
1669
|
+
if (event.key !== "Alt") return;
|
|
1670
|
+
last = { ...last, altKey: false };
|
|
1671
|
+
const revealed = altReveal;
|
|
1672
|
+
setAltReveal(false);
|
|
1673
|
+
if (!revealed) refresh();
|
|
1674
|
+
}
|
|
1675
|
+
],
|
|
1676
|
+
[
|
|
1677
|
+
"blur",
|
|
1678
|
+
() => {
|
|
1679
|
+
last = { ...last, altKey: false };
|
|
1680
|
+
setAltReveal(false);
|
|
1681
|
+
if (!pickMode) clearHover();
|
|
1682
|
+
}
|
|
1683
|
+
],
|
|
1684
|
+
[
|
|
1685
|
+
"pointerdown",
|
|
1686
|
+
(event) => {
|
|
1687
|
+
press = { x: event.clientX, y: event.clientY };
|
|
1688
|
+
}
|
|
1689
|
+
],
|
|
1690
|
+
[
|
|
1691
|
+
"click",
|
|
1692
|
+
(event) => {
|
|
1693
|
+
if (event.target && isInside(event.target)) return;
|
|
1694
|
+
if (pickMode || event.altKey) {
|
|
1695
|
+
swallow(event);
|
|
1696
|
+
const occ2 = occurrenceAt(event.clientX, event.clientY, event.target, { hidden: true });
|
|
1697
|
+
if (occ2) pick2(occ2);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if (!isEditing() || !plainClick(event) || dragged(event)) return;
|
|
1701
|
+
const occ = occurrenceAt(event.clientX, event.clientY, event.target, { hidden: false });
|
|
1702
|
+
if (!occ || !(occ === hover ? hoverOpens : clickOpens(occ))) return;
|
|
1703
|
+
swallow(event);
|
|
1704
|
+
pick2(occ);
|
|
1705
|
+
}
|
|
1706
|
+
]
|
|
1707
|
+
];
|
|
1708
|
+
const pressEvents = ["pointerdown", "mousedown", "pointerup", "mouseup"].map((name) => [
|
|
1709
|
+
name,
|
|
1710
|
+
(event) => {
|
|
1711
|
+
if (!pickMode || event.target && isInside(event.target)) return;
|
|
1712
|
+
event.stopPropagation();
|
|
1713
|
+
if (name === "mousedown") event.preventDefault();
|
|
1714
|
+
}
|
|
1715
|
+
]);
|
|
1716
|
+
const bound = [...listeners, ...pressEvents].map(([name, handler]) => [
|
|
1717
|
+
name,
|
|
1718
|
+
(event) => {
|
|
1719
|
+
if (!paused) handler(event);
|
|
1720
|
+
}
|
|
1721
|
+
]);
|
|
1722
|
+
for (const [name, handler] of bound) {
|
|
1723
|
+
window.addEventListener(name, handler, { capture: true, passive: name === "mousemove" });
|
|
1724
|
+
}
|
|
1725
|
+
return {
|
|
1726
|
+
setPickMode,
|
|
1727
|
+
isPickMode: () => pickMode,
|
|
1728
|
+
setPaused(on) {
|
|
1729
|
+
if (on === paused) return;
|
|
1730
|
+
paused = on;
|
|
1731
|
+
if (!on) return;
|
|
1732
|
+
setPickMode(false);
|
|
1733
|
+
last = { ...last, altKey: false };
|
|
1734
|
+
setAltReveal(false);
|
|
1735
|
+
clearHover();
|
|
1736
|
+
mark(null);
|
|
1737
|
+
},
|
|
1738
|
+
/** The editor opened or closed. */
|
|
1739
|
+
refresh,
|
|
1740
|
+
/** Opens the string under the pointer (its Edit tab was clicked). */
|
|
1741
|
+
pickHover() {
|
|
1742
|
+
if (hover) pick2(hover);
|
|
1743
|
+
},
|
|
1744
|
+
destroy() {
|
|
1745
|
+
setPickMode(false);
|
|
1746
|
+
setAltReveal(false);
|
|
1747
|
+
clearHover();
|
|
1748
|
+
mark(null);
|
|
1749
|
+
style.remove();
|
|
1750
|
+
for (const [name, handler] of bound) {
|
|
1751
|
+
window.removeEventListener(name, handler, { capture: true });
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// js/in_context/args.js
|
|
1758
|
+
function pageFormatter() {
|
|
1759
|
+
const format = typeof window === "undefined" ? void 0 : window.__dialectoFormat;
|
|
1760
|
+
return typeof format === "function" ? format : null;
|
|
1761
|
+
}
|
|
1762
|
+
function reviveArgs(args, raw) {
|
|
1763
|
+
const children = parseSegments(raw);
|
|
1764
|
+
const values = {};
|
|
1765
|
+
for (const [name, value] of Object.entries(args)) {
|
|
1766
|
+
if (value !== null && typeof value === "object" && "$date" in value) {
|
|
1767
|
+
values[name] = new Date(value.$date);
|
|
1768
|
+
} else if (value !== null && typeof value === "object" && "$marked" in value) {
|
|
1769
|
+
const child = children.find((segment) => stripMarks(raw.slice(segment.start, segment.end)) === value.$marked);
|
|
1770
|
+
values[name] = child ? raw.slice(child.start, child.end) : value.$marked;
|
|
1771
|
+
} else {
|
|
1772
|
+
values[name] = value;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
return values;
|
|
1776
|
+
}
|
|
1777
|
+
function renderWithArgs(to, segment, raw, format) {
|
|
1778
|
+
if (!segment.args || typeof format !== "function") return null;
|
|
1779
|
+
try {
|
|
1780
|
+
const text3 = String(format(to, segment.locale, reviveArgs(segment.args, raw), raw));
|
|
1781
|
+
return { ok: true, text: text3, noop: text3 === raw };
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
return { ok: false, reason: error?.dialectoReason === "unsupported" ? "unsupported" : "invalid" };
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// js/in_context/plural_pick.js
|
|
1788
|
+
function pickPluralTemplate(forms, msgid, msgidPlural, n, form, rule = "elixir") {
|
|
1789
|
+
const fallback = rule === "django" ? forms[0] === "" : forms.some((each) => each === "");
|
|
1790
|
+
if (fallback) return { ok: true, template: n === 1 ? msgid : msgidPlural };
|
|
1791
|
+
if (form < forms.length) return { ok: true, template: forms[form] };
|
|
1792
|
+
return { ok: false, reason: "invalid" };
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// js/in_context/icu_simple.js
|
|
1796
|
+
var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1797
|
+
var TAG = /<\/?[A-Za-z]/;
|
|
1798
|
+
function tokenize(src) {
|
|
1799
|
+
if (typeof src !== "string") return null;
|
|
1800
|
+
if (src.includes("#") || src.includes("''") || src.includes("'{") || src.includes("'}") || TAG.test(src)) return null;
|
|
1801
|
+
const tokens = [];
|
|
1802
|
+
let literal = "";
|
|
1803
|
+
for (let i = 0; i < src.length; i++) {
|
|
1804
|
+
const char = src[i];
|
|
1805
|
+
if (char === "{") {
|
|
1806
|
+
const close = src.indexOf("}", i + 1);
|
|
1807
|
+
if (close < 0) return null;
|
|
1808
|
+
const inner = src.slice(i + 1, close);
|
|
1809
|
+
if (inner.includes("{") || inner.includes(",")) return null;
|
|
1810
|
+
const name = inner.trim();
|
|
1811
|
+
if (!IDENT.test(name)) return null;
|
|
1812
|
+
if (literal) tokens.push({ lit: literal });
|
|
1813
|
+
literal = "";
|
|
1814
|
+
tokens.push({ arg: name });
|
|
1815
|
+
i = close;
|
|
1816
|
+
} else if (char === "}") {
|
|
1817
|
+
return null;
|
|
1818
|
+
} else {
|
|
1819
|
+
literal += char;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
if (literal) tokens.push({ lit: literal });
|
|
1823
|
+
return tokens;
|
|
1824
|
+
}
|
|
1825
|
+
var isSimple = (src) => tokenize(src) !== null;
|
|
1826
|
+
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1827
|
+
function matcher(tokens) {
|
|
1828
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1829
|
+
let source = "^";
|
|
1830
|
+
for (const token of tokens) {
|
|
1831
|
+
if ("lit" in token) {
|
|
1832
|
+
source += escapeRegExp(token.lit);
|
|
1833
|
+
} else if (groups.has(token.arg)) {
|
|
1834
|
+
source += `\\${groups.get(token.arg)}`;
|
|
1835
|
+
} else {
|
|
1836
|
+
groups.set(token.arg, groups.size + 1);
|
|
1837
|
+
source += "([\\s\\S]*?)";
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return { regex: new RegExp(`${source}$`), groups };
|
|
1841
|
+
}
|
|
1842
|
+
var PLACEHOLDER_BASE = 57344;
|
|
1843
|
+
var PLACEHOLDER_LIMIT = 6400;
|
|
1844
|
+
function atomize(rawText) {
|
|
1845
|
+
const segments = parseSegments(rawText);
|
|
1846
|
+
if (segments.length === 0 || segments.length > PLACEHOLDER_LIMIT) return null;
|
|
1847
|
+
const first = String.fromCharCode(PLACEHOLDER_BASE);
|
|
1848
|
+
const last = String.fromCharCode(PLACEHOLDER_BASE + segments.length - 1);
|
|
1849
|
+
if ([...rawText].some((char) => char >= first && char <= last)) return null;
|
|
1850
|
+
let text3 = "";
|
|
1851
|
+
let cursor = 0;
|
|
1852
|
+
const parts = [];
|
|
1853
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
1854
|
+
for (const segment of segments) {
|
|
1855
|
+
const part = rawText.slice(segment.start, segment.end);
|
|
1856
|
+
if (!indexOf.has(part)) {
|
|
1857
|
+
indexOf.set(part, parts.length);
|
|
1858
|
+
parts.push(part);
|
|
1859
|
+
}
|
|
1860
|
+
text3 += rawText.slice(cursor, segment.start) + String.fromCharCode(PLACEHOLDER_BASE + indexOf.get(part));
|
|
1861
|
+
cursor = segment.end;
|
|
1862
|
+
}
|
|
1863
|
+
text3 += rawText.slice(cursor);
|
|
1864
|
+
const restore = (value) => [...value].map((char) => {
|
|
1865
|
+
const index = char.charCodeAt(0) - PLACEHOLDER_BASE;
|
|
1866
|
+
return index >= 0 && index < parts.length ? parts[index] : char;
|
|
1867
|
+
}).join("");
|
|
1868
|
+
return { text: text3, restore };
|
|
1869
|
+
}
|
|
1870
|
+
function captureArgs(from, rawText) {
|
|
1871
|
+
const tokens = tokenize(from);
|
|
1872
|
+
if (!tokens) return { ok: false, reason: "unsupported" };
|
|
1873
|
+
const { regex, groups } = matcher(tokens);
|
|
1874
|
+
const atoms = atomize(rawText);
|
|
1875
|
+
const match = regex.exec(atoms ? atoms.text : rawText);
|
|
1876
|
+
if (!match) return { ok: false, reason: "mismatch" };
|
|
1877
|
+
const args = {};
|
|
1878
|
+
for (const [name, index] of groups) args[name] = atoms ? atoms.restore(match[index]) : match[index];
|
|
1879
|
+
return { ok: true, args };
|
|
1880
|
+
}
|
|
1881
|
+
function render(to, args) {
|
|
1882
|
+
const tokens = tokenize(to);
|
|
1883
|
+
if (!tokens) return { ok: false, reason: "unsupported" };
|
|
1884
|
+
let text3 = "";
|
|
1885
|
+
for (const token of tokens) {
|
|
1886
|
+
if ("lit" in token) {
|
|
1887
|
+
text3 += token.lit;
|
|
1888
|
+
} else if (Object.hasOwn(args, token.arg)) {
|
|
1889
|
+
text3 += args[token.arg];
|
|
1890
|
+
} else {
|
|
1891
|
+
return { ok: false, reason: "unknown_argument" };
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return { ok: true, text: text3 };
|
|
1895
|
+
}
|
|
1896
|
+
function previewText(from, to, rawText) {
|
|
1897
|
+
if (!isSimple(from) || !isSimple(to)) return { ok: false, reason: "unsupported" };
|
|
1898
|
+
const captured = captureArgs(from, rawText);
|
|
1899
|
+
if (!captured.ok) {
|
|
1900
|
+
return captureArgs(to, rawText).ok ? { ok: true, text: rawText, noop: true } : captured;
|
|
1901
|
+
}
|
|
1902
|
+
const rendered = render(to, captured.args);
|
|
1903
|
+
if (!rendered.ok) return rendered;
|
|
1904
|
+
return { ok: true, text: rendered.text, noop: rendered.text === rawText };
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// js/in_context/python_format.js
|
|
1908
|
+
var NAMED = /%\(([A-Za-z_]\w*)\)([-#0 +]*\d*(?:\.\d*)?[hlL]?)([diouxXeEfFgGcrsa])/y;
|
|
1909
|
+
var NAMED_LOOSE = /%\([^)]*\)[-#0 +]*(?:\d+|\*)?(?:\.(?:\d+|\*)?)?[hlL]?[diouxXeEfFgGcrsa]/y;
|
|
1910
|
+
var NAMED_ANYWHERE = /%\([A-Za-z_]\w*\)[-#0 +]*\d*(?:\.\d+)?[hlL]?[diouxXeEfFgGcrsa]/;
|
|
1911
|
+
var POSITIONAL = /%[-#0 +]*(?:\d+|\*)?(?:\.(?:\d+|\*)?)?[hlL]?[diouxXeEfFgGcrsa]/y;
|
|
1912
|
+
var PLAIN_SPEC = "";
|
|
1913
|
+
var INTEGER_CONVERSIONS = "di";
|
|
1914
|
+
var PLAIN_CONVERSIONS = "sdi";
|
|
1915
|
+
var STEP_LIMIT = 5e4;
|
|
1916
|
+
function tokenize2(src) {
|
|
1917
|
+
if (/[{}]/.test(src)) return { reason: "unsupported" };
|
|
1918
|
+
const tokens = [];
|
|
1919
|
+
let literal = "";
|
|
1920
|
+
for (let i = 0; i < src.length; i++) {
|
|
1921
|
+
if (src[i] !== "%") {
|
|
1922
|
+
literal += src[i];
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
if (src[i + 1] === "%") {
|
|
1926
|
+
literal += "%";
|
|
1927
|
+
i += 1;
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
NAMED.lastIndex = i;
|
|
1931
|
+
const named = NAMED.exec(src);
|
|
1932
|
+
if (named) {
|
|
1933
|
+
if (literal) tokens.push({ lit: literal });
|
|
1934
|
+
literal = "";
|
|
1935
|
+
tokens.push({ arg: named[1], spec: named[2], conv: named[3] });
|
|
1936
|
+
i += named[0].length - 1;
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
NAMED_LOOSE.lastIndex = i;
|
|
1940
|
+
const loose = NAMED_LOOSE.test(src);
|
|
1941
|
+
POSITIONAL.lastIndex = i;
|
|
1942
|
+
return { reason: loose || POSITIONAL.test(src) ? "unsupported" : "invalid" };
|
|
1943
|
+
}
|
|
1944
|
+
if (literal) tokens.push({ lit: literal });
|
|
1945
|
+
return { tokens };
|
|
1946
|
+
}
|
|
1947
|
+
function usesPythonFormat(edit) {
|
|
1948
|
+
const { msgid } = splitKey(edit.key);
|
|
1949
|
+
const sources = [msgid, edit.msgid_plural, ...edit.from_forms ?? [edit.from]];
|
|
1950
|
+
const texts = sources.filter((each) => typeof each === "string");
|
|
1951
|
+
return texts.some((each) => NAMED_ANYWHERE.test(each)) && !texts.some((each) => each.includes("%{"));
|
|
1952
|
+
}
|
|
1953
|
+
function captures(tokens, text3) {
|
|
1954
|
+
const found = /* @__PURE__ */ new Map();
|
|
1955
|
+
let steps = 0;
|
|
1956
|
+
const walk = (index, pos, bound) => {
|
|
1957
|
+
if (found.size > 1 || steps++ > STEP_LIMIT) return;
|
|
1958
|
+
if (index === tokens.length) {
|
|
1959
|
+
if (pos === text3.length) found.set(JSON.stringify(bound), bound);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
const token = tokens[index];
|
|
1963
|
+
if ("lit" in token) {
|
|
1964
|
+
if (text3.startsWith(token.lit, pos)) walk(index + 1, pos + token.lit.length, bound);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (Object.hasOwn(bound, token.arg)) {
|
|
1968
|
+
const value = bound[token.arg].text;
|
|
1969
|
+
if (text3.startsWith(value, pos)) walk(index + 1, pos + value.length, bound);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
const next = tokens[index + 1];
|
|
1973
|
+
const literalNext = next && "lit" in next ? next.lit : null;
|
|
1974
|
+
for (let end = pos; end <= text3.length && found.size < 2; end++) {
|
|
1975
|
+
if (literalNext !== null && !text3.startsWith(literalNext, end)) continue;
|
|
1976
|
+
walk(index + 1, end, { ...bound, [token.arg]: { text: text3.slice(pos, end), spec: token.spec, conv: token.conv } });
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
walk(0, 0, {});
|
|
1980
|
+
if (found.size > 1 || steps > STEP_LIMIT) return { ambiguous: true };
|
|
1981
|
+
return { values: [...found.values()] };
|
|
1982
|
+
}
|
|
1983
|
+
var sameFormat = (a, b) => a.spec === b.spec && a.conv === b.conv;
|
|
1984
|
+
var plainCount = (token) => token.spec === PLAIN_SPEC && PLAIN_CONVERSIONS.includes(token.conv);
|
|
1985
|
+
function renderPythonFormat(edit, segment, raw) {
|
|
1986
|
+
const { msgid } = splitKey(edit.key);
|
|
1987
|
+
const plural = typeof edit.msgid_plural === "string";
|
|
1988
|
+
const baseForms = edit.from_forms ?? [edit.from];
|
|
1989
|
+
let draftTemplate;
|
|
1990
|
+
let baseTemplate;
|
|
1991
|
+
let count = null;
|
|
1992
|
+
if (plural) {
|
|
1993
|
+
const { __n: n, __form: form } = segment.args ?? {};
|
|
1994
|
+
if (typeof n !== "number" || !Number.isInteger(form) || form < 0) return { ok: false, reason: "unsupported" };
|
|
1995
|
+
const draft2 = pickPluralTemplate(edit.forms, msgid, edit.msgid_plural, n, form, "django");
|
|
1996
|
+
if (!draft2.ok) return draft2;
|
|
1997
|
+
const base2 = pickPluralTemplate(baseForms, msgid, edit.msgid_plural, n, form, "django");
|
|
1998
|
+
if (!base2.ok || typeof base2.template !== "string") return { ok: false, reason: "unsupported" };
|
|
1999
|
+
draftTemplate = draft2.template;
|
|
2000
|
+
baseTemplate = base2.template;
|
|
2001
|
+
count = n;
|
|
2002
|
+
} else {
|
|
2003
|
+
draftTemplate = edit.forms[0] === "" || edit.forms[0] === void 0 ? msgid : edit.forms[0];
|
|
2004
|
+
baseTemplate = baseForms[0] === "" || baseForms[0] === void 0 ? msgid : baseForms[0];
|
|
2005
|
+
}
|
|
2006
|
+
const base = tokenize2(baseTemplate);
|
|
2007
|
+
if (!base.tokens) return { ok: false, reason: "unsupported" };
|
|
2008
|
+
const draft = tokenize2(draftTemplate);
|
|
2009
|
+
if (!draft.tokens) return draft.reason === "invalid" ? { ok: false, reason: "invalid" } : { ok: false, reason: "unsupported" };
|
|
2010
|
+
const atoms = atomize(raw);
|
|
2011
|
+
const matched = captures(base.tokens, atoms ? atoms.text : raw);
|
|
2012
|
+
if (matched.ambiguous) return { ok: false, reason: "unsupported" };
|
|
2013
|
+
if (matched.values.length === 0) return { ok: false, reason: "mismatch" };
|
|
2014
|
+
const values = matched.values[0];
|
|
2015
|
+
if (atoms) for (const value of Object.values(values)) value.text = atoms.restore(value.text);
|
|
2016
|
+
let countName = null;
|
|
2017
|
+
const pluralNames = /* @__PURE__ */ new Map();
|
|
2018
|
+
if (plural) {
|
|
2019
|
+
const named = tokenize2(edit.msgid_plural);
|
|
2020
|
+
if (!named.tokens) return { ok: false, reason: "unsupported" };
|
|
2021
|
+
for (const token of named.tokens) if ("arg" in token) pluralNames.set(token.arg, token.conv);
|
|
2022
|
+
const integers = [...pluralNames].filter(([, conv]) => INTEGER_CONVERSIONS.includes(conv));
|
|
2023
|
+
if (pluralNames.size === 1) countName = [...pluralNames.keys()][0];
|
|
2024
|
+
else if (integers.length === 1) countName = integers[0][0];
|
|
2025
|
+
}
|
|
2026
|
+
let text3 = "";
|
|
2027
|
+
const used = /* @__PURE__ */ new Set();
|
|
2028
|
+
for (const token of draft.tokens) {
|
|
2029
|
+
if ("lit" in token) {
|
|
2030
|
+
text3 += token.lit;
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
used.add(token.arg);
|
|
2034
|
+
if (Object.hasOwn(values, token.arg)) {
|
|
2035
|
+
const shown = values[token.arg];
|
|
2036
|
+
const exempt = token.arg === countName && plainCount(token) && plainCount(shown);
|
|
2037
|
+
if (!exempt && !sameFormat(token, shown)) return { ok: false, reason: "unsupported" };
|
|
2038
|
+
text3 += shown.text;
|
|
2039
|
+
} else if (token.arg === countName) {
|
|
2040
|
+
if (!plainCount(token)) return { ok: false, reason: "unsupported" };
|
|
2041
|
+
text3 += String(count);
|
|
2042
|
+
} else {
|
|
2043
|
+
return { ok: false, reason: pluralNames.has(token.arg) ? "unsupported" : "unknown_argument" };
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
for (const name of Object.keys(values)) {
|
|
2047
|
+
if (!used.has(name) && name !== countName) return { ok: false, reason: "invalid" };
|
|
2048
|
+
}
|
|
2049
|
+
return { ok: true, text: text3 };
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// js/in_context/gettext.js
|
|
2053
|
+
function bindingsOf(args, plural) {
|
|
2054
|
+
const bindings = {};
|
|
2055
|
+
for (const [name, value] of Object.entries(args)) {
|
|
2056
|
+
if (!name.startsWith("__")) bindings[name] = value;
|
|
2057
|
+
}
|
|
2058
|
+
if (plural && typeof args.__n === "number") bindings.count = args.__n;
|
|
2059
|
+
return bindings;
|
|
2060
|
+
}
|
|
2061
|
+
var text = (value) => value === null || value === void 0 ? "" : String(value);
|
|
2062
|
+
function interpolate(template, bindings) {
|
|
2063
|
+
let out = "";
|
|
2064
|
+
let rest = template;
|
|
2065
|
+
const missing = [];
|
|
2066
|
+
for (; ; ) {
|
|
2067
|
+
const start = rest.indexOf("%{");
|
|
2068
|
+
if (start < 0) return { text: out + rest, missing };
|
|
2069
|
+
const before = rest.slice(0, start);
|
|
2070
|
+
const after = rest.slice(start + 2);
|
|
2071
|
+
if (after.startsWith("}")) {
|
|
2072
|
+
out += `${before}%{}`;
|
|
2073
|
+
rest = after.slice(1);
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
const end = after.indexOf("}");
|
|
2077
|
+
if (end < 0) return { text: out + rest, missing };
|
|
2078
|
+
const name = after.slice(0, end);
|
|
2079
|
+
if (Object.hasOwn(bindings, name)) {
|
|
2080
|
+
out += before + text(bindings[name]);
|
|
2081
|
+
} else {
|
|
2082
|
+
missing.push(name);
|
|
2083
|
+
out += `${before}%{${name}}`;
|
|
2084
|
+
}
|
|
2085
|
+
rest = after.slice(end + 1);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
var isGettextEdit = (edit) => Array.isArray(edit?.forms);
|
|
2089
|
+
function renderGettext(edit, segment, raw = "") {
|
|
2090
|
+
if (usesPythonFormat(edit)) return renderPythonFormat(edit, segment, raw);
|
|
2091
|
+
if (!segment.args) return { ok: false, reason: "unsupported" };
|
|
2092
|
+
const args = reviveArgs(segment.args, raw);
|
|
2093
|
+
const plural = typeof edit.msgid_plural === "string";
|
|
2094
|
+
const { msgid } = splitKey(edit.key);
|
|
2095
|
+
let template;
|
|
2096
|
+
if (plural) {
|
|
2097
|
+
const { __n: n, __form: form } = args;
|
|
2098
|
+
if (typeof n !== "number" || !Number.isInteger(form) || form < 0) return { ok: false, reason: "unsupported" };
|
|
2099
|
+
const picked = pickPluralTemplate(edit.forms, msgid, edit.msgid_plural, n, form);
|
|
2100
|
+
if (!picked.ok) return picked;
|
|
2101
|
+
template = picked.template;
|
|
2102
|
+
} else {
|
|
2103
|
+
template = edit.forms[0] === "" || edit.forms[0] === void 0 ? msgid : edit.forms[0];
|
|
2104
|
+
}
|
|
2105
|
+
const { text: rendered, missing } = interpolate(template, bindingsOf(args, plural));
|
|
2106
|
+
return missing.length ? { ok: false, reason: "invalid" } : { ok: true, text: rendered };
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// js/in_context/transform.js
|
|
2110
|
+
var MAX_DEPTH = 8;
|
|
2111
|
+
var MAX_TEXT = 280;
|
|
2112
|
+
function transformValue(base, getEdit, format = null) {
|
|
2113
|
+
const results = [];
|
|
2114
|
+
const value = transform(base, getEdit, format, results, 0);
|
|
2115
|
+
return { value, results };
|
|
2116
|
+
}
|
|
2117
|
+
function isHtmlSegment(segment, raw) {
|
|
2118
|
+
return Boolean(segment.args) && reviveArgs(segment.args, raw).__html === true;
|
|
2119
|
+
}
|
|
2120
|
+
function transform(base, getEdit, format, results, depth) {
|
|
2121
|
+
const segments = parseSegments(base);
|
|
2122
|
+
return segments.length ? rebuild(base, 0, base.length, segments, getEdit, format, results, depth) : base;
|
|
2123
|
+
}
|
|
2124
|
+
function rebuild(base, from, to, segments, getEdit, format, results, depth) {
|
|
2125
|
+
let out = "";
|
|
2126
|
+
let pos = from;
|
|
2127
|
+
for (const segment of segments) {
|
|
2128
|
+
out += base.slice(pos, segment.start);
|
|
2129
|
+
const head = base.slice(segment.start, segment.innerStart);
|
|
2130
|
+
const tail = segment.open ? "" : CLOSE;
|
|
2131
|
+
let inner = null;
|
|
2132
|
+
const edit = getEdit(segment);
|
|
2133
|
+
if (edit) {
|
|
2134
|
+
const raw = base.slice(segment.innerStart, segment.innerEnd);
|
|
2135
|
+
const result = isHtmlSegment(segment, raw) ? { ok: false, reason: "mismatch" } : isGettextEdit(edit) ? renderGettext(edit, segment, raw) : renderWithArgs(edit.to, segment, raw, format) ?? previewText(edit.from, edit.to, raw);
|
|
2136
|
+
results.push({
|
|
2137
|
+
domain: segment.domain,
|
|
2138
|
+
key: segment.key,
|
|
2139
|
+
locale: segment.locale,
|
|
2140
|
+
ok: result.ok,
|
|
2141
|
+
reason: result.reason,
|
|
2142
|
+
text: result.ok ? stripMarks(result.text).slice(0, MAX_TEXT) : void 0
|
|
2143
|
+
});
|
|
2144
|
+
if (result.ok) {
|
|
2145
|
+
inner = depth < MAX_DEPTH ? transform(result.text, getEdit, format, results, depth + 1) : result.text;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
if (inner === null) {
|
|
2149
|
+
inner = rebuild(base, segment.innerStart, segment.innerEnd, segment.children, getEdit, format, results, depth);
|
|
2150
|
+
}
|
|
2151
|
+
out += head + inner + tail;
|
|
2152
|
+
pos = segment.end;
|
|
2153
|
+
}
|
|
2154
|
+
return out + base.slice(pos, to);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// js/in_context/preview.js
|
|
2158
|
+
function createPreview({
|
|
2159
|
+
scanner,
|
|
2160
|
+
onStatus = () => {
|
|
2161
|
+
},
|
|
2162
|
+
onEdits = () => {
|
|
2163
|
+
},
|
|
2164
|
+
onApplied = () => {
|
|
2165
|
+
},
|
|
2166
|
+
format = pageFormatter
|
|
2167
|
+
}) {
|
|
2168
|
+
let edits = /* @__PURE__ */ new Map();
|
|
2169
|
+
const reported = /* @__PURE__ */ new Map();
|
|
2170
|
+
const canvasTally = /* @__PURE__ */ new Map();
|
|
2171
|
+
const lookup = (segment) => edits.get(keyId(segment.domain, segment.key, segment.locale));
|
|
2172
|
+
const run = (value) => transformValue(value, lookup, format());
|
|
2173
|
+
function applySlot(slot) {
|
|
2174
|
+
const base = baseValue(slot);
|
|
2175
|
+
const { value } = run(base);
|
|
2176
|
+
setSlotValue(slot, base, value);
|
|
2177
|
+
scanner.refresh(slot);
|
|
2178
|
+
}
|
|
2179
|
+
function slotsFor(ids) {
|
|
2180
|
+
const out = [];
|
|
2181
|
+
for (const slot of scanner.slots()) {
|
|
2182
|
+
if (slot.occurrences.some((occ) => ids.has(keyId(occ.domain, occ.key, occ.locale)))) out.push(slot);
|
|
2183
|
+
}
|
|
2184
|
+
return out;
|
|
2185
|
+
}
|
|
2186
|
+
function statusOf(id) {
|
|
2187
|
+
let ok = 0;
|
|
2188
|
+
let fail = 0;
|
|
2189
|
+
let text3;
|
|
2190
|
+
let reason;
|
|
2191
|
+
const note = (result) => {
|
|
2192
|
+
if (result.ok) {
|
|
2193
|
+
ok += 1;
|
|
2194
|
+
text3 ?? (text3 = result.text);
|
|
2195
|
+
} else {
|
|
2196
|
+
fail += 1;
|
|
2197
|
+
reason ?? (reason = result.reason);
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
for (const slot of scanner.slots()) {
|
|
2201
|
+
for (const result of run(baseValue(slot)).results) {
|
|
2202
|
+
if (keyId(result.domain, result.key, result.locale) === id) note(result);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
const canvas = canvasTally.get(id);
|
|
2206
|
+
if (canvas) {
|
|
2207
|
+
ok += canvas.ok;
|
|
2208
|
+
fail += canvas.fail;
|
|
2209
|
+
text3 ?? (text3 = canvas.text);
|
|
2210
|
+
reason ?? (reason = canvas.reason);
|
|
2211
|
+
}
|
|
2212
|
+
const status = { applied: ok > 0 && fail === 0, unsupported: fail > 0 };
|
|
2213
|
+
if (text3 !== void 0) status.text = text3;
|
|
2214
|
+
if (fail > 0 && reason) status.reason = reason;
|
|
2215
|
+
return status;
|
|
2216
|
+
}
|
|
2217
|
+
function report(id, force) {
|
|
2218
|
+
const edit = edits.get(id);
|
|
2219
|
+
if (!edit) return;
|
|
2220
|
+
const status = statusOf(id);
|
|
2221
|
+
const signature = `${status.applied}|${status.unsupported}|${status.reason ?? ""}|${status.text ?? ""}`;
|
|
2222
|
+
if (!force && reported.get(id) === signature) return;
|
|
2223
|
+
reported.set(id, signature);
|
|
2224
|
+
onStatus({ domain: edit.domain, key: edit.key, locale: edit.locale, ...status });
|
|
2225
|
+
}
|
|
2226
|
+
function commit(ids, { force }) {
|
|
2227
|
+
for (const slot of slotsFor(ids)) applySlot(slot);
|
|
2228
|
+
for (const id of ids) report(id, force);
|
|
2229
|
+
onEdits([...edits.values()]);
|
|
2230
|
+
onApplied();
|
|
2231
|
+
}
|
|
2232
|
+
const idOf = (edit) => keyId(edit.domain, edit.key, edit.locale);
|
|
2233
|
+
return {
|
|
2234
|
+
/** Set or replace one live preview. */
|
|
2235
|
+
set(edit) {
|
|
2236
|
+
const id = idOf(edit);
|
|
2237
|
+
edits.set(id, edit);
|
|
2238
|
+
canvasTally.delete(id);
|
|
2239
|
+
commit(/* @__PURE__ */ new Set([id]), { force: true });
|
|
2240
|
+
},
|
|
2241
|
+
/** Restore the original text of one message. */
|
|
2242
|
+
clear(identity2) {
|
|
2243
|
+
const id = idOf(identity2);
|
|
2244
|
+
if (!edits.delete(id)) return;
|
|
2245
|
+
reported.delete(id);
|
|
2246
|
+
canvasTally.delete(id);
|
|
2247
|
+
commit(/* @__PURE__ */ new Set([id]), { force: false });
|
|
2248
|
+
},
|
|
2249
|
+
/** Replace the whole preview set. */
|
|
2250
|
+
reset(list) {
|
|
2251
|
+
const previous = new Set(edits.keys());
|
|
2252
|
+
edits = new Map(list.map((edit) => [idOf(edit), edit]));
|
|
2253
|
+
const affected = /* @__PURE__ */ new Set([...previous, ...edits.keys()]);
|
|
2254
|
+
for (const id of previous) {
|
|
2255
|
+
reported.delete(id);
|
|
2256
|
+
canvasTally.delete(id);
|
|
2257
|
+
}
|
|
2258
|
+
commit(affected, { force: true });
|
|
2259
|
+
},
|
|
2260
|
+
/** Slots the scanner just found or the host re-rendered: re-apply active edits. */
|
|
2261
|
+
applyToSlots(slots) {
|
|
2262
|
+
if (!edits.size) return;
|
|
2263
|
+
const touched = /* @__PURE__ */ new Set();
|
|
2264
|
+
for (const slot of slots) {
|
|
2265
|
+
if (!slot.isConnected()) continue;
|
|
2266
|
+
applySlot(slot);
|
|
2267
|
+
for (const occ of slot.occurrences ?? []) touched.add(keyId(occ.domain, occ.key, occ.locale));
|
|
2268
|
+
}
|
|
2269
|
+
for (const id of touched) if (edits.has(id)) report(id, false);
|
|
2270
|
+
onApplied();
|
|
2271
|
+
},
|
|
2272
|
+
/** The page's formatter just became available: re-render every preview with it. */
|
|
2273
|
+
reapply() {
|
|
2274
|
+
if (edits.size) commit(new Set(edits.keys()), { force: false });
|
|
2275
|
+
},
|
|
2276
|
+
/** Canvas draw hook: a marked string about to be drawn. */
|
|
2277
|
+
transformString(text3) {
|
|
2278
|
+
return edits.size ? run(text3) : { value: text3, results: [] };
|
|
2279
|
+
},
|
|
2280
|
+
noteCanvasResults(results) {
|
|
2281
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
2282
|
+
for (const result of results) {
|
|
2283
|
+
const id = keyId(result.domain, result.key, result.locale);
|
|
2284
|
+
const tally = fresh.get(id) ?? { ok: 0, fail: 0 };
|
|
2285
|
+
if (result.ok) {
|
|
2286
|
+
tally.ok += 1;
|
|
2287
|
+
tally.text ?? (tally.text = result.text);
|
|
2288
|
+
} else {
|
|
2289
|
+
tally.fail += 1;
|
|
2290
|
+
tally.reason ?? (tally.reason = result.reason);
|
|
2291
|
+
}
|
|
2292
|
+
fresh.set(id, tally);
|
|
2293
|
+
}
|
|
2294
|
+
for (const [id, tally] of fresh) {
|
|
2295
|
+
canvasTally.set(id, tally);
|
|
2296
|
+
if (edits.has(id)) report(id, false);
|
|
2297
|
+
}
|
|
2298
|
+
},
|
|
2299
|
+
edits: () => [...edits.values()],
|
|
2300
|
+
size: () => edits.size
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// js/in_context/save.js
|
|
2305
|
+
async function saveFiles(endpoint, edits, { fetchImpl = globalThis.fetch?.bind(globalThis) } = {}) {
|
|
2306
|
+
if (!endpoint || !fetchImpl) return { error: "unavailable" };
|
|
2307
|
+
try {
|
|
2308
|
+
const response = await fetchImpl(endpoint, {
|
|
2309
|
+
method: "POST",
|
|
2310
|
+
headers: { "content-type": "application/json" },
|
|
2311
|
+
body: JSON.stringify({ edits }),
|
|
2312
|
+
credentials: "same-origin"
|
|
2313
|
+
});
|
|
2314
|
+
if (!response.ok) return { error: `status_${response.status}` };
|
|
2315
|
+
return await response.json();
|
|
2316
|
+
} catch {
|
|
2317
|
+
return { error: "unreachable" };
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// js/in_context/site.js
|
|
2322
|
+
var SITE_TIMEOUT_MS = 2e3;
|
|
2323
|
+
var REPO = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
2324
|
+
var SHA = /^[0-9a-f]{7,64}$/i;
|
|
2325
|
+
var ADDON = /^([a-z][a-z0-9_-]{0,30}@)?\d+\.\d+\.\d+$/;
|
|
2326
|
+
var MAX_PROJECT = 200;
|
|
2327
|
+
var MAX_BRANCH = 255;
|
|
2328
|
+
var MAX_PATH = 512;
|
|
2329
|
+
var MAX_DIRTY = 200;
|
|
2330
|
+
var text2 = (value, max) => typeof value === "string" && value !== "" && value.length <= max ? value : null;
|
|
2331
|
+
function normalizeSite(raw, fallbackProject = "") {
|
|
2332
|
+
const context = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
2333
|
+
const dirty = Array.isArray(context.dirty) ? context.dirty.filter((path) => text2(path, MAX_PATH)).slice(0, MAX_DIRTY) : [];
|
|
2334
|
+
return {
|
|
2335
|
+
repo: typeof context.repo === "string" && REPO.test(context.repo) ? context.repo : null,
|
|
2336
|
+
project: text2(context.project, MAX_PROJECT) ?? (fallbackProject || null),
|
|
2337
|
+
branch: text2(context.branch, MAX_BRANCH),
|
|
2338
|
+
sha: typeof context.sha === "string" && SHA.test(context.sha) ? context.sha.toLowerCase() : null,
|
|
2339
|
+
dirty,
|
|
2340
|
+
addon: typeof context.addon === "string" && ADDON.test(context.addon) ? context.addon : null,
|
|
2341
|
+
...context.save === true ? { save: true } : {}
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
async function loadSite(path, { fetchImpl = globalThis.fetch, timeoutMs = SITE_TIMEOUT_MS, fallbackProject = "" } = {}) {
|
|
2345
|
+
const fallback = () => normalizeSite(null, fallbackProject);
|
|
2346
|
+
if (!path || typeof fetchImpl !== "function") return fallback();
|
|
2347
|
+
const controller = new AbortController();
|
|
2348
|
+
let timer = 0;
|
|
2349
|
+
const timeout = new Promise((resolve) => {
|
|
2350
|
+
timer = setTimeout(() => {
|
|
2351
|
+
controller.abort();
|
|
2352
|
+
resolve(null);
|
|
2353
|
+
}, timeoutMs);
|
|
2354
|
+
});
|
|
2355
|
+
const request = (async () => {
|
|
2356
|
+
const response = await fetchImpl(path, {
|
|
2357
|
+
credentials: "same-origin",
|
|
2358
|
+
cache: "no-store",
|
|
2359
|
+
headers: { accept: "application/json" },
|
|
2360
|
+
signal: controller.signal
|
|
2361
|
+
});
|
|
2362
|
+
return response.ok ? await response.json() : null;
|
|
2363
|
+
})().catch(() => null);
|
|
2364
|
+
try {
|
|
2365
|
+
return normalizeSite(await Promise.race([request, timeout]), fallbackProject);
|
|
2366
|
+
} finally {
|
|
2367
|
+
clearTimeout(timer);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// js/in_context/snapshot.js
|
|
2372
|
+
var KIND_ORDER = ["text", "attribute", "title", "meta", "canvas"];
|
|
2373
|
+
var MAX_TEXT2 = 280;
|
|
2374
|
+
function clip(text3) {
|
|
2375
|
+
const clean = text3.replace(/\s+/g, " ").trim();
|
|
2376
|
+
return clean.length > MAX_TEXT2 ? `${clean.slice(0, MAX_TEXT2 - 1)}\u2026` : clean;
|
|
2377
|
+
}
|
|
2378
|
+
function aggregate(occurrences) {
|
|
2379
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2380
|
+
for (const occ of occurrences) {
|
|
2381
|
+
const id = keyId(occ.domain, occ.key, occ.locale);
|
|
2382
|
+
let row = rows.get(id);
|
|
2383
|
+
if (!row) {
|
|
2384
|
+
const { msgctxt } = splitKey(occ.key);
|
|
2385
|
+
row = {
|
|
2386
|
+
domain: occ.domain,
|
|
2387
|
+
key: occ.key,
|
|
2388
|
+
locale: occ.locale,
|
|
2389
|
+
...msgctxt === null ? {} : { msgctxt },
|
|
2390
|
+
text: clip(occ.text ?? ""),
|
|
2391
|
+
count: 0,
|
|
2392
|
+
on_screen: false,
|
|
2393
|
+
kinds: /* @__PURE__ */ new Set()
|
|
2394
|
+
};
|
|
2395
|
+
rows.set(id, row);
|
|
2396
|
+
}
|
|
2397
|
+
row.count += 1;
|
|
2398
|
+
row.on_screen = row.on_screen || occ.onScreen === true;
|
|
2399
|
+
row.kinds.add(occ.kind);
|
|
2400
|
+
}
|
|
2401
|
+
return [...rows.values()].map((row) => ({
|
|
2402
|
+
...row,
|
|
2403
|
+
kinds: KIND_ORDER.filter((kind) => row.kinds.has(kind))
|
|
2404
|
+
}));
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// js/in_context/stop.js
|
|
2408
|
+
var TOKEN_PARAM = "dialecto_edit";
|
|
2409
|
+
function stopUrl(value, origin) {
|
|
2410
|
+
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//")) return null;
|
|
2411
|
+
try {
|
|
2412
|
+
const url = new URL(value, origin);
|
|
2413
|
+
return url.origin === origin ? url.href : null;
|
|
2414
|
+
} catch {
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
function cleanUrl(href) {
|
|
2419
|
+
const url = new URL(href);
|
|
2420
|
+
url.searchParams.delete(TOKEN_PARAM);
|
|
2421
|
+
return url.href;
|
|
2422
|
+
}
|
|
2423
|
+
async function stopEditing(url, { fetch = globalThis.fetch, location: location2 = globalThis.location } = {}) {
|
|
2424
|
+
try {
|
|
2425
|
+
const response = await fetch(url, { method: "POST", credentials: "same-origin", cache: "no-store" });
|
|
2426
|
+
if (!response.ok) return false;
|
|
2427
|
+
} catch {
|
|
2428
|
+
return false;
|
|
2429
|
+
}
|
|
2430
|
+
location2.replace(cleanUrl(location2.href));
|
|
2431
|
+
return true;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// js/in_context/toggle.js
|
|
2435
|
+
var HIDDEN_KEY = "dialecto-in-context:hidden";
|
|
2436
|
+
var SHORTCUT_KEY = "dialecto-in-context:shortcut";
|
|
2437
|
+
var DEFAULT_SHORTCUT = "alt+shift+d";
|
|
2438
|
+
var ALIASES = { option: "alt", control: "ctrl", cmd: "meta", command: "meta" };
|
|
2439
|
+
var ORDER = ["ctrl", "alt", "shift", "meta"];
|
|
2440
|
+
var OS_TAKEN = /* @__PURE__ */ new Set(["d", "h", "m", "w", "i", "j", "c", "u", "b"]);
|
|
2441
|
+
var typing = (target) => target?.isContentEditable === true || /^(INPUT|TEXTAREA|SELECT)$/.test(target?.tagName ?? "");
|
|
2442
|
+
function parseCombo(value) {
|
|
2443
|
+
if (typeof value !== "string") return null;
|
|
2444
|
+
const mods = /* @__PURE__ */ new Set();
|
|
2445
|
+
let key = null;
|
|
2446
|
+
for (const raw of value.split("+")) {
|
|
2447
|
+
const token = raw.trim().toLowerCase();
|
|
2448
|
+
const name = ALIASES[token] ?? token;
|
|
2449
|
+
if (ORDER.includes(name)) {
|
|
2450
|
+
if (mods.has(name)) return null;
|
|
2451
|
+
mods.add(name);
|
|
2452
|
+
} else if (/^([a-z0-9]|f([1-9]|1[0-2]))$/.test(token) && key === null) {
|
|
2453
|
+
key = token;
|
|
2454
|
+
} else {
|
|
2455
|
+
return null;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
if (key === null) return null;
|
|
2459
|
+
if (!mods.has("alt") || mods.has("ctrl") && !mods.has("meta")) return null;
|
|
2460
|
+
if (mods.has("meta") && OS_TAKEN.has(key)) return null;
|
|
2461
|
+
return { ...Object.fromEntries(ORDER.map((m) => [m, mods.has(m)])), key };
|
|
2462
|
+
}
|
|
2463
|
+
function canonicalCombo(value) {
|
|
2464
|
+
const combo = parseCombo(value);
|
|
2465
|
+
return combo ? [...ORDER.filter((m) => combo[m]), combo.key].join("+") : "";
|
|
2466
|
+
}
|
|
2467
|
+
var codeOf = (key) => /^\d$/.test(key) ? `Digit${key}` : /^f/.test(key) && key.length > 1 ? key.toUpperCase() : `Key${key.toUpperCase()}`;
|
|
2468
|
+
function isToggle(event, value) {
|
|
2469
|
+
const combo = parseCombo(value);
|
|
2470
|
+
return combo !== null && event.code === codeOf(combo.key) && Boolean(event.ctrlKey) === combo.ctrl && Boolean(event.altKey) === combo.alt && Boolean(event.shiftKey) === combo.shift && Boolean(event.metaKey) === combo.meta && !event.repeat && !typing(event.target);
|
|
2471
|
+
}
|
|
2472
|
+
var MAC_SYMBOLS = { ctrl: "\u2303", alt: "\u2325", shift: "\u21E7", meta: "\u2318" };
|
|
2473
|
+
var PC_NAMES = { ctrl: "Ctrl", alt: "Alt", shift: "Shift", meta: "Meta" };
|
|
2474
|
+
var isMac = () => /Mac|iPhone|iPad/.test(globalThis.navigator?.platform ?? "");
|
|
2475
|
+
function displayCombo(value, mac = isMac()) {
|
|
2476
|
+
const combo = parseCombo(value);
|
|
2477
|
+
if (!combo) return "";
|
|
2478
|
+
const mods = ORDER.filter((m) => combo[m]);
|
|
2479
|
+
const key = combo.key.toUpperCase();
|
|
2480
|
+
return mac ? `${mods.map((m) => MAC_SYMBOLS[m]).join("")}${key}` : [...mods.map((m) => PC_NAMES[m]), key].join("+");
|
|
2481
|
+
}
|
|
2482
|
+
function readShortcut(storage = globalThis.localStorage) {
|
|
2483
|
+
try {
|
|
2484
|
+
return canonicalCombo(storage?.getItem(SHORTCUT_KEY));
|
|
2485
|
+
} catch {
|
|
2486
|
+
return "";
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function writeShortcut(value, storage = globalThis.localStorage) {
|
|
2490
|
+
const combo = canonicalCombo(value);
|
|
2491
|
+
try {
|
|
2492
|
+
if (combo) storage?.setItem(SHORTCUT_KEY, combo);
|
|
2493
|
+
else storage?.removeItem(SHORTCUT_KEY);
|
|
2494
|
+
} catch {
|
|
2495
|
+
}
|
|
2496
|
+
return combo !== "";
|
|
2497
|
+
}
|
|
2498
|
+
function readHidden(storage = globalThis.sessionStorage, fallback = false) {
|
|
2499
|
+
try {
|
|
2500
|
+
const stored = storage?.getItem(HIDDEN_KEY);
|
|
2501
|
+
return stored === "1" ? true : stored === "0" ? false : fallback;
|
|
2502
|
+
} catch {
|
|
2503
|
+
return fallback;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
function writeHidden(hidden, storage = globalThis.sessionStorage) {
|
|
2507
|
+
try {
|
|
2508
|
+
storage?.setItem(HIDDEN_KEY, hidden ? "1" : "0");
|
|
2509
|
+
} catch {
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// js/in_context/overlay.js
|
|
2514
|
+
var script = document.currentScript;
|
|
2515
|
+
var CORNERS = ["bottom-right", "bottom-left", "top-right", "top-left"];
|
|
2516
|
+
var flag = (value) => value === true || value === "true" ? true : value === false || value === "false" ? false : void 0;
|
|
2517
|
+
function readConfig() {
|
|
2518
|
+
const global = window.DIALECTO_IN_CONTEXT ?? {};
|
|
2519
|
+
const data = script?.dataset ?? {};
|
|
2520
|
+
return {
|
|
2521
|
+
url: data.url || global.url || "",
|
|
2522
|
+
// `repo` is what older add-on copies call the project.
|
|
2523
|
+
project: data.project || data.repo || global.project || global.repo || "",
|
|
2524
|
+
context: data.context ?? global.context ?? "",
|
|
2525
|
+
overrides: data.overrides ?? global.overrides ?? "",
|
|
2526
|
+
// Only the Phoenix add-on names one (spec-0017 §1.3).
|
|
2527
|
+
marking: data.marking ?? global.marking ?? "",
|
|
2528
|
+
// Live mode only: when the editor's launch token ends (unix seconds), so the
|
|
2529
|
+
// sign-in asked for here ends no later (spec-0025 §5).
|
|
2530
|
+
exp: Number.parseInt(data.exp ?? global.exp ?? "", 10),
|
|
2531
|
+
// Live mode only, and only from the add-on's own loader tag: the route that ends the session.
|
|
2532
|
+
stop: data.stop ?? "",
|
|
2533
|
+
// The project's shortcut. Any value, even an unusable one (an older add-on's bare letter), still means the
|
|
2534
|
+
// page opens hidden, as it did when that was the only shortcut.
|
|
2535
|
+
toggleKey: canonicalCombo(data.toggleKey ?? global.toggleKey),
|
|
2536
|
+
// Opens hidden until the shortcut; without it the tab's own choice, then `toggleKey`'s presence, decides.
|
|
2537
|
+
hidden: flag(data.hidden ?? global.hidden) ?? (String(data.toggleKey ?? global.toggleKey ?? "").trim() !== "" ? true : void 0),
|
|
2538
|
+
corner: CORNERS.includes(data.corner ?? global.corner) ? data.corner ?? global.corner : "bottom-right",
|
|
2539
|
+
// Only from the add-on's own loader tag: where its dev server saves drafts to files.
|
|
2540
|
+
save: data.save ?? ""
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
function install() {
|
|
2544
|
+
if (window.DialectoInContext) return;
|
|
2545
|
+
const config = readConfig();
|
|
2546
|
+
if (!config.url) {
|
|
2547
|
+
console.warn("[dialecto] in-context overlay needs a Dialecto URL; not started.");
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
let base;
|
|
2551
|
+
try {
|
|
2552
|
+
base = new URL(config.url);
|
|
2553
|
+
} catch {
|
|
2554
|
+
console.warn(`[dialecto] in-context overlay: "${config.url}" is not a valid URL; not started.`);
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
const sitePromise = config.context ? loadSite(config.context, { fallbackProject: config.project }) : Promise.resolve(normalizeSite(null, config.project));
|
|
2558
|
+
let site = normalizeSite(null, config.project);
|
|
2559
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
2560
|
+
let host = null;
|
|
2561
|
+
let panel = null;
|
|
2562
|
+
let bridge = null;
|
|
2563
|
+
let pendingPick = null;
|
|
2564
|
+
let measureFrame = 0;
|
|
2565
|
+
const isOwn = (node) => Boolean(host) && (node === host || host.contains(node));
|
|
2566
|
+
const viewport = () => ({ left: 0, top: 0, right: panel ? panel.usableRight() : window.innerWidth, bottom: window.innerHeight });
|
|
2567
|
+
const scanner = createScanner({
|
|
2568
|
+
isIgnored: isOwn,
|
|
2569
|
+
viewport,
|
|
2570
|
+
onChange: (slots) => {
|
|
2571
|
+
preview.applyToSlots(slots);
|
|
2572
|
+
scheduleMeasure();
|
|
2573
|
+
bridge?.requestSnapshot();
|
|
2574
|
+
}
|
|
2575
|
+
});
|
|
2576
|
+
const overrides = createOverrideSync(config.overrides);
|
|
2577
|
+
window.addEventListener("formdata", (event) => stripFormData(event.formData), { capture: true });
|
|
2578
|
+
const preview = createPreview({
|
|
2579
|
+
scanner,
|
|
2580
|
+
onStatus: (status) => {
|
|
2581
|
+
statuses.set(keyId(status.domain, status.key, status.locale), status);
|
|
2582
|
+
bridge?.sendPreviewStatus(status);
|
|
2583
|
+
},
|
|
2584
|
+
onEdits: (edits) => overrides.push(edits),
|
|
2585
|
+
onApplied: () => {
|
|
2586
|
+
scheduleMeasure();
|
|
2587
|
+
bridge?.requestSnapshot();
|
|
2588
|
+
}
|
|
2589
|
+
});
|
|
2590
|
+
window.addEventListener("dialecto:formatter", () => preview.reapply());
|
|
2591
|
+
const canvas = installCanvas({
|
|
2592
|
+
transform: (text3) => preview.transformString(text3),
|
|
2593
|
+
onResults: (results) => preview.noteCanvasResults(results),
|
|
2594
|
+
onSeen: () => {
|
|
2595
|
+
scheduleMeasure();
|
|
2596
|
+
bridge?.requestSnapshot();
|
|
2597
|
+
}
|
|
2598
|
+
});
|
|
2599
|
+
const marking = createMarking(config.marking, {
|
|
2600
|
+
hasMarkers: () => scanner.occurrences().length > 0 || canvas.occurrences().length > 0
|
|
2601
|
+
});
|
|
2602
|
+
let scanned = false;
|
|
2603
|
+
function currentStrings() {
|
|
2604
|
+
scanner.measure();
|
|
2605
|
+
canvas.measure(viewport());
|
|
2606
|
+
return aggregate([...scanner.occurrences(), ...canvas.occurrences()]);
|
|
2607
|
+
}
|
|
2608
|
+
function scheduleMeasure() {
|
|
2609
|
+
if (measureFrame) return;
|
|
2610
|
+
measureFrame = requestAnimationFrame(() => {
|
|
2611
|
+
measureFrame = 0;
|
|
2612
|
+
const textChanged = scanner.measure();
|
|
2613
|
+
const canvasChanged = canvas.measure(viewport());
|
|
2614
|
+
if (textChanged || canvasChanged) bridge?.requestSnapshot();
|
|
2615
|
+
highlighter?.schedule();
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
let highlighter = null;
|
|
2619
|
+
let picker = null;
|
|
2620
|
+
let hidden = readHidden(void 0, config.hidden === true);
|
|
2621
|
+
let reopen = false;
|
|
2622
|
+
let own = readShortcut();
|
|
2623
|
+
const baseShortcut = () => config.toggleKey || DEFAULT_SHORTCUT;
|
|
2624
|
+
const activeShortcut = () => own || baseShortcut();
|
|
2625
|
+
function setHidden(next) {
|
|
2626
|
+
if (next === hidden) return;
|
|
2627
|
+
hidden = next;
|
|
2628
|
+
writeHidden(hidden);
|
|
2629
|
+
if (scanned) applyHidden();
|
|
2630
|
+
}
|
|
2631
|
+
function setShortcut(value) {
|
|
2632
|
+
const next = canonicalCombo(value);
|
|
2633
|
+
if (value !== "" && next === "") return;
|
|
2634
|
+
own = next;
|
|
2635
|
+
writeShortcut(next);
|
|
2636
|
+
panel?.setShortcut(displayCombo(activeShortcut()));
|
|
2637
|
+
}
|
|
2638
|
+
function applyHidden() {
|
|
2639
|
+
if (hidden && document.activeElement === host) host.shadowRoot.activeElement?.blur();
|
|
2640
|
+
host.style.display = hidden ? "none" : "";
|
|
2641
|
+
picker.setPaused(hidden);
|
|
2642
|
+
if (hidden) {
|
|
2643
|
+
reopen = panel.isOpen();
|
|
2644
|
+
if (reopen) panel.close();
|
|
2645
|
+
} else if (reopen) {
|
|
2646
|
+
reopen = false;
|
|
2647
|
+
panel.open();
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
window.addEventListener(
|
|
2651
|
+
"keydown",
|
|
2652
|
+
(event) => {
|
|
2653
|
+
if (!isToggle(event, activeShortcut())) return;
|
|
2654
|
+
event.preventDefault();
|
|
2655
|
+
event.stopImmediatePropagation();
|
|
2656
|
+
setHidden(!hidden);
|
|
2657
|
+
},
|
|
2658
|
+
{ capture: true }
|
|
2659
|
+
);
|
|
2660
|
+
function handleMessage(message) {
|
|
2661
|
+
switch (message.type) {
|
|
2662
|
+
case "ready":
|
|
2663
|
+
panel.showReady();
|
|
2664
|
+
preview.reset(message.edits);
|
|
2665
|
+
if (pendingPick) {
|
|
2666
|
+
bridge.sendPick(pendingPick.domain, pendingPick.key);
|
|
2667
|
+
pendingPick = null;
|
|
2668
|
+
}
|
|
2669
|
+
break;
|
|
2670
|
+
case "preview":
|
|
2671
|
+
preview.set(message);
|
|
2672
|
+
break;
|
|
2673
|
+
case "preview_clear":
|
|
2674
|
+
preview.clear(message);
|
|
2675
|
+
break;
|
|
2676
|
+
case "previews_reset":
|
|
2677
|
+
preview.reset(message.edits);
|
|
2678
|
+
break;
|
|
2679
|
+
case "highlight":
|
|
2680
|
+
highlighter.setTarget(message.domain === null ? null : { domain: message.domain, key: message.key }, { scroll: message.scroll });
|
|
2681
|
+
break;
|
|
2682
|
+
case "pick_mode":
|
|
2683
|
+
picker.setPickMode(message.on);
|
|
2684
|
+
break;
|
|
2685
|
+
case "save_files":
|
|
2686
|
+
saveFiles(config.save, message.edits).then((result) => bridge.sendFilesSaved(result));
|
|
2687
|
+
break;
|
|
2688
|
+
case "close":
|
|
2689
|
+
panel.close();
|
|
2690
|
+
break;
|
|
2691
|
+
case "toggle":
|
|
2692
|
+
setHidden(!hidden);
|
|
2693
|
+
break;
|
|
2694
|
+
case "shortcut":
|
|
2695
|
+
setShortcut(message.key);
|
|
2696
|
+
break;
|
|
2697
|
+
case "reload":
|
|
2698
|
+
location.reload();
|
|
2699
|
+
break;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
async function start() {
|
|
2703
|
+
host = document.createElement("div");
|
|
2704
|
+
host.setAttribute("data-dialecto-host", "");
|
|
2705
|
+
host.style.cssText = "all: initial";
|
|
2706
|
+
const root = host.attachShadow({ mode: "open" });
|
|
2707
|
+
document.documentElement.append(host);
|
|
2708
|
+
site = await sitePromise;
|
|
2709
|
+
if (Number.isSafeInteger(config.exp) && config.exp > 0) site = { ...site, exp: config.exp };
|
|
2710
|
+
const src = sidebarUrl(base, { site: site.repo, project: site.project, locale: document.documentElement.lang });
|
|
2711
|
+
highlighter = createHighlighter({ root, scanner, canvas, getViewport: viewport, onTab: () => picker.pickHover() });
|
|
2712
|
+
picker = createPicker({
|
|
2713
|
+
scanner,
|
|
2714
|
+
canvas,
|
|
2715
|
+
highlighter,
|
|
2716
|
+
isInside: isOwn,
|
|
2717
|
+
isEditing: () => panel?.isOpen() === true,
|
|
2718
|
+
onPick: ({ domain, key }) => {
|
|
2719
|
+
if (bridge.isReady()) {
|
|
2720
|
+
bridge.sendPick(domain, key);
|
|
2721
|
+
} else {
|
|
2722
|
+
pendingPick = { domain, key };
|
|
2723
|
+
panel.open();
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
});
|
|
2727
|
+
const stopRoute = stopUrl(config.stop, location.origin);
|
|
2728
|
+
panel = createPanel({
|
|
2729
|
+
root,
|
|
2730
|
+
corner: config.corner,
|
|
2731
|
+
src,
|
|
2732
|
+
dialectoUrl: base.origin,
|
|
2733
|
+
siteOrigin: location.origin,
|
|
2734
|
+
onFrameLoad: () => bridge.startHandshake(),
|
|
2735
|
+
onRetry: () => bridge.startHandshake(),
|
|
2736
|
+
onStop: stopRoute ? () => stopEditing(stopRoute) : null,
|
|
2737
|
+
onOpenChange: (open) => {
|
|
2738
|
+
scheduleMeasure();
|
|
2739
|
+
picker.refresh();
|
|
2740
|
+
if (scanned) marking.setOpen(open);
|
|
2741
|
+
}
|
|
2742
|
+
});
|
|
2743
|
+
bridge = createBridge({
|
|
2744
|
+
origin: base.origin,
|
|
2745
|
+
frameWindow: () => panel.frameWindow(),
|
|
2746
|
+
page: () => ({ url: location.href, title: stripMarks(document.title), locale: document.documentElement.lang }),
|
|
2747
|
+
site: () => site,
|
|
2748
|
+
strings: currentStrings,
|
|
2749
|
+
onMessage: handleMessage,
|
|
2750
|
+
onTimeout: () => panel.showError(),
|
|
2751
|
+
toggleKey: activeShortcut,
|
|
2752
|
+
baseKey: baseShortcut
|
|
2753
|
+
});
|
|
2754
|
+
panel.setShortcut(displayCombo(activeShortcut()));
|
|
2755
|
+
const initial = scanner.start();
|
|
2756
|
+
if (initial.length) preview.applyToSlots(initial);
|
|
2757
|
+
scheduleMeasure();
|
|
2758
|
+
scanned = true;
|
|
2759
|
+
marking.setOpen(panel.isOpen());
|
|
2760
|
+
if (hidden) applyHidden();
|
|
2761
|
+
window.addEventListener("scroll", scheduleMeasure, { passive: true, capture: true });
|
|
2762
|
+
window.addEventListener("resize", scheduleMeasure, { passive: true });
|
|
2763
|
+
window.addEventListener("transitionend", scheduleMeasure, { passive: true, capture: true });
|
|
2764
|
+
window.addEventListener("animationend", scheduleMeasure, { passive: true, capture: true });
|
|
2765
|
+
if (typeof ResizeObserver === "function") new ResizeObserver(scheduleMeasure).observe(document.documentElement);
|
|
2766
|
+
}
|
|
2767
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start, { once: true });
|
|
2768
|
+
else start();
|
|
2769
|
+
window.DialectoInContext = {
|
|
2770
|
+
open: () => panel?.open(),
|
|
2771
|
+
close: () => panel?.close(),
|
|
2772
|
+
show: () => setHidden(false),
|
|
2773
|
+
hide: () => setHidden(true),
|
|
2774
|
+
strings: () => currentStrings(),
|
|
2775
|
+
preview(edit) {
|
|
2776
|
+
preview.set(edit);
|
|
2777
|
+
return statuses.get(keyId(edit.domain, edit.key, edit.locale)) ?? null;
|
|
2778
|
+
},
|
|
2779
|
+
clearPreviews: () => preview.reset([]),
|
|
2780
|
+
edits: () => preview.edits()
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
install();
|
|
2784
|
+
})();
|