@freereportjs/core 0.8.0 → 0.10.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.
package/dist/index.js CHANGED
@@ -186,138 +186,6 @@ function pdfFonts(pages) {
186
186
  return out;
187
187
  }
188
188
 
189
- // src/version.ts
190
- var version = "0.8.0";
191
- var releaseDate = "2026-10-15";
192
-
193
- // src/license.ts
194
- var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "MiJXJR8TZJ_-0VKk0wIQ-5mzRsyQ6ruGsbcRRdw9jrw", y: "MTdklYSzCM2pganffz7PPMfxSclQkBLqU_9se_VDznM" };
195
- var PRICING_URL = "https://freereport.app/pricing/";
196
- var USAGE_URL = "https://freereport.app/api/usage";
197
- var STATUS_URL = "https://freereport.app/api/domain-status";
198
- var b64urlDecode = (s) => {
199
- const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
200
- return Uint8Array.from(bin, (c) => c.charCodeAt(0));
201
- };
202
- function isDevHost(host) {
203
- const h = host.toLowerCase().replace(/^\[|\]$/g, "");
204
- return !h || h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0" || h.endsWith(".localhost") || h.endsWith(".test") || h.endsWith(".local") || /^(10\.\d+|192\.168|172\.(1[6-9]|2\d|3[01]))\.\d+\.\d+$/.test(h);
205
- }
206
- function domainMatches(host, domains) {
207
- const h = host.toLowerCase().replace(/\.$/, "");
208
- return domains.some((d) => {
209
- const raw = d.toLowerCase().replace(/\.$/, "");
210
- const wildcard = raw.startsWith("*.");
211
- const dom = wildcard ? raw.slice(2) : raw;
212
- if (!dom) return false;
213
- if (h === dom || h === `www.${dom}`) return true;
214
- return wildcard && h.endsWith(`.${dom}`);
215
- });
216
- }
217
- var today = (now) => `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
218
- async function verifyLicense(key2, host, opts = {}) {
219
- const parts = typeof key2 === "string" ? key2.trim().split(".") : [];
220
- if (parts.length !== 3 || parts[0] !== "FR1") return { ok: false, reason: "format" };
221
- let info;
222
- try {
223
- info = JSON.parse(new TextDecoder().decode(b64urlDecode(parts[1])));
224
- if (!Array.isArray(info.domains) || typeof info.expires !== "string") return { ok: false, reason: "format" };
225
- } catch {
226
- return { ok: false, reason: "format" };
227
- }
228
- const subtle = typeof crypto !== "undefined" ? crypto.subtle : void 0;
229
- if (!subtle) return { ok: false, reason: "unsupported", info };
230
- try {
231
- const pub = await subtle.importKey("jwk", opts.publicKey ?? PUBLIC_KEY, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
232
- const signed = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
233
- const ok = await subtle.verify({ name: "ECDSA", hash: "SHA-256" }, pub, b64urlDecode(parts[2]), signed);
234
- if (!ok) return { ok: false, reason: "signature", info };
235
- } catch {
236
- return { ok: false, reason: "signature", info };
237
- }
238
- if (today(opts.now ?? /* @__PURE__ */ new Date()) > info.expires) return { ok: false, reason: "expired", info };
239
- if (!domainMatches(host, info.domains)) return { ok: false, reason: "domain", info };
240
- if (info.updates && (opts.released ?? releaseDate) > info.updates) return { ok: false, reason: "updates", info };
241
- return { ok: true, info };
242
- }
243
- var globalKey;
244
- var statuses = /* @__PURE__ */ new Map();
245
- var badgeCheck = null;
246
- function setLicenseKey(key2) {
247
- globalKey = key2;
248
- }
249
- var NOTICE = {
250
- missing: `FreeReport is free for personal use. Company use needs a domain license (\u20A9500,000 per domain per year, or \u20A9900,000 perpetual): ${PRICING_URL}
251
- FreeReport\uB294 \uAC1C\uC778\uC5D0\uAC8C \uBB34\uB8CC\uC785\uB2C8\uB2E4. \uD68C\uC0AC\uC5D0\uC11C \uC4F0\uB824\uBA74 \uB3C4\uBA54\uC778 \uB77C\uC774\uC120\uC2A4(\uB3C4\uBA54\uC778\uB2F9 \uC5F0 50\uB9CC \uC6D0, \uC601\uAD6C 90\uB9CC \uC6D0)\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4: ${PRICING_URL}`,
252
- format: "FreeReport: the license key is not in the right format. / \uB77C\uC774\uC120\uC2A4 \uD0A4 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
253
- signature: "FreeReport: the license key could not be verified. / \uB77C\uC774\uC120\uC2A4 \uD0A4\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
254
- expired: "FreeReport: the license key has expired. / \uB77C\uC774\uC120\uC2A4 \uAE30\uAC04\uC774 \uB05D\uB0AC\uC2B5\uB2C8\uB2E4.",
255
- domain: "FreeReport: the license key is for a different domain. / \uB2E4\uB978 \uB3C4\uBA54\uC778\uC6A9 \uB77C\uC774\uC120\uC2A4 \uD0A4\uC785\uB2C8\uB2E4.",
256
- updates: "FreeReport: this version was released after the update period of your perpetual license. / \uC601\uAD6C \uB77C\uC774\uC120\uC2A4\uC758 \uC5C5\uB370\uC774\uD2B8 \uAE30\uAC04 \uB4A4\uC5D0 \uB098\uC628 \uBC84\uC804\uC785\uB2C8\uB2E4."
257
- };
258
- function reportUsage(info, host = typeof location !== "undefined" ? location.hostname : "") {
259
- try {
260
- if (isDevHost(host) || typeof navigator === "undefined") return;
261
- const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
262
- const mark = `${host}|${day}|${info.licensed ? 1 : 0}`;
263
- const store = typeof localStorage !== "undefined" ? localStorage : null;
264
- if (store?.getItem("freereport:usage") === mark) return;
265
- store?.setItem("freereport:usage", mark);
266
- const body = JSON.stringify({ version: info.version, licensed: info.licensed, company: info.company, url: `${location.origin}${location.pathname}` });
267
- if (typeof navigator.sendBeacon === "function") navigator.sendBeacon(USAGE_URL, body);
268
- else if (typeof fetch === "function") void fetch(USAGE_URL, { method: "POST", body, mode: "no-cors", keepalive: true }).catch(() => void 0);
269
- } catch {
270
- }
271
- }
272
- function checkLicense(key2 = globalKey, host = typeof location !== "undefined" ? location.hostname : "") {
273
- if (isDevHost(host)) return Promise.resolve(true);
274
- const k = key2 ?? globalKey;
275
- const id = `${k ?? ""}@${host}`;
276
- let status = statuses.get(id);
277
- if (status) return status;
278
- if (!k) {
279
- console.info(NOTICE.missing);
280
- reportUsage({ version, licensed: false }, host);
281
- status = Promise.resolve(false);
282
- } else {
283
- status = verifyLicense(k, host).then((res) => {
284
- reportUsage({ version, licensed: res.ok, company: res.ok ? res.info.company : void 0 }, host);
285
- if (res.ok || res.reason === "unsupported") return res.ok || res.reason === "unsupported";
286
- const extra = res.info ? ` (${res.info.domains.join(", ")}, ${res.info.expires})` : "";
287
- console.warn(`${NOTICE[res.reason]}${extra}
288
- ${PRICING_URL}`);
289
- return false;
290
- }, () => false);
291
- }
292
- statuses.set(id, status);
293
- return status;
294
- }
295
- function badgeRequested(host = typeof location !== "undefined" ? location.hostname : "") {
296
- if (isDevHost(host) || typeof fetch !== "function") return Promise.resolve(false);
297
- if (badgeCheck) return badgeCheck;
298
- const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
299
- let store = null;
300
- try {
301
- store = typeof localStorage !== "undefined" ? localStorage : null;
302
- const cached = JSON.parse(store?.getItem("freereport:badge") ?? "null");
303
- if (cached && cached.host === host && cached.day === day) return badgeCheck = Promise.resolve(cached.show);
304
- } catch {
305
- }
306
- badgeCheck = fetch(STATUS_URL, { mode: "cors", credentials: "omit" }).then((r) => r.ok ? r.json() : { badge: false }).then((d) => {
307
- const show = d.badge === true;
308
- try {
309
- store?.setItem("freereport:badge", JSON.stringify({ host, day, show }));
310
- } catch {
311
- }
312
- return show;
313
- }, () => false);
314
- return badgeCheck;
315
- }
316
- async function needsFreeBadge(key2, host = typeof location !== "undefined" ? location.hostname : "") {
317
- if (await checkLicense(key2, host)) return false;
318
- return badgeRequested(host);
319
- }
320
-
321
189
  // src/format.ts
322
190
  var DIGITS = ["", "\uC77C", "\uC774", "\uC0BC", "\uC0AC", "\uC624", "\uC721", "\uCE60", "\uD314", "\uAD6C"];
323
191
  var SMALL = ["", "\uC2ED", "\uBC31", "\uCC9C"];
@@ -499,6 +367,138 @@ function formatValue(v, format, locale = "ko") {
499
367
  return String(v);
500
368
  }
501
369
 
370
+ // src/version.ts
371
+ var version = "0.10.0";
372
+ var releaseDate = "2026-10-19";
373
+
374
+ // src/license.ts
375
+ var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "MiJXJR8TZJ_-0VKk0wIQ-5mzRsyQ6ruGsbcRRdw9jrw", y: "MTdklYSzCM2pganffz7PPMfxSclQkBLqU_9se_VDznM" };
376
+ var PRICING_URL = "https://freereport.app/pricing/";
377
+ var USAGE_URL = "https://freereport.app/api/usage";
378
+ var STATUS_URL = "https://freereport.app/api/domain-status";
379
+ var b64urlDecode = (s) => {
380
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
381
+ return Uint8Array.from(bin, (c) => c.charCodeAt(0));
382
+ };
383
+ function isDevHost(host) {
384
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
385
+ return !h || h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0" || h.endsWith(".localhost") || h.endsWith(".test") || h.endsWith(".local") || /^(10\.\d+|192\.168|172\.(1[6-9]|2\d|3[01]))\.\d+\.\d+$/.test(h);
386
+ }
387
+ function domainMatches(host, domains) {
388
+ const h = host.toLowerCase().replace(/\.$/, "");
389
+ return domains.some((d) => {
390
+ const raw = d.toLowerCase().replace(/\.$/, "");
391
+ const wildcard = raw.startsWith("*.");
392
+ const dom = wildcard ? raw.slice(2) : raw;
393
+ if (!dom) return false;
394
+ if (h === dom || h === `www.${dom}`) return true;
395
+ return wildcard && h.endsWith(`.${dom}`);
396
+ });
397
+ }
398
+ var today = (now) => `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
399
+ async function verifyLicense(key2, host, opts = {}) {
400
+ const parts = typeof key2 === "string" ? key2.trim().split(".") : [];
401
+ if (parts.length !== 3 || parts[0] !== "FR1") return { ok: false, reason: "format" };
402
+ let info;
403
+ try {
404
+ info = JSON.parse(new TextDecoder().decode(b64urlDecode(parts[1])));
405
+ if (!Array.isArray(info.domains) || typeof info.expires !== "string") return { ok: false, reason: "format" };
406
+ } catch {
407
+ return { ok: false, reason: "format" };
408
+ }
409
+ const subtle = typeof crypto !== "undefined" ? crypto.subtle : void 0;
410
+ if (!subtle) return { ok: false, reason: "unsupported", info };
411
+ try {
412
+ const pub = await subtle.importKey("jwk", opts.publicKey ?? PUBLIC_KEY, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
413
+ const signed = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
414
+ const ok = await subtle.verify({ name: "ECDSA", hash: "SHA-256" }, pub, b64urlDecode(parts[2]), signed);
415
+ if (!ok) return { ok: false, reason: "signature", info };
416
+ } catch {
417
+ return { ok: false, reason: "signature", info };
418
+ }
419
+ if (today(opts.now ?? /* @__PURE__ */ new Date()) > info.expires) return { ok: false, reason: "expired", info };
420
+ if (!domainMatches(host, info.domains)) return { ok: false, reason: "domain", info };
421
+ if (info.updates && (opts.released ?? releaseDate) > info.updates) return { ok: false, reason: "updates", info };
422
+ return { ok: true, info };
423
+ }
424
+ var globalKey;
425
+ var statuses = /* @__PURE__ */ new Map();
426
+ var badgeCheck = null;
427
+ function setLicenseKey(key2) {
428
+ globalKey = key2;
429
+ }
430
+ var NOTICE = {
431
+ missing: `FreeReport is free for personal use. Company use needs a domain license (\u20A9500,000 per domain per year, or \u20A9900,000 perpetual): ${PRICING_URL}
432
+ FreeReport\uB294 \uAC1C\uC778\uC5D0\uAC8C \uBB34\uB8CC\uC785\uB2C8\uB2E4. \uD68C\uC0AC\uC5D0\uC11C \uC4F0\uB824\uBA74 \uB3C4\uBA54\uC778 \uB77C\uC774\uC120\uC2A4(\uB3C4\uBA54\uC778\uB2F9 \uC5F0 50\uB9CC \uC6D0, \uC601\uAD6C 90\uB9CC \uC6D0)\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4: ${PRICING_URL}`,
433
+ format: "FreeReport: the license key is not in the right format. / \uB77C\uC774\uC120\uC2A4 \uD0A4 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
434
+ signature: "FreeReport: the license key could not be verified. / \uB77C\uC774\uC120\uC2A4 \uD0A4\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
435
+ expired: "FreeReport: the license key has expired. / \uB77C\uC774\uC120\uC2A4 \uAE30\uAC04\uC774 \uB05D\uB0AC\uC2B5\uB2C8\uB2E4.",
436
+ domain: "FreeReport: the license key is for a different domain. / \uB2E4\uB978 \uB3C4\uBA54\uC778\uC6A9 \uB77C\uC774\uC120\uC2A4 \uD0A4\uC785\uB2C8\uB2E4.",
437
+ updates: "FreeReport: this version was released after the update period of your perpetual license. / \uC601\uAD6C \uB77C\uC774\uC120\uC2A4\uC758 \uC5C5\uB370\uC774\uD2B8 \uAE30\uAC04 \uB4A4\uC5D0 \uB098\uC628 \uBC84\uC804\uC785\uB2C8\uB2E4."
438
+ };
439
+ function reportUsage(info, host = typeof location !== "undefined" ? location.hostname : "") {
440
+ try {
441
+ if (isDevHost(host) || typeof navigator === "undefined") return;
442
+ const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
443
+ const mark = `${host}|${day}|${info.licensed ? 1 : 0}`;
444
+ const store = typeof localStorage !== "undefined" ? localStorage : null;
445
+ if (store?.getItem("freereport:usage") === mark) return;
446
+ store?.setItem("freereport:usage", mark);
447
+ const body = JSON.stringify({ version: info.version, licensed: info.licensed, company: info.company, url: `${location.origin}${location.pathname}` });
448
+ if (typeof navigator.sendBeacon === "function") navigator.sendBeacon(USAGE_URL, body);
449
+ else if (typeof fetch === "function") void fetch(USAGE_URL, { method: "POST", body, mode: "no-cors", keepalive: true }).catch(() => void 0);
450
+ } catch {
451
+ }
452
+ }
453
+ function checkLicense(key2 = globalKey, host = typeof location !== "undefined" ? location.hostname : "") {
454
+ if (isDevHost(host)) return Promise.resolve(true);
455
+ const k = key2 ?? globalKey;
456
+ const id = `${k ?? ""}@${host}`;
457
+ let status = statuses.get(id);
458
+ if (status) return status;
459
+ if (!k) {
460
+ console.info(NOTICE.missing);
461
+ reportUsage({ version, licensed: false }, host);
462
+ status = Promise.resolve(false);
463
+ } else {
464
+ status = verifyLicense(k, host).then((res) => {
465
+ reportUsage({ version, licensed: res.ok, company: res.ok ? res.info.company : void 0 }, host);
466
+ if (res.ok || res.reason === "unsupported") return res.ok || res.reason === "unsupported";
467
+ const extra = res.info ? ` (${res.info.domains.join(", ")}, ${res.info.expires})` : "";
468
+ console.warn(`${NOTICE[res.reason]}${extra}
469
+ ${PRICING_URL}`);
470
+ return false;
471
+ }, () => false);
472
+ }
473
+ statuses.set(id, status);
474
+ return status;
475
+ }
476
+ function badgeRequested(host = typeof location !== "undefined" ? location.hostname : "") {
477
+ if (isDevHost(host) || typeof fetch !== "function") return Promise.resolve(false);
478
+ if (badgeCheck) return badgeCheck;
479
+ const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
480
+ let store = null;
481
+ try {
482
+ store = typeof localStorage !== "undefined" ? localStorage : null;
483
+ const cached = JSON.parse(store?.getItem("freereport:badge") ?? "null");
484
+ if (cached && cached.host === host && cached.day === day) return badgeCheck = Promise.resolve(cached.show);
485
+ } catch {
486
+ }
487
+ badgeCheck = fetch(STATUS_URL, { mode: "cors", credentials: "omit" }).then((r) => r.ok ? r.json() : { badge: false }).then((d) => {
488
+ const show = d.badge === true;
489
+ try {
490
+ store?.setItem("freereport:badge", JSON.stringify({ host, day, show }));
491
+ } catch {
492
+ }
493
+ return show;
494
+ }, () => false);
495
+ return badgeCheck;
496
+ }
497
+ async function needsFreeBadge(key2, host = typeof location !== "undefined" ? location.hostname : "") {
498
+ if (await checkLicense(key2, host)) return false;
499
+ return badgeRequested(host);
500
+ }
501
+
502
502
  // src/expr.ts
503
503
  var OPS = ["===", "!==", "==", "!=", "<=", ">=", "&&", "||", "??", "<", ">", "+", "-", "*", "/", "%", "!", "?", ":", "(", ")", ",", ".", "[", "]"];
504
504
  function tokenize(src) {
@@ -933,7 +933,7 @@ function checkExpression(src) {
933
933
  return e.message;
934
934
  }
935
935
  }
936
- function interpolate(template, scope) {
936
+ function interpolate(template, scope, locale) {
937
937
  if (!template.includes("{")) return template.replace(/}}/g, "}");
938
938
  let out = "";
939
939
  let i = 0;
@@ -961,7 +961,7 @@ function interpolate(template, scope) {
961
961
  }
962
962
  const { expr, format } = splitFormat(template.slice(i + 1, end));
963
963
  const v = evaluate(expr, scope);
964
- out += formatValue(v, format);
964
+ out += formatValue(v, format, locale);
965
965
  i = end + 1;
966
966
  }
