@json-to-office/jto 0.26.0 → 0.28.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.
Files changed (30) hide show
  1. package/dist/cli.js +134 -77
  2. package/dist/cli.js.map +1 -1
  3. package/dist/client/assets/{HomePage-CcmtpSuV.js → HomePage-BX2wX21e.js} +3 -3
  4. package/dist/client/assets/HomePage-BX2wX21e.js.map +1 -0
  5. package/dist/client/assets/{JsonEditorPage-RDa8GWDd.js → JsonEditorPage-DseCjMok.js} +3 -3
  6. package/dist/client/assets/{JsonEditorPage-RDa8GWDd.js.map → JsonEditorPage-DseCjMok.js.map} +1 -1
  7. package/dist/client/assets/{MonacoPluginProvider-DM63WAJx.js → MonacoPluginProvider-DvO7h20V.js} +3 -3
  8. package/dist/client/assets/{MonacoPluginProvider-DM63WAJx.js.map → MonacoPluginProvider-DvO7h20V.js.map} +1 -1
  9. package/dist/client/assets/{button-CyTe9rTl.js → button-DzgfrDEj.js} +2 -2
  10. package/dist/client/assets/{button-CyTe9rTl.js.map → button-DzgfrDEj.js.map} +1 -1
  11. package/dist/client/assets/{editor-DCtGYNz2.js → editor-BDi6QWgh.js} +2 -2
  12. package/dist/client/assets/{editor-DCtGYNz2.js.map → editor-BDi6QWgh.js.map} +1 -1
  13. package/dist/client/assets/{editor-monaco-json-CU9ciwPv.js → editor-monaco-json-CsQqBG2A.js} +2 -2
  14. package/dist/client/assets/{editor-monaco-json-CU9ciwPv.js.map → editor-monaco-json-CsQqBG2A.js.map} +1 -1
  15. package/dist/client/assets/{index-CHAIoQtG.js → index-Cxjik2Rg.js} +3 -3
  16. package/dist/client/assets/{index-CHAIoQtG.js.map → index-Cxjik2Rg.js.map} +1 -1
  17. package/dist/client/assets/preview-BOF6SWhK.js +3 -0
  18. package/dist/client/assets/preview-BOF6SWhK.js.map +1 -0
  19. package/dist/client/index.html +1 -1
  20. package/dist/client/templates/management-plan.pptx.json +18 -18
  21. package/dist/client/templates/minimalist-pitch-deck.pptx.json +19 -19
  22. package/dist/client/templates/modern-annual-report-2.docx.json +12 -12
  23. package/dist/client/templates/modern-annual-report-3.docx.json +14 -14
  24. package/dist/client/templates/standard-annual-report.docx.json +2 -2
  25. package/dist/render-server.js +62 -48
  26. package/dist/render-server.js.map +1 -1
  27. package/package.json +7 -8
  28. package/dist/client/assets/HomePage-CcmtpSuV.js.map +0 -1
  29. package/dist/client/assets/preview-oxnxMi4x.js +0 -3
  30. package/dist/client/assets/preview-oxnxMi4x.js.map +0 -1
package/dist/cli.js CHANGED
@@ -422,12 +422,15 @@ var init_generator = __esm({
422
422
  }
423
423
  }
424
424
  const bypassCache = options?.bypassCache === true || extraEntries.length > 0;
425
+ const baseDir = typeof options?.baseDir === "string" ? options.baseDir : void 0;
425
426
  const cacheKeyData = {
426
427
  config: config2,
427
428
  customThemes: customThemes && Object.keys(customThemes).length > 0 ? customThemes : null,
428
429
  fontMode: fontMode ?? null,
429
430
  fontSubstitution: fontSubstitution ?? null,
430
- fontStrict: callerStrict ?? null
431
+ fontStrict: callerStrict ?? null,
432
+ // Same definition, different source directory → different local assets.
433
+ baseDir: baseDir ?? null
431
434
  };
432
435
  const cacheKey = this.cacheService.generateCacheKey(cacheKeyData);
433
436
  const hasDynamicContent = this.cacheService.hasDynamicContent(config2);
