@campxdev/pdfme 1.2.1 → 1.3.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 (50) hide show
  1. package/dist/cjs/chunks/{index-dHRmLCnu.js → fontSizePxWidget-Drk8HKGH.js} +138 -9
  2. package/dist/cjs/chunks/fontSizeTransform-CQQ_O42f.js +37 -0
  3. package/dist/cjs/chunks/{helper-C2o2tpNj.js → helper-DGH62Z2s.js} +16959 -4
  4. package/dist/cjs/chunks/{index-qB7eb2BC.js → index-CoNR0xQU.js} +10 -2
  5. package/dist/cjs/chunks/{index-CXm3doOM.js → index-CsEKt088.js} +750 -535
  6. package/dist/cjs/chunks/{pluginRegistry-Ba3ANzzx.js → pluginRegistry-D2vr9MUy.js} +1 -1
  7. package/dist/cjs/common.js +11 -3
  8. package/dist/cjs/converter.js +1 -1
  9. package/dist/cjs/generator.js +3 -3
  10. package/dist/cjs/index.js +27 -16
  11. package/dist/cjs/print-designer-editor.js +3389 -3383
  12. package/dist/cjs/schemas.js +500 -38
  13. package/dist/cjs/ui.js +83339 -72691
  14. package/dist/esm/chunks/{index-pDt5vVVj.js → fontSizePxWidget-CbzQrSSM.js} +130 -5
  15. package/dist/esm/chunks/fontSizeTransform-CkTVJdRF.js +34 -0
  16. package/dist/esm/chunks/{helper-D7XF1bxK.js → helper-DSxGxZ0j.js} +16955 -5
  17. package/dist/esm/chunks/{index-D1FuD_XZ.js → index-DJkUkUo9.js} +4 -3
  18. package/dist/esm/chunks/{index-qTsnfbi7.js → index-D_-j4c4P.js} +744 -532
  19. package/dist/esm/chunks/{pluginRegistry-DEA2P0ud.js → pluginRegistry-Bgrz5qWG.js} +1 -1
  20. package/dist/esm/common.js +4 -3
  21. package/dist/esm/converter.js +1 -1
  22. package/dist/esm/generator.js +3 -3
  23. package/dist/esm/index.js +7 -6
  24. package/dist/esm/print-designer-editor.js +3374 -3374
  25. package/dist/esm/schemas.js +472 -13
  26. package/dist/esm/ui.js +83339 -72691
  27. package/dist/types/_vendors/common/constants.d.ts +1 -2
  28. package/dist/types/_vendors/common/fontSizeTransform.d.ts +5 -0
  29. package/dist/types/_vendors/common/googleFontUrls.d.ts +9 -0
  30. package/dist/types/_vendors/common/googleFonts.d.ts +7 -0
  31. package/dist/types/_vendors/common/helper.d.ts +2 -0
  32. package/dist/types/_vendors/common/index.d.ts +4 -2
  33. package/dist/types/_vendors/print-designer-editor/index.d.ts +2 -1
  34. package/dist/types/_vendors/print-designer-editor/types.d.ts +1 -1
  35. package/dist/types/_vendors/print-designer-editor/useDesigner.d.ts +1 -1
  36. package/dist/types/_vendors/schemas/index.d.ts +8 -2
  37. package/dist/types/_vendors/schemas/richText/helper.d.ts +3 -0
  38. package/dist/types/_vendors/schemas/richText/index.d.ts +4 -0
  39. package/dist/types/_vendors/schemas/richText/pdfRender.d.ts +3 -0
  40. package/dist/types/_vendors/schemas/richText/propPanel.d.ts +3 -0
  41. package/dist/types/_vendors/schemas/richText/types.d.ts +7 -0
  42. package/dist/types/_vendors/schemas/richText/uiRender.d.ts +3 -0
  43. package/dist/types/_vendors/schemas/singleVariableText/index.d.ts +4 -0
  44. package/dist/types/_vendors/schemas/singleVariableText/propPanel.d.ts +3 -0
  45. package/dist/types/_vendors/schemas/singleVariableText/types.d.ts +4 -0
  46. package/dist/types/_vendors/schemas/text/fontCache.d.ts +10 -0
  47. package/dist/types/_vendors/schemas/text/fontSizePxWidget.d.ts +9 -0
  48. package/dist/types/_vendors/ui/components/CtlBar.d.ts +1 -1
  49. package/dist/types/_vendors/ui/components/Paper.d.ts +1 -0
  50. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var helper = require('./helper-C2o2tpNj.js');
3
+ var helper = require('./helper-DGH62Z2s.js');
4
4
  var fontkit = require('fontkit');
5
5
  var buffer = require('buffer');
6
6
  var colors = require('./colors-BeBcxfhb.js');
@@ -121,6 +121,36 @@ const LINE_END_FORBIDDEN_CHARS = [
121
121
  '«',
122
122
  ];
123
123
 