967
967
  return out;
@@ -1335,6 +1335,11 @@ function pathData(d) {
1335
1335
  return d.map((v) => typeof v === "number" ? n(v) : v).join(" ");
1336
1336
  }
1337
1337
  var opacityAttr = (o) => o !== void 0 && o < 1 ? ` opacity="${n(o)}"` : "";
1338
+ var sourceAttrs = (it) => {
1339
+ const s = it.source;
1340
+ if (!s?.elementId) return "";
1341
+ return ` data-fr-id="${esc(s.elementId)}" data-fr-kind="${it.kind}"${s.row !== void 0 ? ` data-fr-row="${s.row}"` : ""}`;
1342
+ };
1338
1343
  function textSvg(t, clipId) {
1339
1344
  const sizeMm = t.font.size * PT;
1340
1345
  const family = `${quoteFamily(t.font.family)}, ${FALLBACK_FONTS}`.replace(/"/g, "'");
@@ -1348,11 +1353,12 @@ function textSvg(t, clipId) {
1348
1353
  }
1349
1354
  const overflow = t.source?.band !== "watermark" && t.lines.some((l) => l.x < t.x - 0.05 || l.x + l.width > t.x + t.w + 0.05 || l.y - sizeMm * 0.8 < t.y - 0.05 || l.y + sizeMm * 0.2 > t.y + t.h + 0.05);
1350
1355
  const transform = t.rotate ? ` transform="rotate(${n(t.rotate)} ${n(t.x + t.w / 2)} ${n(t.y + t.h / 2)})"` : "";
1351
- if (!overflow) return { defs: "", body: `<g ${attrs}${transform}${opacityAttr(t.opacity)}>${body}</g>` };
1356
+ const hooks = sourceAttrs(t) + (t.overflow ? ' data-fr-overflow="1"' : "");
1357
+ if (!overflow) return { defs: "", body: `<g ${attrs}${hooks}${transform}${opacityAttr(t.opacity)}>${body}</g>` };
1352
1358
  const id = clipId();
1353
1359
  return {
1354
1360
  defs: `<clipPath id="${id}"><rect x="${n(t.x)}" y="${n(t.y)}" width="${n(t.w)}" height="${n(t.h)}"/></clipPath>`,
1355
- body: `<g ${attrs} clip-path="url(#${id})"${transform}${opacityAttr(t.opacity)}>${body}</g>`
1361
+ body: `<g ${attrs}${hooks} clip-path="url(#${id})"${transform}${opacityAttr(t.opacity)}>${body}</g>`
1356
1362
  };
1357
1363
  }
1358
1364
  function itemSvg(it, clipId) {
@@ -1401,7 +1407,8 @@ function pageToSvg(page, opts = {}) {
1401
1407
  for (const it of page.items) {
1402
1408
  const r = itemSvg(it, clipId);
1403
1409
  defs += r.defs;
1404
- body += r.body;
1410
+ const hooks = it.kind === "text" ? "" : sourceAttrs(it);
1411
+ body += hooks && r.body ? `<g${hooks}>${r.body}</g>` : r.body;
1405
1412
  }
1406
1413
  const size = opts.scale ? `width="${n(page.width * opts.scale)}" height="${n(page.height * opts.scale)}"` : `width="${n(page.width)}mm" height="${n(page.height)}mm"`;
1407
1414
  return `<svg xmlns="http://www.w3.org/2000/svg" ${size} viewBox="0 0 ${n(page.width)} ${n(page.height)}"${opts.className ? ` class="${opts.className}"` : ""} text-rendering="geometricPrecision">${defs ? `<defs>${defs}</defs>` : ""}<rect width="100%" height="100%" fill="#fff"/>${body}</svg>`;
@@ -2900,7 +2907,7 @@ function prepare(el, scope, ctx, bandCanGrow) {
2900
2907
  let text = "";
2901
2908
  let value;
2902
2909
  if (!ctx.blank) {
2903
- if (el.type === "text") text = interpolate(el.text ?? "", scope);
2910
+ if (el.type === "text") text = interpolate(el.text ?? "", scope, ctx.locale);
2904
2911
  else {
2905
2912
  value = evaluate(el.expr, scope);
2906
2913
  text = value == null || value === "" ? el.nullText ?? "" : formatValue(value, el.format, ctx.locale);
@@ -2945,6 +2952,7 @@ function textItem(x, y, w, h, lines, font, st, measure) {
2945
2952
  else if (st.align === "right") lx = x + w - pr - width;
2946
2953
  return { text, x: lx, y: top + i * lineH + (lineH - font.size * PT) / 2 + font.size * PT * ASCENT, width };
2947
2954
  });
2955
+ const overflow = total > innerH + 0.05 || out.some((l) => l.width > w - pl - pr + 0.05);
2948
2956
  return {
2949
2957
  kind: "text",
2950
2958
  x,
@@ -2959,6 +2967,7 @@ function textItem(x, y, w, h, lines, font, st, measure) {
2959
2967
  underline: st.underline || void 0,
2960
2968
  strike: st.strike || void 0,
2961
2969
  letterSpacing: st.letterSpacing || void 0,
2970
+ overflow: overflow || void 0,
2962
2971
  text: lines.join("\n")
2963
2972
  };
2964
2973
  }
@@ -3027,25 +3036,68 @@ function barcodeItems(el, x, y, w, h, scope, ctx) {
3027
3036
  return [textItem(x, y, w, h, ["(barcode?)"], { family: "sans-serif", size: 7, bold: false, italic: false }, { ...resolveStyle(ctx.def, el), color: "#cc0000" }, ctx.measure)];
3028
3037
  }
3029
3038
  }
3039
+ function shifts(prepared) {
3040
+ let grew = false;
3041
+ for (const p of prepared) if (p.needed > p.el.h + 0.01) {
3042
+ grew = true;
3043
+ break;
3044
+ }
3045
+ if (!grew) return null;
3046
+ const out = /* @__PURE__ */ new Map();
3047
+ const top = (p) => Math.min(p.el.y, p.el.y + p.el.h);
3048
+ const bottom = (p) => Math.max(p.el.y, p.el.y + p.el.h);
3049
+ const order = [...prepared].sort((a, b) => top(a) - top(b));
3050
+ for (const p of order) {
3051
+ if (p.el.shift === false) continue;
3052
+ let by = 0;
3053
+ for (const q of order) {
3054
+ if (top(q) >= top(p)) break;
3055
+ if (bottom(q) > top(p) + 0.01) continue;
3056
+ by = Math.max(by, (out.get(q) ?? 0) + Math.max(0, q.needed - q.el.h));
3057
+ }
3058
+ if (by > 0) out.set(p, by);
3059
+ }
3060
+ return out;
3061
+ }
3062
+ function addAnchor(out, el, ctx, left, top, y, h, hidden) {
3063
+ if (!el.id) return;
3064
+ const line = el.type === "line";
3065
+ out.push({
3066
+ id: el.id,
3067
+ band: ctx.kind,
3068
+ row: ctx.rowIndex,
3069
+ x: left + (line ? Math.min(el.x, el.x + el.w) : el.x),
3070
+ y: top + (line ? Math.min(y, y + h) : y),
3071
+ w: line ? Math.abs(el.w) : el.w,
3072
+ h: line ? Math.abs(h) : h,
3073
+ ...hidden ? { hidden } : {}
3074
+ });
3075
+ }
3030
3076
  function renderBand(band, scope, ctx, left, top, minHeight = 0) {
3031
3077
  const bandGrow = band.canGrow ?? (ctx.kind === "detail" || ctx.kind === "groupHeader" || ctx.kind === "groupFooter" || ctx.kind === "summary" || ctx.kind === "title");
3032
3078
  const prepared = [];
3079
+ const anchors = ctx.anchors && !ctx.blank ? [] : void 0;
3033
3080
  for (const el of band.elements) {
3034
- if (el.visible && !ctx.blank && !truthy(evaluate(el.visible, scope))) continue;
3081
+ if (el.visible && !ctx.blank && !truthy(evaluate(el.visible, scope))) {
3082
+ if (anchors) addAnchor(anchors, el, ctx, left, top, el.y, el.h, true);
3083
+ continue;
3084
+ }
3035
3085
  prepared.push(prepare(el, scope, ctx, bandGrow));
3036
3086
  }
3087
+ const shift2 = shifts(prepared);
3037
3088
  let height = Math.max(band.height, minHeight);
3038
- for (const p of prepared) height = Math.max(height, p.el.y + p.needed);
3089
+ for (const p of prepared) height = Math.max(height, p.el.y + (shift2?.get(p) ?? 0) + p.needed);
3039
3090
  const items = [];
3040
3091
  const bg = ctx.background ?? band.background;
3041
3092
  if (bg) items.push({ kind: "box", x: left, y: top, w: ctx.width, h: height, fill: bg, source: { band: ctx.kind, row: ctx.rowIndex } });
3042
- const growth = height - band.height;
3043
3093
  for (const p of prepared) {
3044
3094
  const el = p.el;
3095
+ const elY = shift2 ? el.y + (shift2.get(p) ?? 0) : el.y;
3045
3096
  const touchesBottom = el.y + el.h >= band.height - 0.01;
3046
3097
  let h = p.needed;
3047
- if ((el.stretch ?? touchesBottom) && el.type !== "line" && el.type !== "table" && el.type !== "crosstab") h = Math.max(h, el.h + growth, height - el.y);
3048
- const x = left + el.x, y = top + el.y;
3098
+ if ((el.stretch ?? touchesBottom) && el.type !== "line" && el.type !== "table" && el.type !== "crosstab") h = Math.max(h, height - elY);
3099
+ const x = left + el.x, y = top + elY;
3100
+ if (anchors) addAnchor(anchors, el, ctx, left, top, elY, el.type === "line" ? el.h === 0 ? 0 : el.y + el.h >= band.height - 0.01 ? Math.max(el.h, height - elY) : el.h : h);
3049
3101
  switch (el.type) {
3050
3102
  case "text":
3051
3103
  case "field": {
@@ -3060,7 +3112,7 @@ function renderBand(band, scope, ctx, left, top, minHeight = 0) {
3060
3112
  break;
3061
3113
  }
3062
3114
  case "line": {
3063
- const lh = el.h === 0 ? 0 : el.y + el.h >= band.height - 0.01 ? el.h + growth : el.h;
3115
+ const lh = el.h === 0 ? 0 : el.y + el.h >= band.height - 0.01 ? Math.max(el.h, height - elY) : el.h;
3064
3116
  items.push({ kind: "line", x1: x, y1: y, x2: x + el.w, y2: y + lh, width: el.width ?? 0.2, color: el.color ?? "#000000", dash: el.dash, source: { band: ctx.kind, elementId: el.id } });
3065
3117
  break;
3066
3118
  }
@@ -3098,7 +3150,7 @@ function renderBand(band, scope, ctx, left, top, minHeight = 0) {
3098
3150
  break;
3099
3151
  }
3100
3152
  }
3101
- return { height, items };
3153
+ return anchors ? { height, items, anchors } : { height, items };
3102
3154
  }
3103
3155
 
3104
3156
  // src/layout.ts
@@ -3283,7 +3335,7 @@ function paginate(def, data, opts, knownTotal, docHash) {
3283
3335
  groupLevel: e.group?.level
3284
3336
  }
3285
3337
  });
3286
- const ctxFor = (kind, extra = {}) => ({ def, measure, locale, kind, width, previous, ...extra });
3338
+ const ctxFor = (kind, extra = {}) => ({ def, measure, locale, kind, width, previous, anchors: opts.anchors, ...extra });
3287
3339
  const newPage = () => {
3288
3340
  if (pages.length >= maxPages) throw new Error(`Report stopped at ${maxPages} pages (maxPages)`);
3289
3341
  page = { number: pages.length + 1, total: 0, width: geo.width, height: geo.height, items: [] };
@@ -3298,13 +3350,14 @@ function paginate(def, data, opts, knownTotal, docHash) {
3298
3350
  previous.clear();
3299
3351
  };
3300
3352
  const place = (band, kind, scope, extra = {}, pre) => {
3301
- const r = pre ? { height: pre.height, items: shiftItems(pre.items, y) } : renderBand(band, scope, ctxFor(kind, extra), ml, y);
3353
+ const r = pre ? { height: pre.height, items: shiftItems(pre.items, y), anchors: pre.anchors?.map((a) => ({ ...a, y: a.y + y })) } : renderBand(band, scope, ctxFor(kind, extra), ml, y);
3302
3354
  for (const it of r.items) page.items.push(it);
3355
+ if (r.anchors) (page.anchors ?? (page.anchors = [])).push(...r.anchors);
3303
3356
  y += r.height;
3304
3357
  hasBody = true;
3305
3358
  return r.height;
3306
3359
  };
3307
- const heightOf = (band, kind, scope, extra = {}) => renderBand(band, scope, ctxFor(kind, { ...extra, previous: void 0 }), 0, 0).height;
3360
+ const heightOf = (band, kind, scope, extra = {}) => renderBand(band, scope, ctxFor(kind, { ...extra, previous: void 0, anchors: false }), 0, 0).height;
3308
3361
  const renderChunk = (e, scope, extra) => {
3309
3362
  const band = e.band;
3310
3363
  const n2 = Math.max(1, Math.floor(band.columns ?? 1));
@@ -3317,7 +3370,7 @@ function paginate(def, data, opts, knownTotal, docHash) {
3317
3370
  ml + i * (colW + gap),
3318
3371
  0
3319
3372
  ));
3320
- return { height: Math.max(...parts.map((p) => p.height)), items: parts.flatMap((p) => p.items) };
3373
+ return { height: Math.max(...parts.map((p) => p.height)), items: parts.flatMap((p) => p.items), anchors: opts.anchors ? parts.flatMap((p) => p.anchors ?? []) : void 0 };
3321
3374
  };
3322
3375
  const isTableEntry = (e) => e.kind === "detail" || e.kind === "groupHeader" || e.kind === "groupFooter";
3323
3376
  const placeColumnHeader = (e) => {
@@ -3413,7 +3466,9 @@ function paginate(def, data, opts, knownTotal, docHash) {
3413
3466
  const draw = (band, kind, top) => {
3414
3467
  if (!band) return;
3415
3468
  if (band.visible && !truthy2(evaluate(band.visible, scope))) return;
3416
- extraItems.push(...renderBand(band, scope, ctxFor(kind, { previous: void 0 }), ml, top, 0).items);
3469
+ const r = renderBand(band, scope, ctxFor(kind, { previous: void 0 }), ml, top, 0);
3470
+ extraItems.push(...r.items);
3471
+ if (r.anchors) (p.anchors ?? (p.anchors = [])).push(...r.anchors);
3417
3472
  };
3418
3473
  if (!(def.pageHeaderSkipFirst && i === 0)) draw(b.pageHeader, "pageHeader", mt);
3419
3474
  draw(b.pageFooter, "pageFooter", geo.height - mb - footerH);
@@ -5550,8 +5605,10 @@ function createReport(definition, data = {}, opts = {}) {
5550
5605
  let rows = data;
5551
5606
  let params = { ...opts.params };
5552
5607
  let cached = null;
5608
+ let anchored = null;
5553
5609
  void checkLicense(opts.licenseKey);
5554
5610
  const layout = () => cached ?? (cached = layoutReport(def, rows, { ...opts, params }));
5611
+ const withAnchors = () => opts.anchors ? layout() : anchored ?? (anchored = layoutReport(def, rows, { ...opts, params, anchors: true }));
5555
5612
  const allowed = async (kind, fileName) => {
5556
5613
  if (!opts.onOutput) return true;
5557
5614
  const l = layout();
@@ -5565,12 +5622,16 @@ function createReport(definition, data = {}, opts = {}) {
5565
5622
  layout,
5566
5623
  async ready() {
5567
5624
  await ensureFonts(def);
5568
- cached = null;
5625
+ cached = anchored = null;
5569
5626
  return layout();
5570
5627
  },
5628
+ anchors(prefix = "") {
5629
+ const l = withAnchors();
5630
+ return l.pages.flatMap((p, i) => (p.anchors ?? []).filter((a) => a.id.startsWith(prefix)).map((a) => ({ ...a, page: i + 1, pageWidth: p.width, pageHeight: p.height })));
5631
+ },
5571
5632
  setData(d) {
5572
5633
  rows = d;
5573
- cached = null;
5634
+ cached = anchored = null;
5574
5635
  },
5575
5636
  fingerprint() {
5576
5637
  const l = layout();
@@ -5578,11 +5639,11 @@ function createReport(definition, data = {}, opts = {}) {
5578
5639
  },
5579
5640
  setParams(p) {
5580
5641
  params = { ...p };
5581
- cached = null;
5642
+ cached = anchored = null;
5582
5643
  },
5583
5644
  setDefinition(d) {
5584
5645
  def = d;
5585
- cached = null;
5646
+ cached = anchored = null;
5586
5647
  },
5587
5648
  async print(o = {}) {
5588
5649
  if (!await allowed("print")) return false;
@@ -5719,7 +5780,7 @@ var PAGE_GAP = 16;
5719
5780
  function paramInput(name, p, value, t) {
5720
5781
  const id = `fr-p-${name.replace(/[^\w-]/g, "_")}`;
5721
5782
  const label = `<label for="${id}">${escapeHtml(p.label ?? name)}${p.required ? ' <b aria-hidden="true">*</b>' : ""}</label>`;
5722
- const v = value == null ? "" : value instanceof Date ? value.toISOString().slice(0, 10) : String(value);
5783
+ const v = value == null ? "" : value instanceof Date ? formatDate(value, "yyyy-MM-dd") : String(value);
5723
5784
  const req = p.required ? " required" : "";
5724
5785
  if (p.type === "select" || p.options) {
5725
5786
  return `<div class="fr-param">${label}<select id="${id}" name="${escapeHtml(name)}"${req}>${(p.options ?? []).map((o) => `<option value="${escapeHtml(String(o.value))}"${String(o.value) === v ? " selected" : ""}>${escapeHtml(String(o.label ?? o.value))}</option>`).join("")}</select></div>`;
@@ -5784,9 +5845,10 @@ function createViewer(el, options) {
5784
5845
  const toggle = $(".fr-params-toggle");
5785
5846
  const menuBtn = $('[data-act="menu"]');
5786
5847
  const menuList = $(".fr-menu-list");
5848
+ const overlayText = $(".fr-overlay-text");
5787
5849
  const busy = (text) => {
5788
5850
  overlay.hidden = text === null;
5789
- $(".fr-overlay-text").textContent = text ?? "";
5851
+ overlayText.textContent = text ?? "";
5790
5852
  };
5791
5853
  const showParams = (open) => {
5792
5854
  if (!form || !toggle) return;
@@ -5838,6 +5900,7 @@ function createViewer(el, options) {
5838
5900
  const holder = document.createElement("div");
5839
5901
  holder.className = "fr-page";
5840
5902
  holder.dataset.index = String(i);
5903
+ holder.dataset.page = String(i + 1);
5841
5904
  holder.setAttribute("aria-label", `${t.page} ${i + 1}`);
5842
5905
  frag.appendChild(holder);
5843
5906
  });
@@ -5847,7 +5910,11 @@ function createViewer(el, options) {
5847
5910
  for (const e of entries) if (e.isIntersecting) drawPage(e.target);
5848
5911
  updateCurrent();
5849
5912
  }, { root: body, rootMargin: "600px 0px" }) : null;
5850
- pagesEl.querySelectorAll(".fr-page").forEach((p, i) => observer ? observer.observe(p) : i < 3 && drawPage(p));
5913
+ const holders = pagesEl.querySelectorAll(".fr-page");
5914
+ holders.forEach((p, i) => observer ? observer.observe(p) : i < 3 && drawPage(p));
5915
+ const pageH = result.height * MM_PX * scale + PAGE_GAP;
5916
+ const from = Math.min(holders.length - 1, Math.max(0, Math.floor(body.scrollTop / pageH)));
5917
+ for (let i = from; i < Math.min(holders.length, from + 1 + Math.ceil(body.clientHeight / pageH)); i++) drawPage(holders[i]);
5851
5918
  if (totalEl) totalEl.textContent = `/ ${result.pages.length}`;
5852
5919
  lastQuery = "";
5853
5920
  hits = [];
@@ -5859,6 +5926,27 @@ function createViewer(el, options) {
5859
5926
  const pageH = result.height * MM_PX * scale + PAGE_GAP;
5860
5927
  current = Math.min(result.pages.length - 1, Math.max(0, Math.round(body.scrollTop / pageH)));
5861
5928
  if (pageInput && document.activeElement !== pageInput) pageInput.value = String(current + 1);
5929
+ notifyPage();
5930
+ };
5931
+ let reported = -1;
5932
+ const notifyPage = () => {
5933
+ if (!result?.pages.length || current === reported) return;
5934
+ reported = current;
5935
+ try {
5936
+ options.onPageChange?.(current + 1, result.pages.length);
5937
+ } catch (e) {
5938
+ console.error(e);
5939
+ }
5940
+ };
5941
+ const fail = (e, during) => {
5942
+ const err = e instanceof Error ? e : new Error(String(e));
5943
+ console.error(err);
5944
+ try {
5945
+ options.onError?.(err, during);
5946
+ } catch (x) {
5947
+ console.error(x);
5948
+ }
5949
+ return err;
5862
5950
  };
5863
5951
  const goTo = (n2) => {
5864
5952
  if (!result) return;
@@ -5867,6 +5955,7 @@ function createViewer(el, options) {
5867
5955
  if (holder) body.scrollTop = holder.offsetTop - pagesEl.offsetTop;
5868
5956
  current = i;
5869
5957
  if (pageInput) pageInput.value = String(i + 1);
5958
+ notifyPage();
5870
5959
  };
5871
5960
  const setZoom = (z) => {
5872
5961
  const ratio = result ? body.scrollTop / Math.max(1, body.scrollHeight) : 0;
@@ -5875,34 +5964,54 @@ function createViewer(el, options) {
5875
5964
  body.scrollTop = ratio * body.scrollHeight;
5876
5965
  };
5877
5966
  let runSeq = 0;
5878
- const run = async () => {
5879
- const seq2 = ++runSeq;
5967
+ let runs = 0;
5968
+ let latest = Promise.resolve();
5969
+ const setState = (state) => {
5970
+ el.dataset.frState = state;
5971
+ };
5972
+ const runOnce = async (seq2) => {
5880
5973
  busy(t.loading);
5974
+ setState("busy");
5975
+ let during = "data";
5976
+ const stale = () => seq2 !== runSeq;
5881
5977
  try {
5882
5978
  if (options.fetchData) {
5883
5979
  const fetched = await options.fetchData(params);
5884
- if (seq2 !== runSeq) return;
5980
+ if (stale()) return await latest;
5885
5981
  report.setData(fetched);
5886
5982
  }
5983
+ during = "layout";
5887
5984
  report.setParams(params);
5888
5985
  await ensureFonts(report.definition);
5889
5986
  if (destroyed) return;
5987
+ if (stale()) return await latest;
5890
5988
  result = report.layout();
5989
+ reported = -1;
5891
5990
  build();
5991
+ el.dataset.frRun = String(++runs);
5992
+ setState("ready");
5993
+ try {
5994
+ options.onReady?.({ pages: result.pages.length, rows: result.rowCount, ms: result.ms, params: { ...params } });
5995
+ } catch (e) {
5996
+ console.error(e);
5997
+ }
5892
5998
  } catch (e) {
5893
- console.error(e);
5894
- pagesEl.innerHTML = `<p class="fr-empty">${t.error}: ${escapeHtml(String(e.message ?? e))}</p>`;
5999
+ if (stale()) return await latest;
6000
+ const err = fail(e, during);
6001
+ pagesEl.innerHTML = `<p class="fr-empty">${t.error}: ${escapeHtml(err.message)}</p>`;
6002
+ el.dataset.frRun = String(++runs);
6003
+ setState("error");
5895
6004
  } finally {
5896
- busy(null);
6005
+ if (!stale()) busy(null);
5897
6006
  }
5898
6007
  };
6008
+ const run = () => latest = runOnce(++runSeq);
5899
6009
  const exportAs = async (kind) => {
5900
6010
  busy(t.exporting);
5901
6011
  try {
5902
6012
  await report.download(kind, options.fileName);
5903
6013
  } catch (e) {
5904
- console.error(e);
5905
- alertText(`${t.error}: ${e.message}`);
6014
+ alertText(`${t.error}: ${fail(e, "output").message}`);
5906
6015
  } finally {
5907
6016
  busy(null);
5908
6017
  }
@@ -5944,7 +6053,7 @@ function createViewer(el, options) {
5944
6053
  setZoom(Math.round((scale - 0.1) * 10) / 10);
5945
6054
  break;
5946
6055
  case "print":
5947
- void report.print().catch((err) => alertText(`${t.error}: ${err.message}`));
6056
+ void report.print().catch((err) => alertText(`${t.error}: ${fail(err, "output").message}`));
5948
6057
  break;
5949
6058
  case "pdf":
5950
6059
  void exportAs("pdf");
@@ -6019,7 +6128,7 @@ function createViewer(el, options) {
6019
6128
  e.preventDefault();
6020
6129
  setZoom(Math.round(scale * (e.deltaY < 0 ? 1.1 : 1 / 1.1) * 100) / 100);
6021
6130
  }, { passive: false });
6022
- form?.addEventListener("submit", (e) => {
6131
+ form?.addEventListener("submit", async (e) => {
6023
6132
  e.preventDefault();
6024
6133
  const next = { ...params };
6025
6134
  form.querySelectorAll("input, select").forEach((inp) => {
@@ -6027,7 +6136,21 @@ function createViewer(el, options) {
6027
6136
  const v = inp.value;
6028
6137
  next[inp.name] = type === "number" ? v === "" ? null : Number(v) : type === "boolean" ? v === "true" : v;
6029
6138
  });
6030
- params = next;
6139
+ const before = el.dataset.frState;
6140
+ setState("busy");
6141
+ let answer;
6142
+ try {
6143
+ answer = await options.onParamsSubmit?.({ ...next });
6144
+ } catch (x) {
6145
+ fail(x, "data");
6146
+ setState(before ?? "ready");
6147
+ return;
6148
+ }
6149
+ if (answer === false) {
6150
+ setState(before ?? "ready");
6151
+ return;
6152
+ }
6153
+ params = answer && typeof answer === "object" ? { ...next, ...answer } : next;
6031
6154
  void run();
6032
6155
  });
6033
6156
  const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
@@ -6138,6 +6261,8 @@ function createViewer(el, options) {
6138
6261
  listeners.abort();
6139
6262
  el.innerHTML = "";
6140
6263
  el.classList.remove("fr-viewer");
6264
+ delete el.dataset.frState;
6265
+ delete el.dataset.frRun;
6141
6266
  }
6142
6267
  };
6143
6268
  }
@@ -6185,7 +6310,7 @@ function ensureIds(def) {
6185
6310
  for (const s of bandSlots(def)) for (const el of s.band.elements) if (el.id) all.add(el.id);
6186
6311
  const seen = /* @__PURE__ */ new Set();
6187
6312
  for (const s of bandSlots(def)) for (const el of s.band.elements) {
6188
- if (el.id && /^[\w-]{1,64}$/.test(String(el.id)) && !seen.has(el.id)) {
6313
+ if (el.id && /^[\w:.-]{1,64}$/.test(String(el.id)) && !seen.has(el.id)) {
6189
6314
  seen.add(el.id);
6190
6315
  continue;
6191
6316
  }