@@ -464,13 +467,15 @@ var init_generator = __esm({
464
467
  const plugins = registry.getPlugins();
465
468
  const generator = await this.adapter.createGenerator(plugins, {
466
469
  customThemes,
467
- fonts: fontOpts
470
+ fonts: fontOpts,
471
+ baseDir
468
472
  });
469
473
  buffer = await generator.generateBuffer(config2);
470
474
  } else {
471
475
  buffer = await this.adapter.generateBuffer(config2, {
472
476
  customThemes,
473
- fonts: fontOpts
477
+ fonts: fontOpts,
478
+ baseDir
474
479
  });
475
480
  }
476
481
  this.cacheService.set(cacheKey, buffer, config2, {
@@ -1413,7 +1418,12 @@ var init_loose = __esm({
1413
1418
  {
1414
1419
  bypassCache: Type.Optional(Type.Boolean()),
1415
1420
  returnUrl: Type.Optional(Type.Boolean()),
1416
- fonts: Type.Optional(FontOptionsSchema)
1421
+ fonts: Type.Optional(FontOptionsSchema),
1422
+ // Name of the discovered document this definition came from; the
1423
+ // server maps it to that document's directory so relative asset
1424
+ // paths resolve against it (#142). Names, never paths — a client
1425
+ // cannot point the server at an arbitrary directory.
1426
+ sourceName: Type.Optional(Type.String())
1417
1427
  },
1418
1428
  { additionalProperties: true }
1419
1429
  )
@@ -1630,36 +1640,36 @@ function isPrivateHost(host) {
1630
1640
  const firstIpv6Group = Number.parseInt(normalized.split(":")[0] || "0", 16);
1631
1641
  return firstIpv6Group >= 64512 && firstIpv6Group <= 65023 || firstIpv6Group >= 65152 && firstIpv6Group <= 65215;
1632
1642
  }
1633
- function assertAllowedUrl(source, path6, allowedHosts) {
1643
+ function assertAllowedUrl(source, path7, allowedHosts) {
1634
1644
  if (source.startsWith("data:")) return;
1635
1645
  let url;
1636
1646
  try {
1637
1647
  url = new URL(source);
1638
1648
  } catch {
1639
1649
  throw new UnsafeOutboundSourceError(
1640
- path6,
1650
+ path7,
1641
1651
  "local and relative file paths are disabled for HTTP requests"
1642
1652
  );
1643
1653
  }
1644
1654
  if (url.protocol !== "https:") {
1645
- throw new UnsafeOutboundSourceError(path6, "only HTTPS URLs are allowed");
1655
+ throw new UnsafeOutboundSourceError(path7, "only HTTPS URLs are allowed");
1646
1656
  }
1647
1657
  if (url.username || url.password) {
1648
1658
  throw new UnsafeOutboundSourceError(
1649
- path6,
1659
+ path7,
1650
1660
  "URLs containing credentials are not allowed"
1651
1661
  );
1652
1662
  }
1653
1663
  const host = url.hostname.toLowerCase().replace(/\.$/, "");
1654
1664
  if (isPrivateHost(host)) {
1655
1665
  throw new UnsafeOutboundSourceError(
1656
- path6,
1666
+ path7,
1657
1667
  `private or local host "${host}" is not allowed`
1658
1668
  );
1659
1669
  }
1660
1670
  if (!allowedHosts.some((pattern) => hostMatches(host, pattern))) {
1661
1671
  throw new UnsafeOutboundSourceError(
1662
- path6,
1672
+ path7,
1663
1673
  `host "${host}" is not in OUTBOUND_HOST_ALLOWLIST`
1664
1674
  );
1665
1675
  }
@@ -1677,13 +1687,13 @@ function extractCssReferences(input) {
1677
1687
  function extractAbsoluteUrls(input) {
1678
1688
  return input.match(/(?:https?|file):\/\/[^\s'"<>)}\]]+/gi) ?? [];
1679
1689
  }
1680
- function assertSafeSvg(svg, path6, allowedHosts) {
1690
+ function assertSafeSvg(svg, path7, allowedHosts) {
1681
1691
  if (/<\s*script\b/i.test(svg) || /<\s*(?:foreignObject|iframe|object|embed)\b/i.test(svg) || /\bon[a-z]+\s*=/i.test(svg)) {
1682
- throw new UnsafeOutboundSourceError(path6, "active SVG content is disabled");
1692
+ throw new UnsafeOutboundSourceError(path7, "active SVG content is disabled");
1683
1693
  }
1684
1694
  if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svg)) {
1685
1695
  throw new UnsafeOutboundSourceError(
1686
- path6,
1696
+ path7,
1687
1697
  "SVG document types and entities are disabled"
1688
1698
  );
1689
1699
  }
@@ -1692,20 +1702,20 @@ function assertSafeSvg(svg, path6, allowedHosts) {
1692
1702
  while (match = refPattern.exec(svg)) {
1693
1703
  const ref = match[2].trim();
1694
1704
  if (!ref || ref.startsWith("#") || ref.startsWith("data:")) continue;
1695
- assertAllowedUrl(ref, path6, allowedHosts);
1705
+ assertAllowedUrl(ref, path7, allowedHosts);
1696
1706
  }
1697
1707
  for (const ref of extractCssReferences(svg)) {
1698
1708
  if (ref.startsWith("#") || ref.startsWith("data:")) continue;
1699
- assertAllowedUrl(ref, path6, allowedHosts);
1709
+ assertAllowedUrl(ref, path7, allowedHosts);
1700
1710
  }
1701
1711
  }
1702
- function assertSafeEncodedImage(source, path6, allowedHosts) {
1712
+ function assertSafeEncodedImage(source, path7, allowedHosts) {
1703
1713
  let candidate = source.trim();
1704
1714
  let declaredSvg = false;
1705
1715
  if (candidate.startsWith("data:")) {
1706
1716
  const comma = candidate.indexOf(",");
1707
1717
  if (comma < 0) {
1708
- throw new UnsafeOutboundSourceError(path6, "malformed image data URL");
1718
+ throw new UnsafeOutboundSourceError(path7, "malformed image data URL");
1709
1719
  }
1710
1720
  const metadata = candidate.slice(5, comma);
1711
1721
  declaredSvg = metadata.split(";")[0].toLowerCase() === "image/svg+xml";
@@ -1714,7 +1724,7 @@ function assertSafeEncodedImage(source, path6, allowedHosts) {
1714
1724
  try {
1715
1725
  candidate = /(?:^|;)base64(?:;|$)/i.test(metadata) ? Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
1716
1726
  } catch {
1717
- throw new UnsafeOutboundSourceError(path6, "malformed SVG data URL");
1727
+ throw new UnsafeOutboundSourceError(path7, "malformed SVG data URL");
1718
1728
  }
1719
1729
  } else if (!/<\s*svg\b/i.test(candidate)) {
1720
1730
  candidate = Buffer.from(candidate, "base64").toString("utf8");
@@ -1722,37 +1732,37 @@ function assertSafeEncodedImage(source, path6, allowedHosts) {
1722
1732
  const svgStart = candidate.search(/<\s*svg\b/i);
1723
1733
  if (svgStart < 0) {
1724
1734
  if (declaredSvg) {
1725
- throw new UnsafeOutboundSourceError(path6, "malformed SVG image data");
1735
+ throw new UnsafeOutboundSourceError(path7, "malformed SVG image data");
1726
1736
  }
1727
1737
  return;
1728
1738
  }
1729
- assertSafeSvg(candidate, path6, allowedHosts);
1739
+ assertSafeSvg(candidate, path7, allowedHosts);
1730
1740
  }
1731
- function assertSafeResources(resources, path6, allowedHosts) {
1741
+ function assertSafeResources(resources, path7, allowedHosts) {
1732
1742
  if (typeof resources.js === "string" && resources.js.trim()) {
1733
1743
  throw new UnsafeOutboundSourceError(
1734
- `${path6}.js`,
1744
+ `${path7}.js`,
1735
1745
  "remote renderer JavaScript resources are disabled"
1736
1746
  );
1737
1747
  }
1738
1748
  if (typeof resources.css === "string") {
1739
1749
  for (const ref of extractCssReferences(resources.css)) {
1740
1750
  if (ref.startsWith("data:")) continue;
1741
- assertAllowedUrl(ref, `${path6}.css`, allowedHosts);
1751
+ assertAllowedUrl(ref, `${path7}.css`, allowedHosts);
1742
1752
  }
1743
1753
  }
1744
1754
  if (Array.isArray(resources.files) && resources.files.length > 0) {
1745
1755
  throw new UnsafeOutboundSourceError(
1746
- `${path6}.files`,
1756
+ `${path7}.files`,
1747
1757
  "remote renderer JavaScript resources are disabled"
1748
1758
  );
1749
1759
  }
1750
1760
  }
1751
- function assertNoRemoteReferences(value, path6, allowedHosts, seen) {
1761
+ function assertNoRemoteReferences(value, path7, allowedHosts, seen) {
1752
1762
  if (typeof value === "string") {
1753
1763
  if (/\b(?:fetch|XMLHttpRequest|WebSocket|EventSource)\s*\(/i.test(value) || /\b(?:javascript|file):/i.test(value)) {
1754
1764
  throw new UnsafeOutboundSourceError(
1755
- path6,
1765
+ path7,
1756
1766
  "network-capable JavaScript and file URLs are disabled"
1757
1767
  );
1758
1768
  }
@@ -1761,11 +1771,11 @@ function assertNoRemoteReferences(value, path6, allowedHosts, seen) {
1761
1771
  ...extractCssReferences(value)
1762
1772
  ]) {
1763
1773
  if (url.startsWith("data:")) continue;
1764
- assertAllowedUrl(url, path6, allowedHosts);
1774
+ assertAllowedUrl(url, path7, allowedHosts);
1765
1775
  }
1766
1776
  if (/^\/\//.test(value.trim())) {
1767
1777
  throw new UnsafeOutboundSourceError(
1768
- path6,
1778
+ path7,
1769
1779
  "protocol-relative URLs are disabled"
1770
1780
  );
1771
1781
  }
@@ -1775,20 +1785,20 @@ function assertNoRemoteReferences(value, path6, allowedHosts, seen) {
1775
1785
  seen.add(value);
1776
1786
  if (Array.isArray(value)) {
1777
1787
  value.forEach(
1778
- (entry, index) => assertNoRemoteReferences(entry, `${path6}[${index}]`, allowedHosts, seen)
1788
+ (entry, index) => assertNoRemoteReferences(entry, `${path7}[${index}]`, allowedHosts, seen)
1779
1789
  );
1780
1790
  return;
1781
1791
  }
1782
1792
  for (const [key, child] of Object.entries(value)) {
1783
- assertNoRemoteReferences(child, `${path6}.${key}`, allowedHosts, seen);
1793
+ assertNoRemoteReferences(child, `${path7}.${key}`, allowedHosts, seen);
1784
1794
  }
1785
1795
  }
1786
- function visit(value, path6, containerKey, allowedHosts, seen) {
1796
+ function visit(value, path7, containerKey, allowedHosts, seen) {
1787
1797
  if (!value || typeof value !== "object" || seen.has(value)) return;
1788
1798
  seen.add(value);
1789
1799
  if (Array.isArray(value)) {
1790
1800
  value.forEach(
1791
- (entry, index) => visit(entry, `${path6}[${index}]`, containerKey, allowedHosts, seen)
1801
+ (entry, index) => visit(entry, `${path7}[${index}]`, containerKey, allowedHosts, seen)
1792
1802
  );
1793
1803
  return;
1794
1804
  }
@@ -1796,7 +1806,7 @@ function visit(value, path6, containerKey, allowedHosts, seen) {
1796
1806
  for (const key of Object.keys(record)) {
1797
1807
  if (DANGEROUS_KEYS.has(key)) {
1798
1808
  throw new UnsafeOutboundSourceError(
1799
- `${path6}.${key}`,
1809
+ `${path7}.${key}`,
1800
1810
  "prototype mutation keys are disabled"
1801
1811
  );
1802
1812
  }
@@ -1804,31 +1814,31 @@ function visit(value, path6, containerKey, allowedHosts, seen) {
1804
1814
  const kind = typeof record.kind === "string" ? record.kind : void 0;
1805
1815
  if (kind === "file" && typeof record.path === "string") {
1806
1816
  throw new UnsafeOutboundSourceError(
1807
- `${path6}.path`,
1817
+ `${path7}.path`,
1808
1818
  "local file sources are disabled for HTTP requests"
1809
1819
  );
1810
1820
  }
1811
1821
  if ((kind === "url" || kind === "variable") && typeof record.url === "string") {
1812
- assertAllowedUrl(record.url, `${path6}.url`, allowedHosts);
1822
+ assertAllowedUrl(record.url, `${path7}.url`, allowedHosts);
1813
1823
  }
1814
1824
  const componentName = typeof record.name === "string" ? record.name.toLowerCase() : void 0;
1815
1825
  const props = isRecord(record.props) ? record.props : void 0;
1816
1826
  if (componentName && props) {
1817
1827
  if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.path === "string") {
1818
- assertAllowedUrl(props.path, `${path6}.props.path`, allowedHosts);
1819
- assertSafeEncodedImage(props.path, `${path6}.props.path`, allowedHosts);
1828
+ assertAllowedUrl(props.path, `${path7}.props.path`, allowedHosts);
1829
+ assertSafeEncodedImage(props.path, `${path7}.props.path`, allowedHosts);
1820
1830
  }
1821
1831
  if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.base64 === "string") {
1822
1832
  assertSafeEncodedImage(
1823
1833
  props.base64,
1824
- `${path6}.props.base64`,
1834
+ `${path7}.props.base64`,
1825
1835
  allowedHosts
1826
1836
  );
1827
1837
  }
1828
1838
  if ((componentName === "highcharts" || componentName === "visual") && typeof props.serverUrl === "string") {
1829
1839
  assertAllowedUrl(
1830
1840
  props.serverUrl,
1831
- `${path6}.props.serverUrl`,
1841
+ `${path7}.props.serverUrl`,
1832
1842
  allowedHosts
1833
1843
  );
1834
1844
  }
@@ -1836,30 +1846,30 @@ function visit(value, path6, containerKey, allowedHosts, seen) {
1836
1846
  if (isRecord(props.resources)) {
1837
1847
  assertSafeResources(
1838
1848
  props.resources,
1839
- `${path6}.props.resources`,
1849
+ `${path7}.props.resources`,
1840
1850
  allowedHosts
1841
1851
  );
1842
1852
  }
1843
1853
  assertNoRemoteReferences(
1844
1854
  props.options,
1845
- `${path6}.props.options`,
1855
+ `${path7}.props.options`,
1846
1856
  allowedHosts,
1847
1857
  /* @__PURE__ */ new WeakSet()
1848
1858
  );
1849
1859
  }
1850
1860
  }
1851
1861
  if (containerKey === "image" && typeof record.path === "string") {
1852
- assertAllowedUrl(record.path, `${path6}.path`, allowedHosts);
1853
- assertSafeEncodedImage(record.path, `${path6}.path`, allowedHosts);
1862
+ assertAllowedUrl(record.path, `${path7}.path`, allowedHosts);
1863
+ assertSafeEncodedImage(record.path, `${path7}.path`, allowedHosts);
1854
1864
  }
1855
1865
  if (typeof record.svg === "string") {
1856
- assertSafeSvg(record.svg, `${path6}.svg`, allowedHosts);
1866
+ assertSafeSvg(record.svg, `${path7}.svg`, allowedHosts);
1857
1867
  }
1858
1868
  if (containerKey === "resources") {
1859
- assertSafeResources(record, path6, allowedHosts);
1869
+ assertSafeResources(record, path7, allowedHosts);
1860
1870
  }
1861
1871
  for (const [key, child] of Object.entries(record)) {
1862
- visit(child, `${path6}.${key}`, key, allowedHosts, seen);
1872
+ visit(child, `${path7}.${key}`, key, allowedHosts, seen);
1863
1873
  }
1864
1874
  }
1865
1875
  function assertSafeOutboundSources(value, policy, rootPath = "request") {
@@ -1880,9 +1890,9 @@ var init_outbound_source_policy = __esm({
1880
1890
  "use strict";
1881
1891
  init_esm_shims();
1882
1892
  UnsafeOutboundSourceError = class extends Error {
1883
- constructor(path6, reason) {
1884
- super(`Unsafe outbound source at ${path6}: ${reason}`);
1885
- this.path = path6;
1893
+ constructor(path7, reason) {
1894
+ super(`Unsafe outbound source at ${path7}: ${reason}`);
1895
+ this.path = path7;
1886
1896
  this.name = "UnsafeOutboundSourceError";
1887
1897
  }
1888
1898
  };
@@ -1892,6 +1902,7 @@ var init_outbound_source_policy = __esm({
1892
1902
  });
1893
1903
 
1894
1904
  // src/server/rasterize-route.ts
1905
+ import path6 from "path";
1895
1906
  import { Type as Type2 } from "@sinclair/typebox";
1896
1907
  import { bodyLimit } from "hono/body-limit";
1897
1908
  import { HTTPException as HTTPException3 } from "hono/http-exception";
@@ -1922,7 +1933,18 @@ function registerRasterizeRoute(router, options = {}) {
1922
1933
  jsonOnly,
1923
1934
  tbValidator(RasterizeRequestSchema),
1924
1935
  async (c) => {
1925
- const { presentation, dpi } = getValidated(c, "json");
1936
+ const { presentation, dpi, baseDir } = getValidated(c, "json");
1937
+ let safeBaseDir;
1938
+ if (baseDir !== void 0) {
1939
+ const resolved = path6.resolve(baseDir);
1940
+ const cwd = process.cwd();
1941
+ if (resolved !== cwd && !resolved.startsWith(cwd + path6.sep)) {
1942
+ throw new HTTPException3(400, {
1943
+ message: "baseDir must be inside the server working directory"
1944
+ });
1945
+ }
1946
+ safeBaseDir = resolved;
1947
+ }
1926
1948
  try {
1927
1949
  if (options.sourcePolicy) {
1928
1950
  assertSafeOutboundSources(
@@ -1933,7 +1955,8 @@ function registerRasterizeRoute(router, options = {}) {
1933
1955
  }
1934
1956
  const result = await getRasterizer()({
1935
1957
  presentation,
1936
- dpi: clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI)
1958
+ dpi: clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI),
1959
+ baseDir: safeBaseDir
1937
1960
  });
1938
1961
  return c.json(result);
1939
1962
  } catch (error) {
@@ -1968,7 +1991,8 @@ var init_rasterize_route = __esm({
1968
1991
  presentation: Type2.Object({}, { additionalProperties: true }),
1969
1992
  dpi: Type2.Optional(
1970
1993
  Type2.Number({ minimum: MIN_VISUAL_DPI, maximum: MAX_VISUAL_DPI })
1971
- )
1994
+ ),
1995
+ baseDir: Type2.Optional(Type2.String())
1972
1996
  },
1973
1997
  { additionalProperties: false }
1974
1998
  );
@@ -1988,12 +2012,16 @@ var init_rasterize_route = __esm({
1988
2012
  import { Hono as Hono2 } from "hono";
1989
2013
  import { HTTPException as HTTPException4 } from "hono/http-exception";
1990
2014
  import { bodyLimit as bodyLimit2 } from "hono/body-limit";
1991
- import { PluginRegistry as PluginRegistry2 } from "@json-to-office/jto-cli";
2015
+ import {
2016
+ PluginRegistry as PluginRegistry2,
2017
+ PluginDiscoveryService
2018
+ } from "@json-to-office/jto-cli";
2019
+ import { dirname } from "path";
1992
2020
  function createFormatRouter(adapter) {
1993
2021
  const router = new Hono2();
1994
- const assertRequestSources = (value, path6) => {
2022
+ const assertRequestSources = (value, path7) => {
1995
2023
  try {
1996
- assertSafeOutboundSources(value, config.outboundSources, path6);
2024
+ assertSafeOutboundSources(value, config.outboundSources, path7);
1997
2025
  } catch (error) {
1998
2026
  if (error instanceof UnsafeOutboundSourceError) {
1999
2027
  throw new HTTPException4(400, { message: error.message });
@@ -2001,6 +2029,26 @@ function createFormatRouter(adapter) {
2001
2029
  throw error;
2002
2030
  }
2003
2031
  };
2032
+ const resolveSourceBaseDir = async (options) => {
2033
+ const sourceName = options?.sourceName;
2034
+ if (typeof sourceName !== "string" || sourceName.length === 0) {
2035
+ return void 0;
2036
+ }
2037
+ try {
2038
+ const discovery = new PluginDiscoveryService({
2039
+ maxDepth: 10,
2040
+ includeNodeModules: false,
2041
+ verbose: false
2042
+ });
2043
+ const documents = await discovery.discoverDocuments(
2044
+ adapter.name
2045
+ );
2046
+ const match = documents.find((doc) => doc.name === sourceName);
2047
+ return match ? dirname(match.path) : void 0;
2048
+ } catch {
2049
+ return void 0;
2050
+ }
2051
+ };
2004
2052
  const contentTypeMw = async (c, next) => {
2005
2053
  const contentType = c.req.header("content-type");
2006
2054
  if (!contentType || !contentType.includes("application/json")) {
@@ -2036,12 +2084,16 @@ function createFormatRouter(adapter) {
2036
2084
  } else if (rawFonts) {
2037
2085
  sanitizedFonts = rawFonts;
2038
2086
  }
2087
+ const baseDir = await resolveSourceBaseDir(options);
2088
+ const clientOptions = { ...options };
2089
+ delete clientOptions.baseDir;
2039
2090
  const result = await generatorService.generate({
2040
2091
  jsonDefinition,
2041
2092
  customThemes,
2042
2093
  options: {
2043
- ...options,
2094
+ ...clientOptions,
2044
2095
  ...sanitizedFonts !== void 0 && { fonts: sanitizedFonts },
2096
+ ...baseDir !== void 0 && { baseDir },
2045
2097
  bypassCache
2046
2098
  }
2047
2099
  });
@@ -2298,14 +2350,19 @@ function createFormatRouter(adapter) {
2298
2350
  const libreOfficeService = getContainer().get(
2299
2351
  "libreOfficeConverterService"
2300
2352
  );
2301
- const { jsonDefinition, customThemes } = getValidated(c, "json");
2353
+ const { jsonDefinition, customThemes, options } = getValidated(c, "json");
2302
2354
  try {
2303
2355
  assertRequestSources(jsonDefinition, "jsonDefinition");
2304
2356
  assertRequestSources(customThemes, "customThemes");
2357
+ assertRequestSources(options, "options");
2358
+ const baseDir = await resolveSourceBaseDir(options);
2305
2359
  const generated = await generatorService.generate({
2306
2360
  jsonDefinition,
2307
2361
  customThemes,
2308
- options: { bypassCache: true }
2362
+ options: {
2363
+ bypassCache: true,
2364
+ ...baseDir !== void 0 && { baseDir }
2365
+ }
2309
2366
  });
2310
2367
  const pdfBuffer = await libreOfficeService.convertToPdf(
2311
2368
  generated.buffer,
@@ -2479,7 +2536,7 @@ var init_format = __esm({
2479
2536
  // src/server/routes/discovery.ts
2480
2537
  import { Hono as Hono3 } from "hono";
2481
2538
  import {
2482
- PluginDiscoveryService,
2539
+ PluginDiscoveryService as PluginDiscoveryService2,
2483
2540
  PluginRegistry as PluginRegistry3
2484
2541
  } from "@json-to-office/jto-cli";
2485
2542
  import { latestVersion } from "@json-to-office/shared";
@@ -2590,7 +2647,7 @@ var init_discovery = __esm({
2590
2647
  discoveryRouter.get("/all", async (c) => {
2591
2648
  try {
2592
2649
  const format = Container.getAdapter().name;
2593
- const discovery = new PluginDiscoveryService({
2650
+ const discovery = new PluginDiscoveryService2({
2594
2651
  maxDepth: 10,
2595
2652
  includeNodeModules: false,
2596
2653
  verbose: false
@@ -2620,7 +2677,7 @@ var init_discovery = __esm({
2620
2677
  const includeSchemas = c.req.query("schemas") === "true";
2621
2678
  const includeExamples = c.req.query("examples") === "true";
2622
2679
  const format = Container.getAdapter().name;
2623
- const discovery = new PluginDiscoveryService({
2680
+ const discovery = new PluginDiscoveryService2({
2624
2681
  maxDepth: 10,
2625
2682
  includeNodeModules: false,
2626
2683
  verbose: false
@@ -2644,7 +2701,7 @@ var init_discovery = __esm({
2644
2701
  discoveryRouter.get("/documents", async (c) => {
2645
2702
  try {
2646
2703
  const format = Container.getAdapter().name;
2647
- const discovery = new PluginDiscoveryService({
2704
+ const discovery = new PluginDiscoveryService2({
2648
2705
  maxDepth: 10,
2649
2706
  includeNodeModules: false,
2650
2707
  verbose: false
@@ -2659,7 +2716,7 @@ var init_discovery = __esm({
2659
2716
  discoveryRouter.get("/themes", async (c) => {
2660
2717
  try {
2661
2718
  const format = Container.getAdapter().name;
2662
- const discovery = new PluginDiscoveryService({
2719
+ const discovery = new PluginDiscoveryService2({
2663
2720
  maxDepth: 10,
2664
2721
  includeNodeModules: false,
2665
2722
  verbose: false
@@ -2675,7 +2732,7 @@ var init_discovery = __esm({
2675
2732
  try {
2676
2733
  const pluginName = c.req.param("name");
2677
2734
  const format = Container.getAdapter().name;
2678
- const discovery = new PluginDiscoveryService({
2735
+ const discovery = new PluginDiscoveryService2({
2679
2736
  maxDepth: 10,
2680
2737
  includeNodeModules: false,
2681
2738
  verbose: false
@@ -2711,7 +2768,7 @@ var init_discovery = __esm({
2711
2768
  try {
2712
2769
  const name = c.req.param("name");
2713
2770
  const format = Container.getAdapter().name;
2714
- const discovery = new PluginDiscoveryService({
2771
+ const discovery = new PluginDiscoveryService2({
2715
2772
  maxDepth: 10,
2716
2773
  includeNodeModules: false,
2717
2774
  verbose: false
@@ -2727,7 +2784,7 @@ var init_discovery = __esm({
2727
2784
  try {
2728
2785
  const name = c.req.param("name");
2729
2786
  const format = Container.getAdapter().name;
2730
- const discovery = new PluginDiscoveryService({
2787
+ const discovery = new PluginDiscoveryService2({
2731
2788
  maxDepth: 10,
2732
2789
  includeNodeModules: false,
2733
2790
  verbose: false
@@ -2850,7 +2907,7 @@ var init_ai_schema = __esm({
2850
2907
 
2851
2908
  // src/server/services/prompt-loader.ts
2852
2909
  import { readFileSync, existsSync } from "fs";
2853
- import { join, dirname } from "path";
2910
+ import { join, dirname as dirname2 } from "path";
2854
2911
  import { fileURLToPath as fileURLToPath2 } from "url";
2855
2912
  function resolvePromptsDir() {
2856
2913
  const candidates = [
@@ -2885,7 +2942,7 @@ var init_prompt_loader = __esm({
2885
2942
  "src/server/services/prompt-loader.ts"() {
2886
2943
  "use strict";
2887
2944
  init_esm_shims();
2888
- __dirname2 = dirname(fileURLToPath2(import.meta.url));
2945
+ __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2889
2946
  PROMPTS_DIR = resolvePromptsDir();
2890
2947
  cache = /* @__PURE__ */ new Map();
2891
2948
  }
@@ -3760,7 +3817,7 @@ import { Hono as Hono7 } from "hono";
3760
3817
  import { serve } from "@hono/node-server";
3761
3818
  import { logger as logger2 } from "hono/logger";
3762
3819
  import { compress } from "hono/compress";
3763
- import { resolve, dirname as dirname2 } from "path";
3820
+ import { resolve, dirname as dirname3 } from "path";
3764
3821
  import { fileURLToPath as fileURLToPath3 } from "url";
3765
3822
  var __filename2, __dirname3, UnifiedServer;
3766
3823
  var init_unified_server = __esm({
@@ -3770,7 +3827,7 @@ var init_unified_server = __esm({
3770
3827
  init_app();
3771
3828
  init_logger();
3772
3829
  __filename2 = fileURLToPath3(import.meta.url);
3773
- __dirname3 = dirname2(__filename2);
3830
+ __dirname3 = dirname3(__filename2);
3774
3831
  UnifiedServer = class {
3775
3832
  app;
3776
3833
  config;
@@ -3806,9 +3863,9 @@ var init_unified_server = __esm({
3806
3863
  return await apiApp.fetch(c.req.raw, c.env);
3807
3864
  });
3808
3865
  this.app.use("*", async (c, next) => {
3809
- const path6 = c.req.path;
3810
- if (path6.startsWith("/api")) {
3811
- if (/\.(ts|tsx|js|jsx|css|map)$/.test(path6)) {
3866
+ const path7 = c.req.path;
3867
+ if (path7.startsWith("/api")) {
3868
+ if (/\.(ts|tsx|js|jsx|css|map)$/.test(path7)) {
3812
3869
  return next();
3813
3870
  }
3814
3871
  const response = await apiApp.fetch(c.req.raw, c.env);
@@ -3911,8 +3968,8 @@ var init_unified_server = __esm({
3911
3968
  }
3912
3969
  if (this.viteServer) {
3913
3970
  this.app.use("*", async (c, next) => {
3914
- const path6 = c.req.path;
3915
- if ((path6.startsWith("/api") || path6 === "/health") && !/\.(ts|tsx|js|jsx|css|map)$/.test(path6)) {
3971
+ const path7 = c.req.path;
3972
+ if ((path7.startsWith("/api") || path7 === "/health") && !/\.(ts|tsx|js|jsx|css|map)$/.test(path7)) {
3916
3973
  return next();
3917
3974
  }
3918
3975
  const env = c.env;
@@ -3939,7 +3996,7 @@ var init_unified_server = __esm({
3939
3996
  "/assets/*",
3940
3997
  serveStatic({
3941
3998
  root: clientPath,
3942
- rewriteRequestPath: (path6) => path6.replace(/^\/assets/, "/assets")
3999
+ rewriteRequestPath: (path7) => path7.replace(/^\/assets/, "/assets")
3943
4000
  })
3944
4001
  );
3945
4002
  this.app.use("/*", async (c, next) => {
@@ -4124,7 +4181,7 @@ function createDevCommand(adapter) {
4124
4181
  }
4125
4182
 
4126
4183
  // src/cli.ts
4127
- var PACKAGE_VERSION = true ? "0.26.0" : "dev-mode";
4184
+ var PACKAGE_VERSION = true ? "0.28.0" : "dev-mode";
4128
4185
  var program = new Command2();
4129
4186
  program.name("jto").description("JSON to Office CLI - Generate .docx and .pptx from JSON").version(PACKAGE_VERSION);
4130
4187
  registerCoreCommands(program, {