@freereportjs/core 0.9.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.9.0";
191
- var releaseDate = "2026-10-16";
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 = [];
@@ -5897,35 +5964,48 @@ function createViewer(el, options) {
5897
5964
  body.scrollTop = ratio * body.scrollHeight;
5898
5965
  };
5899
5966
  let runSeq = 0;
5900
- const run = async () => {
5901
- 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) => {
5902
5973
  busy(t.loading);
5974
+ setState("busy");
5903
5975
  let during = "data";
5976
+ const stale = () => seq2 !== runSeq;
5904
5977
  try {
5905
5978
  if (options.fetchData) {
5906
5979
  const fetched = await options.fetchData(params);
5907
- if (seq2 !== runSeq) return;
5980
+ if (stale()) return await latest;
5908
5981
  report.setData(fetched);
5909
5982
  }
5910
5983
  during = "layout";
5911
5984
  report.setParams(params);
5912
5985
  await ensureFonts(report.definition);
5913
5986
  if (destroyed) return;
5987
+ if (stale()) return await latest;
5914
5988
  result = report.layout();
5915
5989
  reported = -1;
5916
5990
  build();
5991
+ el.dataset.frRun = String(++runs);
5992
+ setState("ready");
5917
5993
  try {
5918
5994
  options.onReady?.({ pages: result.pages.length, rows: result.rowCount, ms: result.ms, params: { ...params } });
5919
5995
  } catch (e) {
5920
5996
  console.error(e);
5921
5997
  }
5922
5998
  } catch (e) {
5999
+ if (stale()) return await latest;
5923
6000
  const err = fail(e, during);
5924
6001
  pagesEl.innerHTML = `<p class="fr-empty">${t.error}: ${escapeHtml(err.message)}</p>`;
6002
+ el.dataset.frRun = String(++runs);
6003
+ setState("error");
5925
6004
  } finally {
5926
- busy(null);
6005
+ if (!stale()) busy(null);
5927
6006
  }
5928
6007
  };
6008
+ const run = () => latest = runOnce(++runSeq);
5929
6009
  const exportAs = async (kind) => {
5930
6010
  busy(t.exporting);
5931
6011
  try {
@@ -6056,14 +6136,20 @@ function createViewer(el, options) {
6056
6136
  const v = inp.value;
6057
6137
  next[inp.name] = type === "number" ? v === "" ? null : Number(v) : type === "boolean" ? v === "true" : v;
6058
6138
  });
6139
+ const before = el.dataset.frState;
6140
+ setState("busy");
6059
6141
  let answer;
6060
6142
  try {
6061
6143
  answer = await options.onParamsSubmit?.({ ...next });
6062
6144
  } catch (x) {
6063
6145
  fail(x, "data");
6146
+ setState(before ?? "ready");
6147
+ return;
6148
+ }
6149
+ if (answer === false) {
6150
+ setState(before ?? "ready");
6064
6151
  return;
6065
6152
  }
6066
- if (answer === false) return;
6067
6153
  params = answer && typeof answer === "object" ? { ...next, ...answer } : next;
6068
6154
  void run();
6069
6155
  });
@@ -6175,6 +6261,8 @@ function createViewer(el, options) {
6175
6261
  listeners.abort();
6176
6262
  el.innerHTML = "";
6177
6263
  el.classList.remove("fr-viewer");
6264
+ delete el.dataset.frState;
6265
+ delete el.dataset.frRun;
6178
6266
  }
6179
6267
  };
6180
6268
  }
@@ -6222,7 +6310,7 @@ function ensureIds(def) {
6222
6310
  for (const s of bandSlots(def)) for (const el of s.band.elements) if (el.id) all.add(el.id);
6223
6311
  const seen = /* @__PURE__ */ new Set();
6224
6312
  for (const s of bandSlots(def)) for (const el of s.band.elements) {
6225
- 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)) {
6226
6314
  seen.add(el.id);
6227
6315
  continue;
6228
6316
  }