124
+ /**
125
+ * Global font binary cache for URL-based fonts.
126
+ * Fonts are fetched on demand and cached so subsequent uses are instant.
127
+ */
128
+ const _binaryCache = new Map();
129
+ const _inflight = new Map();
130
+ /**
131
+ * Fetch font binary data for a URL, using the global cache.
132
+ * Returns cached ArrayBuffer if available, otherwise fetches and caches.
133
+ * Deduplicates concurrent requests for the same URL.
134
+ */
135
+ const fetchFontBinary = async (url) => {
136
+ const cached = _binaryCache.get(url);
137
+ if (cached)
138
+ return cached;
139
+ // Deduplicate: if a fetch for this URL is already in flight, reuse it
140
+ const inflight = _inflight.get(url);
141
+ if (inflight)
142
+ return inflight;
143
+ const promise = fetch(url)
144
+ .then((res) => res.arrayBuffer())
145
+ .then((buf) => {
146
+ _binaryCache.set(url, buf);
147
+ _inflight.delete(url);
148
+ return buf;
149
+ });
150
+ _inflight.set(url, promise);
151
+ return promise;
152
+ };
153
+
124
154
  const getBrowserVerticalFontAdjustments = (fontKitFont, fontSize, lineHeight, verticalAlignment) => {
125
155
  const { ascent, descent, unitsPerEm } = fontKitFont;
126
156
  // Fonts have a designed line height that the browser renders when using `line-height: normal`
@@ -189,7 +219,7 @@ const getFontKitFont = async (fontName, font, _cache) => {
189
219
  if (typeof fontData === 'string') {
190
220
  const isUrl = fontData.startsWith('http') || fontData.startsWith('/') || fontData.startsWith('./');
191
221
  fontData = isUrl
192
- ? await fetch(fontData).then((res) => res.arrayBuffer())
222
+ ? await fetchFontBinary(fontData)
193
223
  : helper.b64toUint8Array(fontData);
194
224
  }
195
225
  // Convert fontData to Buffer if it's not already a Buffer
@@ -1615,26 +1645,34 @@ const createSvgStr = (icon, attrs) => {
1615
1645
  return `<svg ${svgAttrString}>${elementsString}</svg>`;
1616
1646
  };
1617
1647
 
1618
- const embedAndGetFontObj = async (arg) => {
1619
- const { pdfDoc, font, _cache } = arg;
1620
- if (_cache.has(pdfDoc)) {
1621
- return _cache.get(pdfDoc);
1622
- }
1623
- const fontValues = await Promise.all(Object.values(font).map(async (v) => {
1624
- let fontData = v.data;
1625
- if (typeof fontData === 'string') {
1626
- const isUrl = fontData.startsWith('http') || fontData.startsWith('/') || fontData.startsWith('./');
1627
- fontData = isUrl
1628
- ? await fetch(fontData).then((res) => res.arrayBuffer())
1629
- : helper.b64toUint8Array(fontData);
1630
- }
1631
- return pdfDoc.embedFont(fontData, {
1632
- subset: typeof v.subset === 'undefined' ? true : v.subset,
1633
- });
1634
- }));
1635
- const fontObj = Object.keys(font).reduce((acc, cur, i) => Object.assign(acc, { [cur]: fontValues[i] }), {});
1636
- _cache.set(pdfDoc, fontObj);
1637
- return fontObj;
1648
+ const embedPdfFont = async (pdfDoc, fontName, font, _cache) => {
1649
+ const cacheKey = `embedPdfFont-${fontName}`;
1650
+ if (_cache.has(cacheKey))
1651
+ return _cache.get(cacheKey);
1652
+ const fontEntry = font[fontName];
1653
+ if (!fontEntry)
1654
+ throw new Error(`Font "${fontName}" not found`);
1655
+ let fontData = fontEntry.data;
1656
+ if (typeof fontData === 'string') {
1657
+ const isUrl = fontData.startsWith('http') || fontData.startsWith('/') || fontData.startsWith('./');
1658
+ fontData = isUrl ? await fetchFontBinary(fontData) : helper.b64toUint8Array(fontData);
1659
+ }
1660
+ const useSubset = typeof fontEntry.subset === 'undefined' ? true : fontEntry.subset;
1661
+ let pdfFont;
1662
+ try {
1663
+ pdfFont = await pdfDoc.embedFont(fontData, { subset: useSubset });
1664
+ }
1665
+ catch (err) {
1666
+ // CFF/OpenType fonts fail subsetting (glyph._decode is not a function) — retry without subset
1667
+ if (useSubset) {
1668
+ pdfFont = await pdfDoc.embedFont(fontData, { subset: false });
1669
+ }
1670
+ else {
1671
+ throw err;
1672
+ }
1673
+ }
1674
+ _cache.set(cacheKey, pdfFont);
1675
+ return pdfFont;
1638
1676
  };
1639
1677
  const getFontProp = ({ value, fontKitFont, schema, colorType, }) => {
1640
1678
  const fontSize = schema.dynamicFontSize
@@ -1656,18 +1694,12 @@ const pdfRender$1 = async (arg) => {
1656
1694
  return;
1657
1695
  const { font = helper.getDefaultFont(), colorType } = options;
1658
1696
  const resolvedFontName = resolveFontName(schema.fontName, schema.bold, schema.italic, font);
1659
- const [pdfFontObj, fontKitFont] = await Promise.all([
1660
- embedAndGetFontObj({
1661
- pdfDoc,
1662
- font,
1663
- _cache: _cache,
1664
- }),
1697
+ const [pdfFontValue, fontKitFont] = await Promise.all([
1698
+ embedPdfFont(pdfDoc, resolvedFontName, font, _cache),
1665
1699
  getFontKitFont(resolvedFontName, font, _cache),
1666
1700
  ]);
1667
1701
  const fontProp = getFontProp({ value, fontKitFont, schema, colorType });
1668
1702
  const { fontSize, color, alignment, verticalAlignment, lineHeight, characterSpacing } = fontProp;
1669
- const fontName = resolvedFontName;
1670
- const pdfFontValue = pdfFontObj && pdfFontObj[fontName];
1671
1703
  const pageHeight = page.getHeight();
1672
1704
  const { width, height, rotate, position: { x, y }, opacity, } = convertForPdfLayoutProps({ schema, pageHeight, applyRotateTranslate: false });
1673
1705
  const pivotPoint = { x: x + width / 2, y: pageHeight - helper.mm2pt(schema.position.y) - height / 2 };
@@ -1778,76 +1810,8 @@ const pdfRender$1 = async (arg) => {
1778
1810
  });
1779
1811
  };
1780
1812
 
1781
- const substituteVariables = (text, variablesIn, extraContext) => {
1782
- if (!text) {
1783
- return '';
1784
- }
1785
- let variables;
1786
- try {
1787
- variables =
1788
- typeof variablesIn === 'string'
1789
- ? JSON.parse(variablesIn || '{}')
1790
- : variablesIn;
1791
- }
1792
- catch {
1793
- throw new SyntaxError(`[@campxdev/schemas] MVT: invalid JSON string '${variablesIn}'`);
1794
- }
1795
- // Merge extra context (e.g. currentPage, totalPages) with user variables
1796
- // System context takes precedence over user variables
1797
- const merged = extraContext ? { ...variables, ...extraContext } : variables;
1798
- // Use the full JS expression evaluator — supports {varName}, {expr * 2}, {str.toUpperCase()}, etc.
1799
- const result = expression.replacePlaceholders({ content: text, variables: merged, schemas: [] });
1800
- // Strip any remaining unresolved {placeholders} for clean output
1801
- return result.replace(/\{[^{}]+\}/g, '');
1802
- };
1803
- const validateVariables = (value, schema) => {
1804
- if (!schema.variables || schema.variables.length === 0) {
1805
- return true;
1806
- }
1807
- let values;
1808
- try {
1809
- values = value ? JSON.parse(value) : {};
1810
- }
1811
- catch {
1812
- throw new SyntaxError(`[@campxdev/generator] invalid JSON string '${value}' for variables in field ${schema.name}`);
1813
- }
1814
- for (const variable of schema.variables) {
1815
- if (!values[variable]) {
1816
- if (schema.required) {
1817
- throw new Error(`[@campxdev/generator] variable ${variable} is missing for field ${schema.name}`);
1818
- }
1819
- return false;
1820
- }
1821
- }
1822
- return true;
1823
- };
1824
-
1825
- const pdfRender = async (arg) => {
1826
- const { value, schema, pageContext, ...rest } = arg;
1827
- // Static mode: no template text → render value directly as plain text
1828
- if (!schema.text) {
1829
- await pdfRender$1({ value, schema, ...rest });
1830
- return;
1831
- }
1832
- // readOnly: value is already resolved by generate.ts via replacePlaceholders
1833
- if (schema.readOnly) {
1834
- await pdfRender$1({ value, schema, ...rest });
1835
- return;
1836
- }
1837
- // Dynamic mode (form): substitute variables in template
1838
- if (!validateVariables(value, schema)) {
1839
- return;
1840
- }
1841
- const renderArgs = {
1842
- value: substituteVariables(schema.text, value || '{}', pageContext),
1843
- schema,
1844
- ...rest,
1845
- };
1846
- await pdfRender$1(renderArgs);
1847
- };
1848
-
1849
- const TextBoldIcon = createSvgStr(lucide.Bold);
1850
- const TextItalicIcon = createSvgStr(lucide.Italic);
1813
+ createSvgStr(lucide.Bold);
1814
+ createSvgStr(lucide.Italic);
1851
1815
  const TextStrikethroughIcon = createSvgStr(lucide.Strikethrough);
1852
1816
  const TextUnderlineIcon = createSvgStr(lucide.Underline);
1853
1817
  const TextAlignLeftIcon = createSvgStr(lucide.AlignLeft);
@@ -1871,8 +1835,7 @@ exports.Formatter = void 0;
1871
1835
  })(exports.Formatter || (exports.Formatter = {}));
1872
1836
  function getExtraFormatterSchema(i18n) {
1873
1837
  const buttons = [
1874
- { key: exports.Formatter.BOLD, icon: TextBoldIcon, type: 'boolean' },
1875
- { key: exports.Formatter.ITALIC, icon: TextItalicIcon, type: 'boolean' },
1838
+ // TODO: re-enable bold/italic controls when ready
1876
1839
  { key: exports.Formatter.STRIKETHROUGH, icon: TextStrikethroughIcon, type: 'boolean' },
1877
1840
  { key: exports.Formatter.UNDERLINE, icon: TextUnderlineIcon, type: 'boolean' },
1878
1841
  { key: exports.Formatter.ALIGNMENT, icon: TextAlignLeftIcon, type: 'select', value: DEFAULT_ALIGNMENT },
@@ -1930,120 +1893,111 @@ const UseDynamicFontSize = (props) => {
1930
1893
  label.appendChild(span);
1931
1894
  rootElement.appendChild(label);
1932
1895
  };
1933
- const propPanel$1 = {
1896
+ const propPanel$2 = {
1934
1897
  schema: ({ options, activeSchema, i18n }) => {
1935
1898
  const font = options.font || { [helper.DEFAULT_FONT_NAME]: { data: '', fallback: true } };
1936
1899
  const fontNames = Object.keys(font);
1937
1900
  const fallbackFontName = helper.getFallbackFontName(font);
1938
1901
  const enableDynamicFont = Boolean(activeSchema?.dynamicFontSize);
1939
- const textSchema = {
1940
- fontName: {
1941
- title: i18n('schemas.text.fontName'),
1942
- type: 'string',
1943
- widget: 'select',
1944
- default: fallbackFontName,
1945
- placeholder: fallbackFontName,
1946
- props: { options: fontNames.map((name) => ({ label: name, value: name })) },
1947
- span: 12,
1948
- },
1949
- fontSize: {
1950
- title: i18n('schemas.text.size'),
1951
- type: 'number',
1952
- widget: 'inputNumber',
1953
- span: 6,
1954
- disabled: enableDynamicFont,
1955
- props: { min: 0 },
1956
- },
1957
- characterSpacing: {
1958
- title: i18n('schemas.text.spacing'),
1959
- type: 'number',
1960
- widget: 'inputNumber',
1961
- span: 6,
1962
- props: { min: 0 },
1963
- },
1964
- formatter: getExtraFormatterSchema(i18n),
1965
- lineHeight: {
1966
- title: i18n('schemas.text.lineHeight'),
1967
- type: 'number',
1968
- widget: 'inputNumber',
1969
- props: { step: 0.1, min: 0 },
1970
- span: 8,
1902
+ const result = {};
1903
+ result.fontName = {
1904
+ title: i18n('schemas.text.fontName'),
1905
+ type: 'string',
1906
+ widget: 'select',
1907
+ default: fallbackFontName,
1908
+ placeholder: fallbackFontName,
1909
+ props: {
1910
+ options: fontNames.map((name) => ({ label: name, value: name })),
1911
+ showSearch: true,
1912
+ virtual: true,
1913
+ filterOption: (input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
1914
+ popupMatchSelectWidth: false,
1915
+ listHeight: 300,
1916
+ style: { width: '100%' },
1971
1917
  },
1972
- useDynamicFontSize: { type: 'boolean', widget: 'UseDynamicFontSize', bind: false, span: 16 },
1973
- dynamicFontSize: {
1974
- type: 'object',
1975
- widget: 'card',
1976
- column: 3,
1977
- properties: {
1978
- min: {
1979
- title: i18n('schemas.text.min'),
1980
- type: 'number',
1981
- widget: 'inputNumber',
1982
- hidden: !enableDynamicFont,
1983
- props: { min: 0 },
1984
- },
1985
- max: {
1986
- title: i18n('schemas.text.max'),
1987
- type: 'number',
1988
- widget: 'inputNumber',
1989
- hidden: !enableDynamicFont,
1990
- props: { min: 0 },
1991
- },
1992
- fit: {
1993
- title: i18n('schemas.text.fit'),
1994
- type: 'string',
1995
- widget: 'select',
1996
- hidden: !enableDynamicFont,
1997
- props: {
1998
- options: [
1999
- { label: i18n('schemas.horizontal'), value: DYNAMIC_FIT_HORIZONTAL },
2000
- { label: i18n('schemas.vertical'), value: DYNAMIC_FIT_VERTICAL },
2001
- ],
2002
- },
2003
- },
1918
+ span: 24,
1919
+ };
1920
+ result.fontSize = {
1921
+ title: i18n('schemas.text.size'),
1922
+ type: 'number',
1923
+ widget: 'inputNumber',
1924
+ span: 6,
1925
+ disabled: enableDynamicFont,
1926
+ props: { min: 0 },
1927
+ };
1928
+ result.characterSpacing = {
1929
+ title: i18n('schemas.text.spacing'),
1930
+ type: 'number',
1931
+ widget: 'inputNumber',
1932
+ span: 6,
1933
+ props: { min: 0 },
1934
+ };
1935
+ result.formatter = getExtraFormatterSchema(i18n);
1936
+ result.lineHeight = {
1937
+ title: i18n('schemas.text.lineHeight'),
1938
+ type: 'number',
1939
+ widget: 'inputNumber',
1940
+ props: { step: 0.1, min: 0 },
1941
+ span: 8,
1942
+ };
1943
+ result.useDynamicFontSize = { type: 'boolean', widget: 'UseDynamicFontSize', bind: false, span: 16 };
1944
+ result.dynamicFontSize = {
1945
+ type: 'object',
1946
+ widget: 'card',
1947
+ column: 3,
1948
+ properties: {
1949
+ min: {
1950
+ title: i18n('schemas.text.min'),
1951
+ type: 'number',
1952
+ widget: 'inputNumber',
1953
+ hidden: !enableDynamicFont,
1954
+ props: { min: 0 },
2004
1955
  },
2005
- },
2006
- fontColor: {
2007
- title: i18n('schemas.textColor'),
2008
- type: 'string',
2009
- widget: 'color',
2010
- props: {
2011
- disabledAlpha: true,
1956
+ max: {
1957
+ title: i18n('schemas.text.max'),
1958
+ type: 'number',
1959
+ widget: 'inputNumber',
1960
+ hidden: !enableDynamicFont,
1961
+ props: { min: 0 },
2012
1962
  },
2013
- rules: [
2014
- {
2015
- pattern: HEX_COLOR_PATTERN,
2016
- message: i18n('validation.hexColor'),
1963
+ fit: {
1964
+ title: i18n('schemas.text.fit'),
1965
+ type: 'string',
1966
+ widget: 'select',
1967
+ hidden: !enableDynamicFont,
1968
+ props: {
1969
+ options: [
1970
+ { label: i18n('schemas.horizontal'), value: DYNAMIC_FIT_HORIZONTAL },
1971
+ { label: i18n('schemas.vertical'), value: DYNAMIC_FIT_VERTICAL },
1972
+ ],
2017
1973
  },
2018
- ],
2019
- },
2020
- backgroundColor: {
2021
- title: i18n('schemas.bgColor'),
2022
- type: 'string',
2023
- widget: 'color',
2024
- props: {
2025
- disabledAlpha: true,
2026
1974
  },
2027
- rules: [
2028
- {
2029
- pattern: HEX_COLOR_PATTERN,
2030
- message: i18n('validation.hexColor'),
2031
- },
2032
- ],
2033
1975
  },
2034
1976
  };
2035
- return textSchema;
1977
+ result.fontColor = {
1978
+ title: i18n('schemas.textColor'),
1979
+ type: 'string',
1980
+ widget: 'color',
1981
+ props: { disabledAlpha: true },
1982
+ rules: [{ pattern: HEX_COLOR_PATTERN, message: i18n('validation.hexColor') }],
1983
+ };
1984
+ result.backgroundColor = {
1985
+ title: i18n('schemas.bgColor'),
1986
+ type: 'string',
1987
+ widget: 'color',
1988
+ props: { disabledAlpha: true },
1989
+ rules: [{ pattern: HEX_COLOR_PATTERN, message: i18n('validation.hexColor') }],
1990
+ };
1991
+ return result;
2036
1992
  },
2037
1993
  widgets: { UseDynamicFontSize },
2038
1994
  defaultSchema: {
2039
1995
  name: '',
2040
1996
  type: 'text',
2041
- content: 'Type Something...',
1997
+ content: '',
2042
1998
  position: { x: 0, y: 0 },
2043
1999
  width: 45,
2044
- height: 10,
2045
- // If the value of "rotate" is set to undefined or not set at all, rotation will be disabled in the UI.
2046
- // Check this document: https://pdfme.com//docs/custom-schemas#learning-how-to-create-from-pdfmeschemas-code
2000
+ height: 7,
2047
2001
  rotate: 0,
2048
2002
  alignment: DEFAULT_ALIGNMENT,
2049
2003
  verticalAlignment: DEFAULT_VERTICAL_ALIGNMENT,
@@ -2062,338 +2016,120 @@ const propPanel$1 = {
2062
2016
  },
2063
2017
  };
2064
2018
 
2065
- /**
2066
- * Factory function that creates an "Insert Variable" widget for a specific schema field.
2067
- * The widget renders a dropdown of available variables and appends {varName} to the field value.
2068
- *
2069
- * @param targetKey - The schema field key to insert variables into (e.g., 'text', 'content')
2070
- * @returns A PropPanelWidgetProps function that renders the variable picker
2071
- */
2072
- const createInsertVariableWidget = (targetKey) => {
2073
- return (props) => {
2074
- const { rootElement, changeSchemas, activeSchema, options } = props;
2075
- const variables = options.variables?.textVariables ?? [];
2076
- console.log('[insertVariableWidget] targetKey:', targetKey);
2077
- console.log('[insertVariableWidget] options:', options);
2078
- console.log('[insertVariableWidget] options.variables:', options.variables);
2079
- console.log('[insertVariableWidget] variables:', variables);
2080
- console.log('[insertVariableWidget] variables.length:', variables.length);
2081
- if (variables.length === 0) {
2082
- console.log('[insertVariableWidget] NO VARIABLES - returning early');
2083
- return;
2019
+ const _loadedGoogleFonts = new Set();
2020
+ const ensureGoogleFontLoaded = (fontName, fontData) => {
2021
+ if (!fontName || _loadedGoogleFonts.has(fontName) || !helper.isGoogleFont(fontName))
2022
+ return;
2023
+ if (!document?.fonts)
2024
+ return;
2025
+ _loadedGoogleFonts.add(fontName);
2026
+ const fontFace = new FontFace(fontName, typeof fontData === 'string' ? `url(${fontData})` : fontData, { display: 'swap' });
2027
+ fontFace.load().then(() => document.fonts.add(fontFace)).catch(() => { });
2028
+ };
2029
+ const replaceUnsupportedChars = (text, fontKitFont) => {
2030
+ const charSupportCache = {};
2031
+ const isCharSupported = (char) => {
2032
+ if (char in charSupportCache) {
2033
+ return charSupportCache[char];
2084
2034
  }
2085
- console.log('[insertVariableWidget] Creating widget UI...');
2086
- const container = document.createElement('div');
2087
- container.style.cssText =
2088
- 'display:flex; gap:6px; align-items:center; margin-bottom:10px; z-index:9999; position:relative;';
2089
- console.log('[insertVariableWidget] rootElement:', rootElement);
2090
- console.log('[insertVariableWidget] rootElement.parentElement:', rootElement.parentElement);
2091
- console.log('[insertVariableWidget] rootElement computed style:', window.getComputedStyle(rootElement));
2092
- const label = document.createElement('span');
2093
- label.textContent = 'Insert Variable:';
2094
- label.style.cssText = 'font-size:12px; color:#666; white-space:nowrap;';
2095
- const select = document.createElement('select');
2096
- select.style.cssText =
2097
- 'flex:1; height:30px; border:1px solid #E0E0E0; border-radius:4px; padding:0 8px; font-size:13px; background:#fff; cursor:pointer; z-index:9999;';
2098
- const blank = document.createElement('option');
2099
- blank.value = '';
2100
- blank.textContent = '— pick variable —';
2101
- select.appendChild(blank);
2102
- for (const v of variables) {
2103
- const opt = document.createElement('option');
2104
- opt.value = v.value;
2105
- opt.textContent = v.label;
2106
- select.appendChild(opt);
2107
- }
2108
- select.onchange = (e) => {
2109
- const varName = e.target.value;
2110
- if (!varName)
2111
- return;
2112
- const current = String(activeSchema[targetKey] ?? '');
2113
- changeSchemas([{ key: targetKey, value: current + `{${varName}}`, schemaId: activeSchema.id }]);
2114
- select.value = '';
2115
- };
2116
- container.appendChild(label);
2117
- container.appendChild(select);
2118
- rootElement.appendChild(container);
2119
- console.log('[insertVariableWidget] Successfully created and appended widget');
2035
+ const isSupported = fontKitFont.hasGlyphForCodePoint(char.codePointAt(0) || 0);
2036
+ charSupportCache[char] = isSupported;
2037
+ return isSupported;
2120
2038
  };
2039
+ const segments = text.split(/(\r\n|\n|\r)/);
2040
+ return segments
2041
+ .map((segment) => {
2042
+ if (/\r\n|\n|\r/.test(segment)) {
2043
+ return segment;
2044
+ }
2045
+ return segment
2046
+ .split('')
2047
+ .map((char) => {
2048
+ if (/\s/.test(char) || char.charCodeAt(0) < 32) {
2049
+ return char;
2050
+ }
2051
+ return isCharSupported(char) ? char : '〿';
2052
+ })
2053
+ .join('');
2054
+ })
2055
+ .join('');
2121
2056
  };
2122
-
2123
- const insertVariableWidget = createInsertVariableWidget('text');
2124
- const mapDynamicVariables = (props) => {
2125
- const { rootElement, changeSchemas, activeSchema } = props;
2126
- const mvtSchema = activeSchema;
2127
- const text = mvtSchema.text ?? '';
2128
- if (!text) {
2129
- rootElement.style.display = 'none';
2057
+ const uiRender$2 = async (arg) => {
2058
+ const { value, schema, mode, onChange, stopEditing, tabIndex, placeholder, options, _cache } = arg;
2059
+ const effectivePlaceholder = placeholder || 'Type Something...';
2060
+ const usePlaceholder = isEditable(mode, schema) && !value;
2061
+ const getText = (element) => {
2062
+ let text = element.innerText;
2063
+ if (text.endsWith('\n')) {
2064
+ // contenteditable adds additional newline char retrieved with innerText
2065
+ text = text.slice(0, -1);
2066
+ }
2067
+ return text;
2068
+ };
2069
+ const font = options?.font || helper.getDefaultFont();
2070
+ const resolvedFontName = resolveFontName(schema.fontName, schema.bold, schema.italic, font);
2071
+ // Lazily load Google Font for CSS rendering if needed
2072
+ if (resolvedFontName && font[resolvedFontName]) {
2073
+ ensureGoogleFontLoaded(resolvedFontName, font[resolvedFontName].data);
2074
+ }
2075
+ // Show a subtle loading state while the font binary is being fetched
2076
+ const isLoading = helper.isGoogleFont(resolvedFontName);
2077
+ if (isLoading) {
2078
+ arg.rootElement.style.opacity = '0.4';
2079
+ arg.rootElement.style.transition = 'opacity 0.15s ease-out';
2080
+ }
2081
+ const fontKitFont = await getFontKitFont(resolvedFontName, font, _cache);
2082
+ const textBlock = buildStyledTextContainer(arg, fontKitFont, usePlaceholder ? effectivePlaceholder : value);
2083
+ // Fade in once the font is ready
2084
+ if (isLoading) {
2085
+ // Force a reflow so the transition triggers from the dimmed state
2086
+ void arg.rootElement.offsetHeight;
2087
+ arg.rootElement.style.opacity = '1';
2088
+ }
2089
+ const processedText = replaceUnsupportedChars(value, fontKitFont);
2090
+ if (!isEditable(mode, schema)) {
2091
+ // Read-only mode
2092
+ textBlock.innerHTML = processedText
2093
+ .split('')
2094
+ .map((l, i) => `<span style="letter-spacing:${String(value).length === i + 1 ? 0 : 'inherit'};">${l}</span>`)
2095
+ .join('');
2130
2096
  return;
2131
2097
  }
2132
- const variables = JSON.parse(mvtSchema.content || '{}');
2133
- const variablesChanged = updateVariablesFromText(text, variables);
2134
- const varNames = Object.keys(variables);
2135
- if (variablesChanged) {
2136
- changeSchemas([
2137
- { key: 'content', value: JSON.stringify(variables), schemaId: activeSchema.id },
2138
- { key: 'variables', value: varNames, schemaId: activeSchema.id },
2139
- { key: 'readOnly', value: varNames.length === 0, schemaId: activeSchema.id },
2140
- ]);
2141
- }
2142
- // No UI needed — sample data is auto-generated from variable paths
2143
- rootElement.style.display = 'none';
2144
- };
2145
- const propPanel = {
2146
- schema: (propPanelProps) => {
2147
- if (typeof propPanel$1.schema !== 'function') {
2148
- throw new Error('Oops, is text schema no longer a function?');
2149
- }
2150
- // Safely call schema function with proper type handling
2151
- const parentSchema = typeof propPanel$1.schema === 'function' ? propPanel$1.schema(propPanelProps) : {};
2152
- return {
2153
- insertVariablePicker: {
2154
- type: 'void',
2155
- widget: 'insertVariableWidget',
2156
- bind: false,
2157
- span: 24,
2158
- },
2159
- '----': { type: 'void', widget: 'Divider' },
2160
- ...parentSchema,
2161
- dynamicVariables: {
2162
- type: 'object',
2163
- widget: 'mapDynamicVariables',
2164
- bind: false,
2165
- span: 0,
2166
- },
2167
- };
2168
- },
2169
- widgets: { ...(propPanel$1.widgets || {}), mapDynamicVariables, insertVariableWidget },
2170
- defaultSchema: {
2171
- ...propPanel$1.defaultSchema,
2172
- readOnly: false,
2173
- type: 'text',
2174
- text: 'Type Something...',
2175
- width: 50,
2176
- height: 15,
2177
- content: '{}',
2178
- variables: [],
2179
- },
2180
- };
2181
- /** Known JS globals/keywords that should NOT be treated as user-defined variables */
2182
- const RESERVED_NAMES$1 = new Set([
2183
- 'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
2184
- 'void', 'delete', 'new', 'this', 'NaN', 'Infinity',
2185
- 'Math', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'JSON',
2186
- 'isNaN', 'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent',
2187
- 'encodeURI', 'encodeURIComponent', 'date', 'dateTime',
2188
- 'currentPage', 'totalPages',
2189
- ]);
2190
- /**
2191
- * Extract full dot-notation paths from an expression string.
2192
- * E.g. "student.marks.sem1 > 80" → ["student.marks.sem1"]
2193
- * Handles method calls: "student.name.toUpperCase()" → ["student.name"]
2194
- * Skips string literals and reserved names.
2195
- */
2196
- const extractDotPaths = (expr) => {
2197
- // Replace string literals with spaces (preserving positions for nextChar lookup)
2198
- const cleaned = expr.replace(/'[^']*'|"[^"]*"|`[^`]*`/g, (m) => ' '.repeat(m.length));
2199
- const pathRegex = /[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g;
2200
- const paths = new Set();
2201
- let m;
2202
- while ((m = pathRegex.exec(cleaned)) !== null) {
2203
- let path = m[0];
2204
- // If followed by '(', the last segment is a method call — trim it
2205
- const nextChar = cleaned[m.index + path.length];
2206
- if (nextChar === '(') {
2207
- const lastDot = path.lastIndexOf('.');
2208
- if (lastDot !== -1) {
2209
- path = path.substring(0, lastDot);
2210
- }
2211
- else {
2212
- // Standalone function call like parseInt(...) — skip
2213
- continue;
2214
- }
2215
- }
2216
- const root = path.split('.')[0];
2217
- if (!RESERVED_NAMES$1.has(root))
2218
- paths.add(path);
2219
- }
2220
- return Array.from(paths);
2221
- };
2222
- /**
2223
- * Build a nested default object from dot-paths.
2224
- * E.g. ["student.name", "student.marks.sem1"] →
2225
- * { name: "NAME", marks: { sem1: "SEM1" } }
2226
- * Merges into an existing object, only adding missing leaves.
2227
- * Returns true if anything was added.
2228
- */
2229
- const buildNestedDefault = (obj, paths) => {
2230
- let added = false;
2231
- for (const path of paths) {
2232
- const parts = path.split('.');
2233
- if (parts.length <= 1)
2234
- continue; // no nested parts
2235
- let current = obj;
2236
- for (let i = 1; i < parts.length - 1; i++) {
2237
- if (!(parts[i] in current) || typeof current[parts[i]] !== 'object' || current[parts[i]] === null) {
2238
- current[parts[i]] = {};
2239
- added = true;
2240
- }
2241
- current = current[parts[i]];
2242
- }
2243
- const leaf = parts[parts.length - 1];
2244
- if (!(leaf in current)) {
2245
- current[leaf] = path.replace(/\./g, '_').toUpperCase();
2246
- added = true;
2247
- }
2248
- }
2249
- return added;
2250
- };
2251
- const updateVariablesFromText = (text, variables) => {
2252
- // Find all {...} blocks and extract dot-notation paths from each
2253
- const blockRegex = /\{([^{}]+)\}/g;
2254
- const allPaths = new Set();
2255
- let blockMatch;
2256
- while ((blockMatch = blockRegex.exec(text)) !== null) {
2257
- for (const path of extractDotPaths(blockMatch[1])) {
2258
- allPaths.add(path);
2259
- }
2260
- }
2261
- // Group paths by root identifier
2262
- const rootToPaths = new Map();
2263
- for (const path of allPaths) {
2264
- const root = path.split('.')[0];
2265
- if (!rootToPaths.has(root))
2266
- rootToPaths.set(root, []);
2267
- rootToPaths.get(root).push(path);
2268
- }
2269
- const allRoots = new Set(rootToPaths.keys());
2270
- let changed = false;
2271
- for (const [root, paths] of rootToPaths) {
2272
- const hasNested = paths.some((p) => p.includes('.'));
2273
- if (hasNested) {
2274
- // Parse existing value or start fresh
2275
- let obj = {};
2276
- if (root in variables) {
2277
- try {
2278
- const parsed = JSON.parse(variables[root]);
2279
- if (typeof parsed === 'object' && parsed !== null) {
2280
- obj = parsed;
2281
- }
2282
- }
2283
- catch {
2284
- /* not JSON, will rebuild */
2285
- }
2286
- }
2287
- const added = buildNestedDefault(obj, paths);
2288
- if (!(root in variables) || added) {
2289
- variables[root] = JSON.stringify(obj);
2290
- changed = true;
2291
- }
2292
- }
2293
- else {
2294
- if (!(root in variables)) {
2295
- variables[root] = root.toUpperCase();
2296
- changed = true;
2297
- }
2298
- }
2299
- }
2300
- // Remove variables whose root is no longer referenced
2301
- for (const varName of Object.keys(variables)) {
2302
- if (!allRoots.has(varName)) {
2303
- delete variables[varName];
2304
- changed = true;
2305
- }
2306
- }
2307
- return changed;
2308
- };
2309
-
2310
- const replaceUnsupportedChars = (text, fontKitFont) => {
2311
- const charSupportCache = {};
2312
- const isCharSupported = (char) => {
2313
- if (char in charSupportCache) {
2314
- return charSupportCache[char];
2315
- }
2316
- const isSupported = fontKitFont.hasGlyphForCodePoint(char.codePointAt(0) || 0);
2317
- charSupportCache[char] = isSupported;
2318
- return isSupported;
2319
- };
2320
- const segments = text.split(/(\r\n|\n|\r)/);
2321
- return segments
2322
- .map((segment) => {
2323
- if (/\r\n|\n|\r/.test(segment)) {
2324
- return segment;
2325
- }
2326
- return segment
2327
- .split('')
2328
- .map((char) => {
2329
- if (/\s/.test(char) || char.charCodeAt(0) < 32) {
2330
- return char;
2331
- }
2332
- return isCharSupported(char) ? char : '〿';
2333
- })
2334
- .join('');
2335
- })
2336
- .join('');
2337
- };
2338
- const uiRender$1 = async (arg) => {
2339
- const { value, schema, mode, onChange, stopEditing, tabIndex, placeholder, options, _cache } = arg;
2340
- const usePlaceholder = isEditable(mode, schema) && placeholder && !value;
2341
- const getText = (element) => {
2342
- let text = element.innerText;
2343
- if (text.endsWith('\n')) {
2344
- // contenteditable adds additional newline char retrieved with innerText
2345
- text = text.slice(0, -1);
2346
- }
2347
- return text;
2348
- };
2349
- const font = options?.font || helper.getDefaultFont();
2350
- const resolvedFontName = resolveFontName(schema.fontName, schema.bold, schema.italic, font);
2351
- const fontKitFont = await getFontKitFont(resolvedFontName, font, _cache);
2352
- const textBlock = buildStyledTextContainer(arg, fontKitFont, usePlaceholder ? placeholder : value);
2353
- const processedText = replaceUnsupportedChars(value, fontKitFont);
2354
- if (!isEditable(mode, schema)) {
2355
- // Read-only mode
2356
- textBlock.innerHTML = processedText
2357
- .split('')
2358
- .map((l, i) => `<span style="letter-spacing:${String(value).length === i + 1 ? 0 : 'inherit'};">${l}</span>`)
2359
- .join('');
2360
- return;
2361
- }
2362
- makeElementPlainTextContentEditable(textBlock);
2363
- textBlock.tabIndex = tabIndex || 0;
2364
- textBlock.innerText = mode === 'designer' ? value : processedText;
2365
- textBlock.addEventListener('blur', (e) => {
2366
- if (onChange)
2367
- onChange({ key: 'content', value: getText(e.target) });
2368
- if (stopEditing)
2369
- stopEditing();
2370
- });
2371
- if (schema.dynamicFontSize) {
2372
- let dynamicFontSize = undefined;
2373
- textBlock.addEventListener('keyup', () => {
2374
- setTimeout(() => {
2375
- // Use a regular function instead of an async one since we don't need await
2376
- (() => {
2377
- if (!textBlock.textContent)
2378
- return;
2379
- dynamicFontSize = calculateDynamicFontSize({
2380
- textSchema: schema,
2381
- fontKitFont,
2382
- value: getText(textBlock),
2383
- startingFontSize: dynamicFontSize,
2384
- });
2385
- textBlock.style.fontSize = `${dynamicFontSize}pt`;
2386
- const { topAdj: newTopAdj, bottomAdj: newBottomAdj } = getBrowserVerticalFontAdjustments(fontKitFont, dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE, schema.lineHeight ?? DEFAULT_LINE_HEIGHT, schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT);
2387
- textBlock.style.paddingTop = `${newTopAdj}px`;
2388
- textBlock.style.marginBottom = `${newBottomAdj}px`;
2389
- })();
2390
- }, 0);
2391
- });
2098
+ makeElementPlainTextContentEditable(textBlock);
2099
+ textBlock.tabIndex = tabIndex || 0;
2100
+ textBlock.innerText = mode === 'designer' ? value : processedText;
2101
+ textBlock.addEventListener('blur', (e) => {
2102
+ if (onChange)
2103
+ onChange({ key: 'content', value: getText(e.target) });
2104
+ if (stopEditing)
2105
+ stopEditing();
2106
+ });
2107
+ if (schema.dynamicFontSize) {
2108
+ let dynamicFontSize = undefined;
2109
+ textBlock.addEventListener('keyup', () => {
2110
+ setTimeout(() => {
2111
+ // Use a regular function instead of an async one since we don't need await
2112
+ (() => {
2113
+ if (!textBlock.textContent)
2114
+ return;
2115
+ dynamicFontSize = calculateDynamicFontSize({
2116
+ textSchema: schema,
2117
+ fontKitFont,
2118
+ value: getText(textBlock),
2119
+ startingFontSize: dynamicFontSize,
2120
+ });
2121
+ textBlock.style.fontSize = `${dynamicFontSize}pt`;
2122
+ const { topAdj: newTopAdj, bottomAdj: newBottomAdj } = getBrowserVerticalFontAdjustments(fontKitFont, dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE, schema.lineHeight ?? DEFAULT_LINE_HEIGHT, schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT);
2123
+ textBlock.style.paddingTop = `${newTopAdj}px`;
2124
+ textBlock.style.marginBottom = `${newBottomAdj}px`;
2125
+ })();
2126
+ }, 0);
2127
+ });
2392
2128
  }
2393
2129
  if (usePlaceholder) {
2394
2130
  textBlock.style.color = PLACEHOLDER_FONT_COLOR;
2395
2131
  textBlock.addEventListener('focus', () => {
2396
- if (textBlock.innerText === placeholder) {
2132
+ if (textBlock.innerText === effectivePlaceholder) {
2397
2133
  textBlock.innerText = '';
2398
2134
  textBlock.style.color = schema.fontColor ?? DEFAULT_FONT_COLOR;
2399
2135
  }
@@ -2523,11 +2259,301 @@ const getBackgroundColor = (value, schema) => {
2523
2259
  return schema.backgroundColor;
2524
2260
  };
2525
2261
 
2526
- const uiRender = async (arg) => {
2262
+ const substituteVariables = (text, variablesIn, extraContext) => {
2263
+ if (!text) {
2264
+ return '';
2265
+ }
2266
+ let variables;
2267
+ try {
2268
+ variables =
2269
+ typeof variablesIn === 'string'
2270
+ ? JSON.parse(variablesIn || '{}')
2271
+ : variablesIn;
2272
+ }
2273
+ catch {
2274
+ throw new SyntaxError(`[@campxdev/schemas] richText: invalid JSON string '${variablesIn}'`);
2275
+ }
2276
+ // Merge extra context (e.g. currentPage, totalPages) with user variables
2277
+ // System context takes precedence over user variables
2278
+ const merged = extraContext ? { ...variables, ...extraContext } : variables;
2279
+ // Use the full JS expression evaluator — supports {varName}, {expr * 2}, {str.toUpperCase()}, etc.
2280
+ const result = expression.replacePlaceholders({ content: text, variables: merged, schemas: [] });
2281
+ // Strip any remaining unresolved {placeholders} for clean output
2282
+ return result.replace(/\{[^{}]+\}/g, '');
2283
+ };
2284
+ const validateVariables = (value, schema) => {
2285
+ if (!schema.variables || schema.variables.length === 0) {
2286
+ return true;
2287
+ }
2288
+ let values;
2289
+ try {
2290
+ values = value ? JSON.parse(value) : {};
2291
+ }
2292
+ catch {
2293
+ throw new SyntaxError(`[@campxdev/generator] invalid JSON string '${value}' for variables in field ${schema.name}`);
2294
+ }
2295
+ for (const variable of schema.variables) {
2296
+ if (!values[variable]) {
2297
+ if (schema.required) {
2298
+ throw new Error(`[@campxdev/generator] variable ${variable} is missing for field ${schema.name}`);
2299
+ }
2300
+ return false;
2301
+ }
2302
+ }
2303
+ return true;
2304
+ };
2305
+
2306
+ const pdfRender = async (arg) => {
2307
+ const { value, schema, pageContext, ...rest } = arg;
2308
+ // Static mode: no template text → render value directly as plain text
2309
+ if (!schema.text) {
2310
+ await pdfRender$1({ value, schema, ...rest });
2311
+ return;
2312
+ }
2313
+ // readOnly: value is already resolved by generate.ts via replacePlaceholders
2314
+ if (schema.readOnly) {
2315
+ await pdfRender$1({ value, schema, ...rest });
2316
+ return;
2317
+ }
2318
+ // Dynamic mode (form): substitute variables in template
2319
+ if (!validateVariables(value, schema)) {
2320
+ return;
2321
+ }
2322
+ const renderArgs = {
2323
+ value: substituteVariables(schema.text, value || '{}', pageContext),
2324
+ schema,
2325
+ ...rest,
2326
+ };
2327
+ await pdfRender$1(renderArgs);
2328
+ };
2329
+
2330
+ const variablePrefixSuffixWidget = (props) => {
2331
+ const { rootElement, changeSchemas, activeSchema, options } = props;
2332
+ const schema = activeSchema;
2333
+ const variables = options.variables?.textVariables ?? [];
2334
+ if (variables.length === 0) {
2335
+ const hint = document.createElement('div');
2336
+ hint.textContent = 'Type {variableName} directly in the text field';
2337
+ hint.style.cssText = 'font-size:11px;color:#aaa;margin-bottom:8px;font-style:italic;';
2338
+ rootElement.appendChild(hint);
2339
+ return;
2340
+ }
2341
+ const container = document.createElement('div');
2342
+ container.style.cssText = 'display:flex;flex-direction:column;gap:4px;margin-bottom:8px;';
2343
+ const title = document.createElement('div');
2344
+ title.textContent = 'Insert Variable:';
2345
+ title.style.cssText = 'font-size:12px;color:#666;margin-bottom:2px;';
2346
+ container.appendChild(title);
2347
+ for (const v of variables) {
2348
+ const row = document.createElement('div');
2349
+ row.style.cssText = 'display:flex;align-items:center;gap:6px;';
2350
+ const makeBtn = (side) => {
2351
+ const btn = document.createElement('button');
2352
+ btn.textContent = '+';
2353
+ btn.title = side === 'prefix' ? `Add ${v.label} as prefix` : `Add ${v.label} as suffix`;
2354
+ btn.style.cssText =
2355
+ 'width:22px;height:22px;cursor:pointer;border:1px solid #d9d9d9;border-radius:4px;' +
2356
+ 'background:#fff;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center;padding:0;';
2357
+ btn.onclick = () => {
2358
+ const current = String(schema.text ?? '');
2359
+ const next = side === 'prefix' ? `{${v.value}} ${current}` : `${current} {${v.value}}`;
2360
+ changeSchemas([{ key: 'text', value: next, schemaId: activeSchema.id }]);
2361
+ };
2362
+ return btn;
2363
+ };
2364
+ const label = document.createElement('span');
2365
+ label.textContent = v.label;
2366
+ label.style.cssText = 'font-size:12px;flex:1;color:#333;';
2367
+ row.appendChild(makeBtn('prefix'));
2368
+ row.appendChild(label);
2369
+ row.appendChild(makeBtn('suffix'));
2370
+ container.appendChild(row);
2371
+ }
2372
+ rootElement.appendChild(container);
2373
+ };
2374
+ const mapDynamicVariables$1 = (props) => {
2375
+ const { rootElement, changeSchemas, activeSchema } = props;
2376
+ const rtSchema = activeSchema;
2377
+ const text = rtSchema.text ?? '';
2378
+ if (!text) {
2379
+ rootElement.style.display = 'none';
2380
+ return;
2381
+ }
2382
+ const variables = JSON.parse((rtSchema.content && rtSchema.content !== '' ? rtSchema.content : '{}'));
2383
+ const variablesChanged = updateVariablesFromText(text, variables);
2384
+ const varNames = Object.keys(variables);
2385
+ if (variablesChanged) {
2386
+ changeSchemas([
2387
+ { key: 'content', value: JSON.stringify(variables), schemaId: activeSchema.id },
2388
+ { key: 'variables', value: varNames, schemaId: activeSchema.id },
2389
+ { key: 'readOnly', value: varNames.length === 0, schemaId: activeSchema.id },
2390
+ ]);
2391
+ }
2392
+ rootElement.style.display = 'none';
2393
+ };
2394
+ const propPanel$1 = {
2395
+ schema: (propPanelProps) => {
2396
+ if (typeof propPanel$2.schema !== 'function') {
2397
+ throw new Error('Oops, is text schema no longer a function?');
2398
+ }
2399
+ const parentSchema = typeof propPanel$2.schema === 'function' ? propPanel$2.schema(propPanelProps) : {};
2400
+ return {
2401
+ insertVariablePicker: {
2402
+ type: 'void',
2403
+ widget: 'variablePrefixSuffixWidget',
2404
+ bind: false,
2405
+ span: 24,
2406
+ },
2407
+ '----': { type: 'void', widget: 'Divider' },
2408
+ ...parentSchema,
2409
+ dynamicVariables: {
2410
+ type: 'object',
2411
+ widget: 'mapDynamicVariables',
2412
+ bind: false,
2413
+ span: 0,
2414
+ },
2415
+ };
2416
+ },
2417
+ widgets: { ...(propPanel$2.widgets || {}), mapDynamicVariables: mapDynamicVariables$1, variablePrefixSuffixWidget },
2418
+ defaultSchema: {
2419
+ ...propPanel$2.defaultSchema,
2420
+ readOnly: false,
2421
+ type: 'richText',
2422
+ text: '',
2423
+ width: 50,
2424
+ height: 7,
2425
+ content: '',
2426
+ variables: [],
2427
+ },
2428
+ };
2429
+ /** Known JS globals/keywords that should NOT be treated as user-defined variables */
2430
+ const RESERVED_NAMES$1 = new Set([
2431
+ 'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
2432
+ 'void', 'delete', 'new', 'this', 'NaN', 'Infinity',
2433
+ 'Math', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'JSON',
2434
+ 'isNaN', 'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent',
2435
+ 'encodeURI', 'encodeURIComponent', 'date', 'dateTime',
2436
+ 'currentPage', 'totalPages',
2437
+ ]);
2438
+ const extractDotPaths = (expr) => {
2439
+ const cleaned = expr.replace(/'[^']*'|"[^"]*"|`[^`]*`/g, (m) => ' '.repeat(m.length));
2440
+ const pathRegex = /[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g;
2441
+ const paths = new Set();
2442
+ let m;
2443
+ while ((m = pathRegex.exec(cleaned)) !== null) {
2444
+ let path = m[0];
2445
+ const nextChar = cleaned[m.index + path.length];
2446
+ if (nextChar === '(') {
2447
+ const lastDot = path.lastIndexOf('.');
2448
+ if (lastDot !== -1) {
2449
+ path = path.substring(0, lastDot);
2450
+ }
2451
+ else {
2452
+ continue;
2453
+ }
2454
+ }
2455
+ const root = path.split('.')[0];
2456
+ if (!RESERVED_NAMES$1.has(root))
2457
+ paths.add(path);
2458
+ }
2459
+ return Array.from(paths);
2460
+ };
2461
+ const buildNestedDefault = (obj, paths) => {
2462
+ let added = false;
2463
+ for (const path of paths) {
2464
+ const parts = path.split('.');
2465
+ if (parts.length <= 1)
2466
+ continue;
2467
+ let current = obj;
2468
+ for (let i = 1; i < parts.length - 1; i++) {
2469
+ if (!(parts[i] in current) || typeof current[parts[i]] !== 'object' || current[parts[i]] === null) {
2470
+ current[parts[i]] = {};
2471
+ added = true;
2472
+ }
2473
+ current = current[parts[i]];
2474
+ }
2475
+ const leaf = parts[parts.length - 1];
2476
+ const isOldLeafFormat = leaf in current && (/^[A-Z][A-Z0-9_]+$/.test(String(current[leaf])) || /^\{\{.*\}\}$/.test(String(current[leaf])));
2477
+ if (!(leaf in current) || isOldLeafFormat) {
2478
+ current[leaf] = path.replace(/\./g, ' ');
2479
+ added = true;
2480
+ }
2481
+ }
2482
+ return added;
2483
+ };
2484
+ const updateVariablesFromText = (text, variables) => {
2485
+ const blockRegex = /\{([^{}]+)\}/g;
2486
+ const allPaths = new Set();
2487
+ let blockMatch;
2488
+ while ((blockMatch = blockRegex.exec(text)) !== null) {
2489
+ for (const path of extractDotPaths(blockMatch[1])) {
2490
+ allPaths.add(path);
2491
+ }
2492
+ }
2493
+ const rootToPaths = new Map();
2494
+ for (const path of allPaths) {
2495
+ const root = path.split('.')[0];
2496
+ if (!rootToPaths.has(root))
2497
+ rootToPaths.set(root, []);
2498
+ rootToPaths.get(root).push(path);
2499
+ }
2500
+ const allRoots = new Set(rootToPaths.keys());
2501
+ let changed = false;
2502
+ for (const [root, paths] of rootToPaths) {
2503
+ const hasNested = paths.some((p) => p.includes('.'));
2504
+ if (hasNested) {
2505
+ let obj = {};
2506
+ if (root in variables) {
2507
+ try {
2508
+ const parsed = JSON.parse(variables[root]);
2509
+ if (typeof parsed === 'object' && parsed !== null) {
2510
+ obj = parsed;
2511
+ }
2512
+ }
2513
+ catch {
2514
+ /* not JSON, will rebuild */
2515
+ }
2516
+ }
2517
+ const added = buildNestedDefault(obj, paths);
2518
+ if (!(root in variables) || added) {
2519
+ variables[root] = JSON.stringify(obj);
2520
+ changed = true;
2521
+ }
2522
+ }
2523
+ else {
2524
+ const existingVal = variables[root];
2525
+ const isStaleObject = typeof existingVal === 'string' && (() => {
2526
+ try {
2527
+ const p = JSON.parse(existingVal);
2528
+ return typeof p === 'object' && p !== null;
2529
+ }
2530
+ catch {
2531
+ return false;
2532
+ }
2533
+ })();
2534
+ const isOldFormat = typeof existingVal === 'string' && (/^[A-Z][A-Z0-9_]*$/.test(existingVal) || /^\{\{.*\}\}$/.test(existingVal));
2535
+ if (!(root in variables) || isStaleObject || isOldFormat) {
2536
+ variables[root] = root;
2537
+ changed = true;
2538
+ }
2539
+ }
2540
+ }
2541
+ for (const varName of Object.keys(variables)) {
2542
+ if (!allRoots.has(varName)) {
2543
+ delete variables[varName];
2544
+ changed = true;
2545
+ }
2546
+ }
2547
+ return changed;
2548
+ };
2549
+
2550
+ /** Format {expr} → {{ expr with dots as spaces }} for display */
2551
+ const formatTemplateDisplay$1 = (text) => text.replace(/\{([^{}]+)\}/g, (_, expr) => '{{ ' + expr.trim().replace(/\./g, ' ') + ' }}');
2552
+ const uiRender$1 = async (arg) => {
2527
2553
  const { value, schema, rootElement, mode, onChange, pageContext, ...rest } = arg;
2528
2554
  // Static mode: no template text → delegate to plain text behavior
2529
2555
  if (!schema.text) {
2530
- await uiRender$1(arg);
2556
+ await uiRender$2({ ...arg, value: value || '' });
2531
2557
  return;
2532
2558
  }
2533
2559
  // Dynamic mode: template with optional variables
@@ -2537,8 +2563,8 @@ const uiRender = async (arg) => {
2537
2563
  await formUiRender(arg);
2538
2564
  return;
2539
2565
  }
2540
- await uiRender$1({
2541
- value: isEditable(mode, schema) ? text : substituteVariables(text, value, pageContext),
2566
+ await uiRender$2({
2567
+ value: isEditable(mode, schema) ? text : formatTemplateDisplay$1(text),
2542
2568
  schema,
2543
2569
  mode: mode === 'form' ? 'viewer' : mode, // if no variables for form it's just a viewer
2544
2570
  rootElement,
@@ -2559,11 +2585,34 @@ const uiRender = async (arg) => {
2559
2585
  throw new Error('Text block not found. Ensure the text block has an id of "text-" + schema.id');
2560
2586
  }
2561
2587
  if (mode === 'designer') {
2562
- textBlock.addEventListener('keyup', (event) => {
2588
+ // Show formatted display initially: {value} → {{ value }}
2589
+ if (text) {
2590
+ textBlock.textContent = formatTemplateDisplay$1(text);
2591
+ }
2592
+ textBlock.addEventListener('focus', () => {
2593
+ // Switch to raw template for editing
2594
+ textBlock.textContent = text;
2595
+ const sel = window.getSelection();
2596
+ const range = document.createRange();
2597
+ range.selectNodeContents(textBlock);
2598
+ range.collapse(false);
2599
+ sel?.removeAllRanges();
2600
+ sel?.addRange(range);
2601
+ });
2602
+ textBlock.addEventListener('blur', () => {
2563
2603
  text = textBlock.textContent || '';
2604
+ if (onChange) {
2605
+ onChange({ key: 'text', value: text });
2606
+ }
2607
+ // Show formatted display again
2608
+ textBlock.textContent = formatTemplateDisplay$1(text);
2609
+ });
2610
+ textBlock.addEventListener('keyup', (event) => {
2611
+ const currentText = textBlock.textContent || '';
2564
2612
  if (keyPressShouldBeChecked(event)) {
2565
- const newNumVariables = countUniqueVariableNames(text);
2613
+ const newNumVariables = countUniqueVariableNames(currentText);
2566
2614
  if (numVariables !== newNumVariables) {
2615
+ text = currentText;
2567
2616
  if (onChange) {
2568
2617
  onChange({ key: 'text', value: text });
2569
2618
  }
@@ -2748,7 +2797,6 @@ const countUniqueVariableNames = (content) => {
2748
2797
  /**
2749
2798
  * An optimisation to try to minimise jank while typing.
2750
2799
  * Only check whether variables were modified based on certain key presses.
2751
- * Regex would otherwise be performed on every key press (which isn't terrible, but this code helps).
2752
2800
  */
2753
2801
  const keyPressShouldBeChecked = (event) => {
2754
2802
  if (event.key === 'ArrowUp' ||
@@ -2770,15 +2818,179 @@ const keyPressShouldBeChecked = (event) => {
2770
2818
  return true;
2771
2819
  };
2772
2820
 
2821
+ const schema$1 = {
2822
+ pdf: pdfRender,
2823
+ ui: uiRender$1,
2824
+ propPanel: propPanel$1,
2825
+ icon: createSvgStr(lucide.FileText),
2826
+ uninterruptedEditMode: true,
2827
+ };
2828
+
2829
+ /** Replaces any existing {var} in the template with {newVar}, or appends if none exists */
2830
+ const replaceOrAppendVariable = (currentText, varName) => {
2831
+ // Replace any existing {identifier} block with the new variable
2832
+ const replaced = currentText.replace(/\{[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*\}/g, `{${varName}}`);
2833
+ if (replaced !== currentText)
2834
+ return replaced;
2835
+ // No existing variable — append with a space separator
2836
+ return currentText ? `${currentText} {${varName}}` : `{${varName}}`;
2837
+ };
2838
+ const singleVariablePickerWidget = (props) => {
2839
+ const { rootElement, changeSchemas, activeSchema, options } = props;
2840
+ const schema = activeSchema;
2841
+ const variables = options.variables?.textVariables ?? [];
2842
+ if (variables.length === 0)
2843
+ return;
2844
+ const container = document.createElement('div');
2845
+ container.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:10px;';
2846
+ const label = document.createElement('span');
2847
+ label.textContent = 'Variable:';
2848
+ label.style.cssText = 'font-size:12px;color:#666;white-space:nowrap;';
2849
+ const select = document.createElement('select');
2850
+ select.style.cssText =
2851
+ 'flex:1;height:30px;border:1px solid #E0E0E0;border-radius:4px;padding:0 8px;font-size:13px;background:#fff;cursor:pointer;';
2852
+ const blank = document.createElement('option');
2853
+ blank.value = '';
2854
+ blank.textContent = '— pick variable —';
2855
+ select.appendChild(blank);
2856
+ for (const v of variables) {
2857
+ const opt = document.createElement('option');
2858
+ opt.value = v.value;
2859
+ opt.textContent = v.label;
2860
+ select.appendChild(opt);
2861
+ }
2862
+ select.onchange = (e) => {
2863
+ const varName = e.target.value;
2864
+ if (!varName)
2865
+ return;
2866
+ const current = String(schema.text ?? '');
2867
+ const next = replaceOrAppendVariable(current, varName);
2868
+ changeSchemas([{ key: 'text', value: next, schemaId: activeSchema.id }]);
2869
+ select.value = '';
2870
+ };
2871
+ container.appendChild(label);
2872
+ container.appendChild(select);
2873
+ rootElement.appendChild(container);
2874
+ };
2875
+ const mapDynamicVariables = (props) => {
2876
+ const { rootElement, changeSchemas, activeSchema } = props;
2877
+ const svtSchema = activeSchema;
2878
+ const text = svtSchema.text ?? '';
2879
+ if (!text) {
2880
+ rootElement.style.display = 'none';
2881
+ return;
2882
+ }
2883
+ // Extract all simple variable references from the template
2884
+ const varMatches = [...text.matchAll(/\{([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\}/g)].map(m => m[1]);
2885
+ // Keep only the first unique variable (enforce single-variable restriction)
2886
+ const uniqueVars = [...new Set(varMatches)];
2887
+ const variable = uniqueVars[0];
2888
+ const varNames = variable ? [variable] : [];
2889
+ const variables = {};
2890
+ if (variable) {
2891
+ const existing = JSON.parse(svtSchema.content && svtSchema.content !== '' ? svtSchema.content : '{}');
2892
+ const existingVal = existing[variable];
2893
+ const isOldFormat = typeof existingVal === 'string' && (/^[A-Z][A-Z0-9_]*$/.test(existingVal) || /^\{\{.*\}\}$/.test(existingVal));
2894
+ const isStaleObject = typeof existingVal === 'string' && (() => {
2895
+ try {
2896
+ const p = JSON.parse(existingVal);
2897
+ return typeof p === 'object' && p !== null;
2898
+ }
2899
+ catch {
2900
+ return false;
2901
+ }
2902
+ })();
2903
+ variables[variable] = (!existingVal || isOldFormat || isStaleObject) ? variable.replace(/\./g, ' ') : existingVal;
2904
+ }
2905
+ const content = varNames.length > 0 ? JSON.stringify(variables) : '';
2906
+ const changed = JSON.stringify(svtSchema.variables ?? []) !== JSON.stringify(varNames) ||
2907
+ (svtSchema.content ?? '') !== content;
2908
+ if (changed) {
2909
+ changeSchemas([
2910
+ { key: 'content', value: content, schemaId: activeSchema.id },
2911
+ { key: 'variables', value: varNames, schemaId: activeSchema.id },
2912
+ { key: 'readOnly', value: varNames.length === 0, schemaId: activeSchema.id },
2913
+ ]);
2914
+ }
2915
+ rootElement.style.display = 'none';
2916
+ };
2917
+ const propPanel = {
2918
+ schema: (propPanelProps) => {
2919
+ if (typeof propPanel$1.schema !== 'function') {
2920
+ throw new Error('richText propPanel schema is not a function');
2921
+ }
2922
+ const parentSchema = propPanel$1.schema(propPanelProps);
2923
+ // Strip out richText-specific structural keys; we'll re-add our own versions
2924
+ const { insertVariablePicker: _ip, '----': _div, dynamicVariables: _dv, ...textFields } = parentSchema;
2925
+ return {
2926
+ insertVariablePicker: {
2927
+ type: 'void',
2928
+ widget: 'singleVariablePickerWidget',
2929
+ bind: false,
2930
+ span: 24,
2931
+ },
2932
+ '----': { type: 'void', widget: 'Divider' },
2933
+ ...textFields,
2934
+ dynamicVariables: {
2935
+ type: 'object',
2936
+ widget: 'mapDynamicVariables',
2937
+ bind: false,
2938
+ span: 0,
2939
+ },
2940
+ };
2941
+ },
2942
+ widgets: {
2943
+ ...(propPanel$1.widgets || {}),
2944
+ singleVariablePickerWidget,
2945
+ mapDynamicVariables,
2946
+ },
2947
+ defaultSchema: {
2948
+ ...propPanel$1.defaultSchema,
2949
+ type: 'singleVariableText',
2950
+ text: '',
2951
+ content: '',
2952
+ height: 7,
2953
+ variables: [],
2954
+ },
2955
+ };
2956
+
2957
+ /** Format {expr} → {{ expr with dots as spaces }} */
2958
+ const formatTemplateDisplay = (text) => text.replace(/\{([^{}]+)\}/g, (_, expr) => '{{ ' + expr.trim().replace(/\./g, ' ') + ' }}');
2959
+ const uiRender = async (arg) => {
2960
+ const { mode, schema, options, _cache } = arg;
2961
+ if (mode === 'designer') {
2962
+ // ReadOnly preview — show {{ variable }} formatted display
2963
+ const text = schema.text ?? '';
2964
+ const displayText = text ? formatTemplateDisplay(text) : '';
2965
+ const font = options?.font || helper.getDefaultFont();
2966
+ const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
2967
+ const textBlock = buildStyledTextContainer(arg, fontKitFont, displayText);
2968
+ textBlock.textContent = displayText;
2969
+ textBlock.style.cursor = 'default';
2970
+ return;
2971
+ }
2972
+ // Viewer/form modes: delegate to richText render
2973
+ await uiRender$1(arg);
2974
+ };
2773
2975
  const schema = {
2774
2976
  pdf: pdfRender,
2775
2977
  ui: uiRender,
2776
2978
  propPanel,
2777
- icon: createSvgStr(lucide.Type),
2778
- uninterruptedEditMode: true,
2979
+ icon: createSvgStr(lucide.BetweenHorizontalStart),
2779
2980
  };
2780
2981
 
2781
- const builtInPlugins = { Text: schema };
2982
+ const textSchema = {
2983
+ pdf: pdfRender$1,
2984
+ ui: uiRender$2,
2985
+ propPanel: propPanel$2,
2986
+ icon: createSvgStr(lucide.TextCursorInput),
2987
+ };
2988
+
2989
+ const builtInPlugins = {
2990
+ Text: textSchema,
2991
+ 'Dynamic Text': schema,
2992
+ 'Rich Text': schema$1,
2993
+ };
2782
2994
 
2783
2995
  exports.DEFAULT_ALIGNMENT = DEFAULT_ALIGNMENT;
2784
2996
  exports.DEFAULT_CHARACTER_SPACING = DEFAULT_CHARACTER_SPACING;
@@ -2789,10 +3001,10 @@ exports.DEFAULT_OPACITY = DEFAULT_OPACITY;
2789
3001
  exports.HEX_COLOR_PATTERN = HEX_COLOR_PATTERN;
2790
3002
  exports.VERTICAL_ALIGN_MIDDLE = VERTICAL_ALIGN_MIDDLE;
2791
3003
  exports.addAlphaToHex = addAlphaToHex;
3004
+ exports.buildStyledTextContainer = buildStyledTextContainer;
2792
3005
  exports.builtInPlugins = builtInPlugins;
2793
3006
  exports.convertForPdfLayoutProps = convertForPdfLayoutProps;
2794
3007
  exports.createErrorElm = createErrorElm;
2795
- exports.createInsertVariableWidget = createInsertVariableWidget;
2796
3008
  exports.createSingleTable = createSingleTable;
2797
3009
  exports.createSvgStr = createSvgStr;
2798
3010
  exports.getBody = getBody;
@@ -2802,13 +3014,16 @@ exports.getColumnStylesPropPanelSchema = getColumnStylesPropPanelSchema;
2802
3014
  exports.getDefaultCellStyles = getDefaultCellStyles;
2803
3015
  exports.getDynamicHeightsForTable = getDynamicHeightsForTable;
2804
3016
  exports.getExtraFormatterSchema = getExtraFormatterSchema;
3017
+ exports.getFontKitFont = getFontKitFont;
2805
3018
  exports.hex2PrintingColor = hex2PrintingColor;
2806
3019
  exports.isEditable = isEditable;
3020
+ exports.makeElementPlainTextContentEditable = makeElementPlainTextContentEditable;
2807
3021
  exports.mapVerticalAlignToFlex = mapVerticalAlignToFlex;
2808
3022
  exports.pdfRender = pdfRender$1;
2809
- exports.propPanel = propPanel$1;
3023
+ exports.propPanel = propPanel$2;
2810
3024
  exports.readFile = readFile;
2811
3025
  exports.rotatePoint = rotatePoint;
2812
- exports.schema = schema;
2813
- exports.substituteVariables = substituteVariables;
2814
- exports.uiRender = uiRender$1;
3026
+ exports.schema = schema$1;
3027
+ exports.schema$1 = schema;
3028
+ exports.textSchema = textSchema;
3029
+ exports.uiRender = uiRender$2;