@campxdev/pdfme 1.2.2 → 1.3.1

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 (46) hide show
  1. package/dist/cjs/chunks/{index-C8qZMUOU.js → fontSizePxWidget-Dmj19RJR.js} +138 -9
  2. package/dist/cjs/chunks/fontSizeTransform-CQQ_O42f.js +37 -0
  3. package/dist/cjs/chunks/{helper-BfoMn47R.js → helper-DGH62Z2s.js} +4 -0
  4. package/dist/cjs/chunks/{index-CVqJfcgy.js → index-BGO0T6u7.js} +743 -597
  5. package/dist/cjs/chunks/{index-COKtXyPp.js → index-CoNR0xQU.js} +6 -2
  6. package/dist/cjs/chunks/{pluginRegistry-C8bMreez.js → pluginRegistry-D2vr9MUy.js} +1 -1
  7. package/dist/cjs/common.js +7 -3
  8. package/dist/cjs/converter.js +1 -1
  9. package/dist/cjs/generator.js +3 -3
  10. package/dist/cjs/index.js +23 -16
  11. package/dist/cjs/print-designer-editor.js +3320 -3296
  12. package/dist/cjs/schemas.js +500 -38
  13. package/dist/cjs/ui.js +2031 -1887
  14. package/dist/esm/chunks/{index-C4F7EwBG.js → fontSizePxWidget-BHHixwEk.js} +130 -5
  15. package/dist/esm/chunks/fontSizeTransform-CkTVJdRF.js +34 -0
  16. package/dist/esm/chunks/{helper-D5PPN6Bv.js → helper-DSxGxZ0j.js} +4 -1
  17. package/dist/esm/chunks/{index-CDhErAtE.js → index-DJkUkUo9.js} +4 -3
  18. package/dist/esm/chunks/{index-C7jr4GIK.js → index-iZeHwQ5z.js} +737 -594
  19. package/dist/esm/chunks/{pluginRegistry-B-XSNgmK.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 +3307 -3286
  25. package/dist/esm/schemas.js +472 -13
  26. package/dist/esm/ui.js +2031 -1887
  27. package/dist/types/_vendors/common/fontSizeTransform.d.ts +5 -0
  28. package/dist/types/_vendors/common/helper.d.ts +1 -0
  29. package/dist/types/_vendors/common/index.d.ts +3 -2
  30. package/dist/types/_vendors/print-designer-editor/index.d.ts +2 -1
  31. package/dist/types/_vendors/print-designer-editor/types.d.ts +2 -1
  32. package/dist/types/_vendors/print-designer-editor/useDesigner.d.ts +1 -1
  33. package/dist/types/_vendors/schemas/index.d.ts +8 -2
  34. package/dist/types/_vendors/schemas/richText/helper.d.ts +3 -0
  35. package/dist/types/_vendors/schemas/richText/index.d.ts +4 -0
  36. package/dist/types/_vendors/schemas/richText/pdfRender.d.ts +3 -0
  37. package/dist/types/_vendors/schemas/richText/propPanel.d.ts +3 -0
  38. package/dist/types/_vendors/schemas/richText/types.d.ts +7 -0
  39. package/dist/types/_vendors/schemas/richText/uiRender.d.ts +3 -0
  40. package/dist/types/_vendors/schemas/singleVariableText/index.d.ts +4 -0
  41. package/dist/types/_vendors/schemas/singleVariableText/propPanel.d.ts +3 -0
  42. package/dist/types/_vendors/schemas/singleVariableText/types.d.ts +4 -0
  43. package/dist/types/_vendors/schemas/text/fontSizePxWidget.d.ts +9 -0
  44. package/dist/types/_vendors/ui/components/CtlBar.d.ts +1 -1
  45. package/dist/types/_vendors/ui/components/Paper.d.ts +1 -0
  46. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
- import { r as getFallbackFontName, q as getDefaultFont, D as DEFAULT_FONT_NAME, c as b64toUint8Array, A as pt2px, z as pt2mm, y as mm2pt, v as isBlankPdf, l as cloneDeep, x as isHexValid, w as isGoogleFont } from './helper-D5PPN6Bv.js';
1
+ import { r as getFallbackFontName, q as getDefaultFont, D as DEFAULT_FONT_NAME, c as b64toUint8Array, A as pt2px, y as mm2pt, z as pt2mm, v as isBlankPdf, l as cloneDeep, x as isHexValid, w as isGoogleFont } from './helper-DSxGxZ0j.js';
2
2
  import * as fontkit from 'fontkit';
3
3
  import { Buffer } from 'buffer';
4
4
  import { d as degrees, c as cmyk, r as rgb, a as degreesToRadians } from './colors-jzbEzNi4.js';
5
5
  import { r as replacePlaceholders } from './expression-B-F1KCfk.js';
6
- import { Strikethrough, Underline, AlignLeft, AlignCenter, AlignRight, AlignJustify, ArrowUpToLine, ArrowDownToLine, Bold, Italic, Type } from 'lucide';
6
+ import { Strikethrough, Underline, AlignLeft, AlignCenter, AlignRight, AlignJustify, ArrowUpToLine, ArrowDownToLine, Bold, Italic, FileText, BetweenHorizontalStart, TextCursorInput } from 'lucide';
7
7
 
8
8
  const DEFAULT_FONT_SIZE = 13;
9
9
  const ALIGN_LEFT = 'left';
@@ -1788,74 +1788,6 @@ const pdfRender$1 = async (arg) => {
1788
1788
  });
1789
1789
  };
1790
1790
 
1791
- const substituteVariables = (text, variablesIn, extraContext) => {
1792
- if (!text) {
1793
- return '';
1794
- }
1795
- let variables;
1796
- try {
1797
- variables =
1798
- typeof variablesIn === 'string'
1799
- ? JSON.parse(variablesIn || '{}')
1800
- : variablesIn;
1801
- }
1802
- catch {
1803
- throw new SyntaxError(`[@campxdev/schemas] MVT: invalid JSON string '${variablesIn}'`);
1804
- }
1805
- // Merge extra context (e.g. currentPage, totalPages) with user variables
1806
- // System context takes precedence over user variables
1807
- const merged = extraContext ? { ...variables, ...extraContext } : variables;
1808
- // Use the full JS expression evaluator — supports {varName}, {expr * 2}, {str.toUpperCase()}, etc.
1809
- const result = replacePlaceholders({ content: text, variables: merged, schemas: [] });
1810
- // Strip any remaining unresolved {placeholders} for clean output
1811
- return result.replace(/\{[^{}]+\}/g, '');
1812
- };
1813
- const validateVariables = (value, schema) => {
1814
- if (!schema.variables || schema.variables.length === 0) {
1815
- return true;
1816
- }
1817
- let values;
1818
- try {
1819
- values = value ? JSON.parse(value) : {};
1820
- }
1821
- catch {
1822
- throw new SyntaxError(`[@campxdev/generator] invalid JSON string '${value}' for variables in field ${schema.name}`);
1823
- }
1824
- for (const variable of schema.variables) {
1825
- if (!values[variable]) {
1826
- if (schema.required) {
1827
- throw new Error(`[@campxdev/generator] variable ${variable} is missing for field ${schema.name}`);
1828
- }
1829
- return false;
1830
- }
1831
- }
1832
- return true;
1833
- };
1834
-
1835
- const pdfRender = async (arg) => {
1836
- const { value, schema, pageContext, ...rest } = arg;
1837
- // Static mode: no template text → render value directly as plain text
1838
- if (!schema.text) {
1839
- await pdfRender$1({ value, schema, ...rest });
1840
- return;
1841
- }
1842
- // readOnly: value is already resolved by generate.ts via replacePlaceholders
1843
- if (schema.readOnly) {
1844
- await pdfRender$1({ value, schema, ...rest });
1845
- return;
1846
- }
1847
- // Dynamic mode (form): substitute variables in template
1848
- if (!validateVariables(value, schema)) {
1849
- return;
1850
- }
1851
- const renderArgs = {
1852
- value: substituteVariables(schema.text, value || '{}', pageContext),
1853
- schema,
1854
- ...rest,
1855
- };
1856
- await pdfRender$1(renderArgs);
1857
- };
1858
-
1859
1791
  createSvgStr(Bold);
1860
1792
  createSvgStr(Italic);
1861
1793
  const TextStrikethroughIcon = createSvgStr(Strikethrough);
@@ -1939,128 +1871,111 @@ const UseDynamicFontSize = (props) => {
1939
1871
  label.appendChild(span);
1940
1872
  rootElement.appendChild(label);
1941
1873
  };
1942
- const propPanel$1 = {
1874
+ const propPanel$2 = {
1943
1875
  schema: ({ options, activeSchema, i18n }) => {
1944
1876
  const font = options.font || { [DEFAULT_FONT_NAME]: { data: '', fallback: true } };
1945
1877
  const fontNames = Object.keys(font);
1946
1878
  const fallbackFontName = getFallbackFontName(font);
1947
1879
  const enableDynamicFont = Boolean(activeSchema?.dynamicFontSize);
1948
- const textSchema = {
1949
- fontName: {
1950
- title: i18n('schemas.text.fontName'),
1951
- type: 'string',
1952
- widget: 'select',
1953
- default: fallbackFontName,
1954
- placeholder: fallbackFontName,
1955
- props: {
1956
- options: fontNames.map((name) => ({ label: name, value: name })),
1957
- showSearch: true,
1958
- virtual: true,
1959
- filterOption: (input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
1960
- popupMatchSelectWidth: false,
1961
- listHeight: 300,
1962
- style: { width: '100%' },
1963
- },
1964
- span: 24,
1965
- },
1966
- fontSize: {
1967
- title: i18n('schemas.text.size'),
1968
- type: 'number',
1969
- widget: 'inputNumber',
1970
- span: 6,
1971
- disabled: enableDynamicFont,
1972
- props: { min: 0 },
1973
- },
1974
- characterSpacing: {
1975
- title: i18n('schemas.text.spacing'),
1976
- type: 'number',
1977
- widget: 'inputNumber',
1978
- span: 6,
1979
- props: { min: 0 },
1980
- },
1981
- formatter: getExtraFormatterSchema(i18n),
1982
- lineHeight: {
1983
- title: i18n('schemas.text.lineHeight'),
1984
- type: 'number',
1985
- widget: 'inputNumber',
1986
- props: { step: 0.1, min: 0 },
1987
- span: 8,
1880
+ const result = {};
1881
+ result.fontName = {
1882
+ title: i18n('schemas.text.fontName'),
1883
+ type: 'string',
1884
+ widget: 'select',
1885
+ default: fallbackFontName,
1886
+ placeholder: fallbackFontName,
1887
+ props: {
1888
+ options: fontNames.map((name) => ({ label: name, value: name })),
1889
+ showSearch: true,
1890
+ virtual: true,
1891
+ filterOption: (input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
1892
+ popupMatchSelectWidth: false,
1893
+ listHeight: 300,
1894
+ style: { width: '100%' },
1988
1895
  },
1989
- useDynamicFontSize: { type: 'boolean', widget: 'UseDynamicFontSize', bind: false, span: 16 },
1990
- dynamicFontSize: {
1991
- type: 'object',
1992
- widget: 'card',
1993
- column: 3,
1994
- properties: {
1995
- min: {
1996
- title: i18n('schemas.text.min'),
1997
- type: 'number',
1998
- widget: 'inputNumber',
1999
- hidden: !enableDynamicFont,
2000
- props: { min: 0 },
2001
- },
2002
- max: {
2003
- title: i18n('schemas.text.max'),
2004
- type: 'number',
2005
- widget: 'inputNumber',
2006
- hidden: !enableDynamicFont,
2007
- props: { min: 0 },
2008
- },
2009
- fit: {
2010
- title: i18n('schemas.text.fit'),
2011
- type: 'string',
2012
- widget: 'select',
2013
- hidden: !enableDynamicFont,
2014
- props: {
2015
- options: [
2016
- { label: i18n('schemas.horizontal'), value: DYNAMIC_FIT_HORIZONTAL },
2017
- { label: i18n('schemas.vertical'), value: DYNAMIC_FIT_VERTICAL },
2018
- ],
2019
- },
2020
- },
1896
+ span: 24,
1897
+ };
1898
+ result.fontSize = {
1899
+ title: i18n('schemas.text.size'),
1900
+ type: 'number',
1901
+ widget: 'inputNumber',
1902
+ span: 6,
1903
+ disabled: enableDynamicFont,
1904
+ props: { min: 0 },
1905
+ };
1906
+ result.characterSpacing = {
1907
+ title: i18n('schemas.text.spacing'),
1908
+ type: 'number',
1909
+ widget: 'inputNumber',
1910
+ span: 6,
1911
+ props: { min: 0 },
1912
+ };
1913
+ result.formatter = getExtraFormatterSchema(i18n);
1914
+ result.lineHeight = {
1915
+ title: i18n('schemas.text.lineHeight'),
1916
+ type: 'number',
1917
+ widget: 'inputNumber',
1918
+ props: { step: 0.1, min: 0 },
1919
+ span: 8,
1920
+ };
1921
+ result.useDynamicFontSize = { type: 'boolean', widget: 'UseDynamicFontSize', bind: false, span: 16 };
1922
+ result.dynamicFontSize = {
1923
+ type: 'object',
1924
+ widget: 'card',
1925
+ column: 3,
1926
+ properties: {
1927
+ min: {
1928
+ title: i18n('schemas.text.min'),
1929
+ type: 'number',
1930
+ widget: 'inputNumber',
1931
+ hidden: !enableDynamicFont,
1932
+ props: { min: 0 },
2021
1933
  },
2022
- },
2023
- fontColor: {
2024
- title: i18n('schemas.textColor'),
2025
- type: 'string',
2026
- widget: 'color',
2027
- props: {
2028
- disabledAlpha: true,
1934
+ max: {
1935
+ title: i18n('schemas.text.max'),
1936
+ type: 'number',
1937
+ widget: 'inputNumber',
1938
+ hidden: !enableDynamicFont,
1939
+ props: { min: 0 },
2029
1940
  },
2030
- rules: [
2031
- {
2032
- pattern: HEX_COLOR_PATTERN,
2033
- message: i18n('validation.hexColor'),
1941
+ fit: {
1942
+ title: i18n('schemas.text.fit'),
1943
+ type: 'string',
1944
+ widget: 'select',
1945
+ hidden: !enableDynamicFont,
1946
+ props: {
1947
+ options: [
1948
+ { label: i18n('schemas.horizontal'), value: DYNAMIC_FIT_HORIZONTAL },
1949
+ { label: i18n('schemas.vertical'), value: DYNAMIC_FIT_VERTICAL },
1950
+ ],
2034
1951
  },
2035
- ],
2036
- },
2037
- backgroundColor: {
2038
- title: i18n('schemas.bgColor'),
2039
- type: 'string',
2040
- widget: 'color',
2041
- props: {
2042
- disabledAlpha: true,
2043
1952
  },
2044
- rules: [
2045
- {
2046
- pattern: HEX_COLOR_PATTERN,
2047
- message: i18n('validation.hexColor'),
2048
- },
2049
- ],
2050
1953
  },
2051
1954
  };
2052
- return textSchema;
1955
+ result.fontColor = {
1956
+ title: i18n('schemas.textColor'),
1957
+ type: 'string',
1958
+ widget: 'color',
1959
+ props: { disabledAlpha: true },
1960
+ rules: [{ pattern: HEX_COLOR_PATTERN, message: i18n('validation.hexColor') }],
1961
+ };
1962
+ result.backgroundColor = {
1963
+ title: i18n('schemas.bgColor'),
1964
+ type: 'string',
1965
+ widget: 'color',
1966
+ props: { disabledAlpha: true },
1967
+ rules: [{ pattern: HEX_COLOR_PATTERN, message: i18n('validation.hexColor') }],
1968
+ };
1969
+ return result;
2053
1970
  },
2054
1971
  widgets: { UseDynamicFontSize },
2055
1972
  defaultSchema: {
2056
1973
  name: '',
2057
1974
  type: 'text',
2058
- content: 'Type Something...',
1975
+ content: '',
2059
1976
  position: { x: 0, y: 0 },
2060
1977
  width: 45,
2061
- height: 10,
2062
- // If the value of "rotate" is set to undefined or not set at all, rotation will be disabled in the UI.
2063
- // Check this document: https://pdfme.com//docs/custom-schemas#learning-how-to-create-from-pdfmeschemas-code
1978
+ height: 7,
2064
1979
  rotate: 0,
2065
1980
  alignment: DEFAULT_ALIGNMENT,
2066
1981
  verticalAlignment: DEFAULT_VERTICAL_ALIGNMENT,
@@ -2079,305 +1994,61 @@ const propPanel$1 = {
2079
1994
  },
2080
1995
  };
2081
1996
 
2082
- /**
2083
- * Factory function that creates an "Insert Variable" widget for a specific schema field.
2084
- * The widget renders a dropdown of available variables and appends {varName} to the field value.
2085
- *
2086
- * @param targetKey - The schema field key to insert variables into (e.g., 'text', 'content')
2087
- * @returns A PropPanelWidgetProps function that renders the variable picker
2088
- */
2089
- const createInsertVariableWidget = (targetKey) => {
2090
- return (props) => {
2091
- const { rootElement, changeSchemas, activeSchema, options } = props;
2092
- const variables = options.variables?.textVariables ?? [];
2093
- console.log('[insertVariableWidget] targetKey:', targetKey);
2094
- console.log('[insertVariableWidget] options:', options);
2095
- console.log('[insertVariableWidget] options.variables:', options.variables);
2096
- console.log('[insertVariableWidget] variables:', variables);
2097
- console.log('[insertVariableWidget] variables.length:', variables.length);
2098
- if (variables.length === 0) {
2099
- console.log('[insertVariableWidget] NO VARIABLES - returning early');
2100
- return;
2101
- }
2102
- console.log('[insertVariableWidget] Creating widget UI...');
2103
- const container = document.createElement('div');
2104
- container.style.cssText =
2105
- 'display:flex; gap:6px; align-items:center; margin-bottom:10px; z-index:9999; position:relative;';
2106
- console.log('[insertVariableWidget] rootElement:', rootElement);
2107
- console.log('[insertVariableWidget] rootElement.parentElement:', rootElement.parentElement);
2108
- console.log('[insertVariableWidget] rootElement computed style:', window.getComputedStyle(rootElement));
2109
- const label = document.createElement('span');
2110
- label.textContent = 'Insert Variable:';
2111
- label.style.cssText = 'font-size:12px; color:#666; white-space:nowrap;';
2112
- const select = document.createElement('select');
2113
- select.style.cssText =
2114
- 'flex:1; height:30px; border:1px solid #E0E0E0; border-radius:4px; padding:0 8px; font-size:13px; background:#fff; cursor:pointer; z-index:9999;';
2115
- const blank = document.createElement('option');
2116
- blank.value = '';
2117
- blank.textContent = '— pick variable —';
2118
- select.appendChild(blank);
2119
- for (const v of variables) {
2120
- const opt = document.createElement('option');
2121
- opt.value = v.value;
2122
- opt.textContent = v.label;
2123
- select.appendChild(opt);
2124
- }
2125
- select.onchange = (e) => {
2126
- const varName = e.target.value;
2127
- if (!varName)
2128
- return;
2129
- const current = String(activeSchema[targetKey] ?? '');
2130
- changeSchemas([{ key: targetKey, value: current + `{${varName}}`, schemaId: activeSchema.id }]);
2131
- select.value = '';
2132
- };
2133
- container.appendChild(label);
2134
- container.appendChild(select);
2135
- rootElement.appendChild(container);
2136
- console.log('[insertVariableWidget] Successfully created and appended widget');
2137
- };
2138
- };
2139
-
2140
- const insertVariableWidget = createInsertVariableWidget('text');
2141
- const mapDynamicVariables = (props) => {
2142
- const { rootElement, changeSchemas, activeSchema } = props;
2143
- const mvtSchema = activeSchema;
2144
- const text = mvtSchema.text ?? '';
2145
- if (!text) {
2146
- rootElement.style.display = 'none';
1997
+ const _loadedGoogleFonts = new Set();
1998
+ const ensureGoogleFontLoaded = (fontName, fontData) => {
1999
+ if (!fontName || _loadedGoogleFonts.has(fontName) || !isGoogleFont(fontName))
2147
2000
  return;
2148
- }
2149
- const variables = JSON.parse(mvtSchema.content || '{}');
2150
- const variablesChanged = updateVariablesFromText(text, variables);
2151
- const varNames = Object.keys(variables);
2152
- if (variablesChanged) {
2153
- changeSchemas([
2154
- { key: 'content', value: JSON.stringify(variables), schemaId: activeSchema.id },
2155
- { key: 'variables', value: varNames, schemaId: activeSchema.id },
2156
- { key: 'readOnly', value: varNames.length === 0, schemaId: activeSchema.id },
2157
- ]);
2158
- }
2159
- // No UI needed — sample data is auto-generated from variable paths
2160
- rootElement.style.display = 'none';
2001
+ if (!document?.fonts)
2002
+ return;
2003
+ _loadedGoogleFonts.add(fontName);
2004
+ const fontFace = new FontFace(fontName, typeof fontData === 'string' ? `url(${fontData})` : fontData, { display: 'swap' });
2005
+ fontFace.load().then(() => document.fonts.add(fontFace)).catch(() => { });
2161
2006
  };
2162
- const propPanel = {
2163
- schema: (propPanelProps) => {
2164
- if (typeof propPanel$1.schema !== 'function') {
2165
- throw new Error('Oops, is text schema no longer a function?');
2007
+ const replaceUnsupportedChars = (text, fontKitFont) => {
2008
+ const charSupportCache = {};
2009
+ const isCharSupported = (char) => {
2010
+ if (char in charSupportCache) {
2011
+ return charSupportCache[char];
2166
2012
  }
2167
- // Safely call schema function with proper type handling
2168
- const parentSchema = typeof propPanel$1.schema === 'function' ? propPanel$1.schema(propPanelProps) : {};
2169
- return {
2170
- insertVariablePicker: {
2171
- type: 'void',
2172
- widget: 'insertVariableWidget',
2173
- bind: false,
2174
- span: 24,
2175
- },
2176
- '----': { type: 'void', widget: 'Divider' },
2177
- ...parentSchema,
2178
- dynamicVariables: {
2179
- type: 'object',
2180
- widget: 'mapDynamicVariables',
2181
- bind: false,
2182
- span: 0,
2183
- },
2184
- };
2185
- },
2186
- widgets: { ...(propPanel$1.widgets || {}), mapDynamicVariables, insertVariableWidget },
2187
- defaultSchema: {
2188
- ...propPanel$1.defaultSchema,
2189
- readOnly: false,
2190
- type: 'text',
2191
- text: 'Type Something...',
2192
- width: 50,
2193
- height: 15,
2194
- content: '{}',
2195
- variables: [],
2196
- },
2197
- };
2198
- /** Known JS globals/keywords that should NOT be treated as user-defined variables */
2199
- const RESERVED_NAMES$1 = new Set([
2200
- 'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
2201
- 'void', 'delete', 'new', 'this', 'NaN', 'Infinity',
2202
- 'Math', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'JSON',
2203
- 'isNaN', 'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent',
2204
- 'encodeURI', 'encodeURIComponent', 'date', 'dateTime',
2205
- 'currentPage', 'totalPages',
2206
- ]);
2207
- /**
2208
- * Extract full dot-notation paths from an expression string.
2209
- * E.g. "student.marks.sem1 > 80" → ["student.marks.sem1"]
2210
- * Handles method calls: "student.name.toUpperCase()" → ["student.name"]
2211
- * Skips string literals and reserved names.
2212
- */
2213
- const extractDotPaths = (expr) => {
2214
- // Replace string literals with spaces (preserving positions for nextChar lookup)
2215
- const cleaned = expr.replace(/'[^']*'|"[^"]*"|`[^`]*`/g, (m) => ' '.repeat(m.length));
2216
- const pathRegex = /[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g;
2217
- const paths = new Set();
2218
- let m;
2219
- while ((m = pathRegex.exec(cleaned)) !== null) {
2220
- let path = m[0];
2221
- // If followed by '(', the last segment is a method call — trim it
2222
- const nextChar = cleaned[m.index + path.length];
2223
- if (nextChar === '(') {
2224
- const lastDot = path.lastIndexOf('.');
2225
- if (lastDot !== -1) {
2226
- path = path.substring(0, lastDot);
2227
- }
2228
- else {
2229
- // Standalone function call like parseInt(...) — skip
2230
- continue;
2013
+ const isSupported = fontKitFont.hasGlyphForCodePoint(char.codePointAt(0) || 0);
2014
+ charSupportCache[char] = isSupported;
2015
+ return isSupported;
2016
+ };
2017
+ const segments = text.split(/(\r\n|\n|\r)/);
2018
+ return segments
2019
+ .map((segment) => {
2020
+ if (/\r\n|\n|\r/.test(segment)) {
2021
+ return segment;
2022
+ }
2023
+ return segment
2024
+ .split('')
2025
+ .map((char) => {
2026
+ if (/\s/.test(char) || char.charCodeAt(0) < 32) {
2027
+ return char;
2231
2028
  }
2029
+ return isCharSupported(char) ? char : '〿';
2030
+ })
2031
+ .join('');
2032
+ })
2033
+ .join('');
2034
+ };
2035
+ const uiRender$2 = async (arg) => {
2036
+ const { value, schema, mode, onChange, stopEditing, tabIndex, placeholder, options, _cache } = arg;
2037
+ const effectivePlaceholder = placeholder || 'Type Something...';
2038
+ const usePlaceholder = isEditable(mode, schema) && !value;
2039
+ const getText = (element) => {
2040
+ let text = element.innerText;
2041
+ if (text.endsWith('\n')) {
2042
+ // contenteditable adds additional newline char retrieved with innerText
2043
+ text = text.slice(0, -1);
2232
2044
  }
2233
- const root = path.split('.')[0];
2234
- if (!RESERVED_NAMES$1.has(root))
2235
- paths.add(path);
2236
- }
2237
- return Array.from(paths);
2238
- };
2239
- /**
2240
- * Build a nested default object from dot-paths.
2241
- * E.g. ["student.name", "student.marks.sem1"] →
2242
- * { name: "NAME", marks: { sem1: "SEM1" } }
2243
- * Merges into an existing object, only adding missing leaves.
2244
- * Returns true if anything was added.
2245
- */
2246
- const buildNestedDefault = (obj, paths) => {
2247
- let added = false;
2248
- for (const path of paths) {
2249
- const parts = path.split('.');
2250
- if (parts.length <= 1)
2251
- continue; // no nested parts
2252
- let current = obj;
2253
- for (let i = 1; i < parts.length - 1; i++) {
2254
- if (!(parts[i] in current) || typeof current[parts[i]] !== 'object' || current[parts[i]] === null) {
2255
- current[parts[i]] = {};
2256
- added = true;
2257
- }
2258
- current = current[parts[i]];
2259
- }
2260
- const leaf = parts[parts.length - 1];
2261
- if (!(leaf in current)) {
2262
- current[leaf] = path.replace(/\./g, '_').toUpperCase();
2263
- added = true;
2264
- }
2265
- }
2266
- return added;
2267
- };
2268
- const updateVariablesFromText = (text, variables) => {
2269
- // Find all {...} blocks and extract dot-notation paths from each
2270
- const blockRegex = /\{([^{}]+)\}/g;
2271
- const allPaths = new Set();
2272
- let blockMatch;
2273
- while ((blockMatch = blockRegex.exec(text)) !== null) {
2274
- for (const path of extractDotPaths(blockMatch[1])) {
2275
- allPaths.add(path);
2276
- }
2277
- }
2278
- // Group paths by root identifier
2279
- const rootToPaths = new Map();
2280
- for (const path of allPaths) {
2281
- const root = path.split('.')[0];
2282
- if (!rootToPaths.has(root))
2283
- rootToPaths.set(root, []);
2284
- rootToPaths.get(root).push(path);
2285
- }
2286
- const allRoots = new Set(rootToPaths.keys());
2287
- let changed = false;
2288
- for (const [root, paths] of rootToPaths) {
2289
- const hasNested = paths.some((p) => p.includes('.'));
2290
- if (hasNested) {
2291
- // Parse existing value or start fresh
2292
- let obj = {};
2293
- if (root in variables) {
2294
- try {
2295
- const parsed = JSON.parse(variables[root]);
2296
- if (typeof parsed === 'object' && parsed !== null) {
2297
- obj = parsed;
2298
- }
2299
- }
2300
- catch {
2301
- /* not JSON, will rebuild */
2302
- }
2303
- }
2304
- const added = buildNestedDefault(obj, paths);
2305
- if (!(root in variables) || added) {
2306
- variables[root] = JSON.stringify(obj);
2307
- changed = true;
2308
- }
2309
- }
2310
- else {
2311
- if (!(root in variables)) {
2312
- variables[root] = root.toUpperCase();
2313
- changed = true;
2314
- }
2315
- }
2316
- }
2317
- // Remove variables whose root is no longer referenced
2318
- for (const varName of Object.keys(variables)) {
2319
- if (!allRoots.has(varName)) {
2320
- delete variables[varName];
2321
- changed = true;
2322
- }
2323
- }
2324
- return changed;
2325
- };
2326
-
2327
- const _loadedGoogleFonts = new Set();
2328
- const ensureGoogleFontLoaded = (fontName, fontData) => {
2329
- if (!fontName || _loadedGoogleFonts.has(fontName) || !isGoogleFont(fontName))
2330
- return;
2331
- if (!document?.fonts)
2332
- return;
2333
- _loadedGoogleFonts.add(fontName);
2334
- const fontFace = new FontFace(fontName, typeof fontData === 'string' ? `url(${fontData})` : fontData, { display: 'swap' });
2335
- fontFace.load().then(() => document.fonts.add(fontFace)).catch(() => { });
2336
- };
2337
- const replaceUnsupportedChars = (text, fontKitFont) => {
2338
- const charSupportCache = {};
2339
- const isCharSupported = (char) => {
2340
- if (char in charSupportCache) {
2341
- return charSupportCache[char];
2342
- }
2343
- const isSupported = fontKitFont.hasGlyphForCodePoint(char.codePointAt(0) || 0);
2344
- charSupportCache[char] = isSupported;
2345
- return isSupported;
2346
- };
2347
- const segments = text.split(/(\r\n|\n|\r)/);
2348
- return segments
2349
- .map((segment) => {
2350
- if (/\r\n|\n|\r/.test(segment)) {
2351
- return segment;
2352
- }
2353
- return segment
2354
- .split('')
2355
- .map((char) => {
2356
- if (/\s/.test(char) || char.charCodeAt(0) < 32) {
2357
- return char;
2358
- }
2359
- return isCharSupported(char) ? char : '〿';
2360
- })
2361
- .join('');
2362
- })
2363
- .join('');
2364
- };
2365
- const uiRender$1 = async (arg) => {
2366
- const { value, schema, mode, onChange, stopEditing, tabIndex, placeholder, options, _cache } = arg;
2367
- const usePlaceholder = isEditable(mode, schema) && placeholder && !value;
2368
- const getText = (element) => {
2369
- let text = element.innerText;
2370
- if (text.endsWith('\n')) {
2371
- // contenteditable adds additional newline char retrieved with innerText
2372
- text = text.slice(0, -1);
2373
- }
2374
- return text;
2375
- };
2376
- const font = options?.font || getDefaultFont();
2377
- const resolvedFontName = resolveFontName(schema.fontName, schema.bold, schema.italic, font);
2378
- // Lazily load Google Font for CSS rendering if needed
2379
- if (resolvedFontName && font[resolvedFontName]) {
2380
- ensureGoogleFontLoaded(resolvedFontName, font[resolvedFontName].data);
2045
+ return text;
2046
+ };
2047
+ const font = options?.font || getDefaultFont();
2048
+ const resolvedFontName = resolveFontName(schema.fontName, schema.bold, schema.italic, font);
2049
+ // Lazily load Google Font for CSS rendering if needed
2050
+ if (resolvedFontName && font[resolvedFontName]) {
2051
+ ensureGoogleFontLoaded(resolvedFontName, font[resolvedFontName].data);
2381
2052
  }
2382
2053
  // Show a subtle loading state while the font binary is being fetched
2383
2054
  const isLoading = isGoogleFont(resolvedFontName);
@@ -2386,7 +2057,7 @@ const uiRender$1 = async (arg) => {
2386
2057
  arg.rootElement.style.transition = 'opacity 0.15s ease-out';
2387
2058
  }
2388
2059
  const fontKitFont = await getFontKitFont(resolvedFontName, font, _cache);
2389
- const textBlock = buildStyledTextContainer(arg, fontKitFont, usePlaceholder ? placeholder : value);
2060
+ const textBlock = buildStyledTextContainer(arg, fontKitFont, usePlaceholder ? effectivePlaceholder : value);
2390
2061
  // Fade in once the font is ready
2391
2062
  if (isLoading) {
2392
2063
  // Force a reflow so the transition triggers from the dimmed state
@@ -2436,141 +2107,437 @@ const uiRender$1 = async (arg) => {
2436
2107
  if (usePlaceholder) {
2437
2108
  textBlock.style.color = PLACEHOLDER_FONT_COLOR;
2438
2109
  textBlock.addEventListener('focus', () => {
2439
- if (textBlock.innerText === placeholder) {
2110
+ if (textBlock.innerText === effectivePlaceholder) {
2440
2111
  textBlock.innerText = '';
2441
2112
  textBlock.style.color = schema.fontColor ?? DEFAULT_FONT_COLOR;
2442
2113
  }
2443
- });
2444
- }
2445
- if (mode === 'designer') {
2446
- setTimeout(() => {
2447
- textBlock.focus();
2448
- // Set the focus to the end of the editable element when you focus, as we would for a textarea
2449
- const selection = window.getSelection();
2450
- const range = document.createRange();
2451
- if (selection && range) {
2452
- range.selectNodeContents(textBlock);
2453
- range.collapse(false); // Collapse range to the end
2454
- selection?.removeAllRanges();
2455
- selection?.addRange(range);
2114
+ });
2115
+ }
2116
+ if (mode === 'designer') {
2117
+ setTimeout(() => {
2118
+ textBlock.focus();
2119
+ // Set the focus to the end of the editable element when you focus, as we would for a textarea
2120
+ const selection = window.getSelection();
2121
+ const range = document.createRange();
2122
+ if (selection && range) {
2123
+ range.selectNodeContents(textBlock);
2124
+ range.collapse(false); // Collapse range to the end
2125
+ selection?.removeAllRanges();
2126
+ selection?.addRange(range);
2127
+ }
2128
+ });
2129
+ }
2130
+ };
2131
+ const buildStyledTextContainer = (arg, fontKitFont, value) => {
2132
+ const { schema, rootElement, mode } = arg;
2133
+ let dynamicFontSize = undefined;
2134
+ if (schema.dynamicFontSize && value) {
2135
+ dynamicFontSize = calculateDynamicFontSize({
2136
+ textSchema: schema,
2137
+ fontKitFont,
2138
+ value,
2139
+ startingFontSize: dynamicFontSize,
2140
+ });
2141
+ }
2142
+ // Depending on vertical alignment, we need to move the top or bottom of the font to keep
2143
+ // it within it's defined box and align it with the generated pdf.
2144
+ const { topAdj, bottomAdj } = getBrowserVerticalFontAdjustments(fontKitFont, dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE, schema.lineHeight ?? DEFAULT_LINE_HEIGHT, schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT);
2145
+ const topAdjustment = topAdj.toString();
2146
+ const bottomAdjustment = bottomAdj.toString();
2147
+ const container = document.createElement('div');
2148
+ const containerStyle = {
2149
+ padding: 0,
2150
+ resize: 'none',
2151
+ backgroundColor: getBackgroundColor(value, schema),
2152
+ border: 'none',
2153
+ display: 'flex',
2154
+ flexDirection: 'column',
2155
+ justifyContent: mapVerticalAlignToFlex(schema.verticalAlignment),
2156
+ width: '100%',
2157
+ height: '100%',
2158
+ cursor: isEditable(mode, schema) ? 'text' : 'default',
2159
+ };
2160
+ Object.assign(container.style, containerStyle);
2161
+ rootElement.innerHTML = '';
2162
+ rootElement.appendChild(container);
2163
+ // text decoration
2164
+ const textDecorations = [];
2165
+ if (schema.strikethrough)
2166
+ textDecorations.push('line-through');
2167
+ if (schema.underline)
2168
+ textDecorations.push('underline');
2169
+ const textBlockStyle = {
2170
+ // Font formatting styles
2171
+ fontFamily: schema.fontName ? `'${schema.fontName}'` : 'inherit',
2172
+ fontWeight: schema.bold ? 'bold' : 'normal',
2173
+ fontStyle: schema.italic ? 'italic' : 'normal',
2174
+ color: schema.fontColor ? schema.fontColor : DEFAULT_FONT_COLOR,
2175
+ fontSize: `${dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE}pt`,
2176
+ letterSpacing: `${schema.characterSpacing ?? DEFAULT_CHARACTER_SPACING}pt`,
2177
+ lineHeight: `${schema.lineHeight ?? DEFAULT_LINE_HEIGHT}em`,
2178
+ textAlign: schema.alignment ?? DEFAULT_ALIGNMENT,
2179
+ whiteSpace: 'pre-wrap',
2180
+ wordBreak: 'break-word',
2181
+ // Block layout styles
2182
+ resize: 'none',
2183
+ border: 'none',
2184
+ outline: 'none',
2185
+ marginBottom: `${bottomAdjustment}px`,
2186
+ paddingTop: `${topAdjustment}px`,
2187
+ backgroundColor: 'transparent',
2188
+ textDecoration: textDecorations.join(' '),
2189
+ };
2190
+ const textBlock = document.createElement('div');
2191
+ textBlock.id = 'text-' + String(schema.id);
2192
+ Object.assign(textBlock.style, textBlockStyle);
2193
+ container.appendChild(textBlock);
2194
+ return textBlock;
2195
+ };
2196
+ /**
2197
+ * Firefox doesn't support 'plaintext-only' contentEditable mode, which we want to avoid mark-up.
2198
+ * This function adds a workaround for Firefox to make the contentEditable element behave like 'plaintext-only'.
2199
+ */
2200
+ const makeElementPlainTextContentEditable = (element) => {
2201
+ if (!isFirefox()) {
2202
+ element.contentEditable = 'plaintext-only';
2203
+ return;
2204
+ }
2205
+ element.contentEditable = 'true';
2206
+ element.addEventListener('keydown', (e) => {
2207
+ if (e.key === 'Enter' && !e.shiftKey) {
2208
+ e.preventDefault();
2209
+ document.execCommand('insertLineBreak', false, undefined);
2210
+ }
2211
+ });
2212
+ element.addEventListener('paste', (e) => {
2213
+ e.preventDefault();
2214
+ const paste = e.clipboardData?.getData('text');
2215
+ const selection = window.getSelection();
2216
+ if (!selection?.rangeCount)
2217
+ return;
2218
+ selection.deleteFromDocument();
2219
+ selection.getRangeAt(0).insertNode(document.createTextNode(paste || ''));
2220
+ selection.collapseToEnd();
2221
+ });
2222
+ };
2223
+ const mapVerticalAlignToFlex = (verticalAlignmentValue) => {
2224
+ switch (verticalAlignmentValue) {
2225
+ case VERTICAL_ALIGN_TOP:
2226
+ return 'flex-start';
2227
+ case VERTICAL_ALIGN_MIDDLE:
2228
+ return 'center';
2229
+ case VERTICAL_ALIGN_BOTTOM:
2230
+ return 'flex-end';
2231
+ }
2232
+ return 'flex-start';
2233
+ };
2234
+ const getBackgroundColor = (value, schema) => {
2235
+ if (!value || !schema.backgroundColor)
2236
+ return 'transparent';
2237
+ return schema.backgroundColor;
2238
+ };
2239
+
2240
+ const substituteVariables = (text, variablesIn, extraContext) => {
2241
+ if (!text) {
2242
+ return '';
2243
+ }
2244
+ let variables;
2245
+ try {
2246
+ variables =
2247
+ typeof variablesIn === 'string'
2248
+ ? JSON.parse(variablesIn || '{}')
2249
+ : variablesIn;
2250
+ }
2251
+ catch {
2252
+ throw new SyntaxError(`[@campxdev/schemas] richText: invalid JSON string '${variablesIn}'`);
2253
+ }
2254
+ // Merge extra context (e.g. currentPage, totalPages) with user variables
2255
+ // System context takes precedence over user variables
2256
+ const merged = extraContext ? { ...variables, ...extraContext } : variables;
2257
+ // Use the full JS expression evaluator — supports {varName}, {expr * 2}, {str.toUpperCase()}, etc.
2258
+ const result = replacePlaceholders({ content: text, variables: merged, schemas: [] });
2259
+ // Strip any remaining unresolved {placeholders} for clean output
2260
+ return result.replace(/\{[^{}]+\}/g, '');
2261
+ };
2262
+ const validateVariables = (value, schema) => {
2263
+ if (!schema.variables || schema.variables.length === 0) {
2264
+ return true;
2265
+ }
2266
+ let values;
2267
+ try {
2268
+ values = value ? JSON.parse(value) : {};
2269
+ }
2270
+ catch {
2271
+ throw new SyntaxError(`[@campxdev/generator] invalid JSON string '${value}' for variables in field ${schema.name}`);
2272
+ }
2273
+ for (const variable of schema.variables) {
2274
+ if (!values[variable]) {
2275
+ if (schema.required) {
2276
+ throw new Error(`[@campxdev/generator] variable ${variable} is missing for field ${schema.name}`);
2277
+ }
2278
+ return false;
2279
+ }
2280
+ }
2281
+ return true;
2282
+ };
2283
+
2284
+ const pdfRender = async (arg) => {
2285
+ const { value, schema, pageContext, ...rest } = arg;
2286
+ // Static mode: no template text → render value directly as plain text
2287
+ if (!schema.text) {
2288
+ await pdfRender$1({ value, schema, ...rest });
2289
+ return;
2290
+ }
2291
+ // readOnly: value is already resolved by generate.ts via replacePlaceholders
2292
+ if (schema.readOnly) {
2293
+ await pdfRender$1({ value, schema, ...rest });
2294
+ return;
2295
+ }
2296
+ // Dynamic mode (form): substitute variables in template
2297
+ if (!validateVariables(value, schema)) {
2298
+ return;
2299
+ }
2300
+ const renderArgs = {
2301
+ value: substituteVariables(schema.text, value || '{}', pageContext),
2302
+ schema,
2303
+ ...rest,
2304
+ };
2305
+ await pdfRender$1(renderArgs);
2306
+ };
2307
+
2308
+ const variablePrefixSuffixWidget = (props) => {
2309
+ const { rootElement, changeSchemas, activeSchema, options } = props;
2310
+ const schema = activeSchema;
2311
+ const variables = options.variables?.textVariables ?? [];
2312
+ if (variables.length === 0) {
2313
+ const hint = document.createElement('div');
2314
+ hint.textContent = 'Type {variableName} directly in the text field';
2315
+ hint.style.cssText = 'font-size:11px;color:#aaa;margin-bottom:8px;font-style:italic;';
2316
+ rootElement.appendChild(hint);
2317
+ return;
2318
+ }
2319
+ const container = document.createElement('div');
2320
+ container.style.cssText = 'display:flex;flex-direction:column;gap:4px;margin-bottom:8px;';
2321
+ const title = document.createElement('div');
2322
+ title.textContent = 'Insert Variable:';
2323
+ title.style.cssText = 'font-size:12px;color:#666;margin-bottom:2px;';
2324
+ container.appendChild(title);
2325
+ for (const v of variables) {
2326
+ const row = document.createElement('div');
2327
+ row.style.cssText = 'display:flex;align-items:center;gap:6px;';
2328
+ const makeBtn = (side) => {
2329
+ const btn = document.createElement('button');
2330
+ btn.textContent = '+';
2331
+ btn.title = side === 'prefix' ? `Add ${v.label} as prefix` : `Add ${v.label} as suffix`;
2332
+ btn.style.cssText =
2333
+ 'width:22px;height:22px;cursor:pointer;border:1px solid #d9d9d9;border-radius:4px;' +
2334
+ 'background:#fff;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center;padding:0;';
2335
+ btn.onclick = () => {
2336
+ const current = String(schema.text ?? '');
2337
+ const next = side === 'prefix' ? `{${v.value}} ${current}` : `${current} {${v.value}}`;
2338
+ changeSchemas([{ key: 'text', value: next, schemaId: activeSchema.id }]);
2339
+ };
2340
+ return btn;
2341
+ };
2342
+ const label = document.createElement('span');
2343
+ label.textContent = v.label;
2344
+ label.style.cssText = 'font-size:12px;flex:1;color:#333;';
2345
+ row.appendChild(makeBtn('prefix'));
2346
+ row.appendChild(label);
2347
+ row.appendChild(makeBtn('suffix'));
2348
+ container.appendChild(row);
2349
+ }
2350
+ rootElement.appendChild(container);
2351
+ };
2352
+ const mapDynamicVariables$1 = (props) => {
2353
+ const { rootElement, changeSchemas, activeSchema, options } = props;
2354
+ const rtSchema = activeSchema;
2355
+ const text = rtSchema.text ?? '';
2356
+ if (!text) {
2357
+ rootElement.style.display = 'none';
2358
+ return;
2359
+ }
2360
+ const textVars = options?.variables?.textVariables ?? [];
2361
+ const defaults = {};
2362
+ for (const v of textVars) {
2363
+ if (v.defaultValue)
2364
+ defaults[v.value] = v.defaultValue;
2365
+ }
2366
+ const variables = JSON.parse((rtSchema.content && rtSchema.content !== '' ? rtSchema.content : '{}'));
2367
+ const variablesChanged = updateVariablesFromText(text, variables, defaults);
2368
+ const varNames = Object.keys(variables);
2369
+ if (variablesChanged) {
2370
+ changeSchemas([
2371
+ { key: 'content', value: JSON.stringify(variables), schemaId: activeSchema.id },
2372
+ { key: 'variables', value: varNames, schemaId: activeSchema.id },
2373
+ { key: 'readOnly', value: varNames.length === 0, schemaId: activeSchema.id },
2374
+ ]);
2375
+ }
2376
+ rootElement.style.display = 'none';
2377
+ };
2378
+ const propPanel$1 = {
2379
+ schema: (propPanelProps) => {
2380
+ if (typeof propPanel$2.schema !== 'function') {
2381
+ throw new Error('Oops, is text schema no longer a function?');
2382
+ }
2383
+ const parentSchema = typeof propPanel$2.schema === 'function' ? propPanel$2.schema(propPanelProps) : {};
2384
+ return {
2385
+ insertVariablePicker: {
2386
+ type: 'void',
2387
+ widget: 'variablePrefixSuffixWidget',
2388
+ bind: false,
2389
+ span: 24,
2390
+ },
2391
+ '----': { type: 'void', widget: 'Divider' },
2392
+ ...parentSchema,
2393
+ dynamicVariables: {
2394
+ type: 'object',
2395
+ widget: 'mapDynamicVariables',
2396
+ bind: false,
2397
+ span: 0,
2398
+ },
2399
+ };
2400
+ },
2401
+ widgets: { ...(propPanel$2.widgets || {}), mapDynamicVariables: mapDynamicVariables$1, variablePrefixSuffixWidget },
2402
+ defaultSchema: {
2403
+ ...propPanel$2.defaultSchema,
2404
+ readOnly: false,
2405
+ type: 'richText',
2406
+ text: '',
2407
+ width: 50,
2408
+ height: 7,
2409
+ content: '',
2410
+ variables: [],
2411
+ },
2412
+ };
2413
+ /** Known JS globals/keywords that should NOT be treated as user-defined variables */
2414
+ const RESERVED_NAMES$1 = new Set([
2415
+ 'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
2416
+ 'void', 'delete', 'new', 'this', 'NaN', 'Infinity',
2417
+ 'Math', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Date', 'JSON',
2418
+ 'isNaN', 'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent',
2419
+ 'encodeURI', 'encodeURIComponent', 'date', 'dateTime',
2420
+ 'currentPage', 'totalPages',
2421
+ ]);
2422
+ const extractDotPaths = (expr) => {
2423
+ const cleaned = expr.replace(/'[^']*'|"[^"]*"|`[^`]*`/g, (m) => ' '.repeat(m.length));
2424
+ const pathRegex = /[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g;
2425
+ const paths = new Set();
2426
+ let m;
2427
+ while ((m = pathRegex.exec(cleaned)) !== null) {
2428
+ let path = m[0];
2429
+ const nextChar = cleaned[m.index + path.length];
2430
+ if (nextChar === '(') {
2431
+ const lastDot = path.lastIndexOf('.');
2432
+ if (lastDot !== -1) {
2433
+ path = path.substring(0, lastDot);
2434
+ }
2435
+ else {
2436
+ continue;
2456
2437
  }
2457
- });
2438
+ }
2439
+ const root = path.split('.')[0];
2440
+ if (!RESERVED_NAMES$1.has(root))
2441
+ paths.add(path);
2458
2442
  }
2443
+ return Array.from(paths);
2459
2444
  };
2460
- const buildStyledTextContainer = (arg, fontKitFont, value) => {
2461
- const { schema, rootElement, mode } = arg;
2462
- let dynamicFontSize = undefined;
2463
- if (schema.dynamicFontSize && value) {
2464
- dynamicFontSize = calculateDynamicFontSize({
2465
- textSchema: schema,
2466
- fontKitFont,
2467
- value,
2468
- startingFontSize: dynamicFontSize,
2469
- });
2445
+ const buildNestedDefault = (obj, paths, defaults) => {
2446
+ let added = false;
2447
+ for (const path of paths) {
2448
+ const parts = path.split('.');
2449
+ if (parts.length <= 1)
2450
+ continue;
2451
+ let current = obj;
2452
+ for (let i = 1; i < parts.length - 1; i++) {
2453
+ if (!(parts[i] in current) || typeof current[parts[i]] !== 'object' || current[parts[i]] === null) {
2454
+ current[parts[i]] = {};
2455
+ added = true;
2456
+ }
2457
+ current = current[parts[i]];
2458
+ }
2459
+ const leaf = parts[parts.length - 1];
2460
+ const isOldLeafFormat = leaf in current && (/^[A-Z][A-Z0-9_]+$/.test(String(current[leaf])) || /^\{\{.*\}\}$/.test(String(current[leaf])));
2461
+ if (!(leaf in current) || isOldLeafFormat) {
2462
+ current[leaf] = defaults?.[path] ?? path.replace(/\./g, ' ');
2463
+ added = true;
2464
+ }
2470
2465
  }
2471
- // Depending on vertical alignment, we need to move the top or bottom of the font to keep
2472
- // it within it's defined box and align it with the generated pdf.
2473
- const { topAdj, bottomAdj } = getBrowserVerticalFontAdjustments(fontKitFont, dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE, schema.lineHeight ?? DEFAULT_LINE_HEIGHT, schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT);
2474
- const topAdjustment = topAdj.toString();
2475
- const bottomAdjustment = bottomAdj.toString();
2476
- const container = document.createElement('div');
2477
- const containerStyle = {
2478
- padding: 0,
2479
- resize: 'none',
2480
- backgroundColor: getBackgroundColor(value, schema),
2481
- border: 'none',
2482
- display: 'flex',
2483
- flexDirection: 'column',
2484
- justifyContent: mapVerticalAlignToFlex(schema.verticalAlignment),
2485
- width: '100%',
2486
- height: '100%',
2487
- cursor: isEditable(mode, schema) ? 'text' : 'default',
2488
- };
2489
- Object.assign(container.style, containerStyle);
2490
- rootElement.innerHTML = '';
2491
- rootElement.appendChild(container);
2492
- // text decoration
2493
- const textDecorations = [];
2494
- if (schema.strikethrough)
2495
- textDecorations.push('line-through');
2496
- if (schema.underline)
2497
- textDecorations.push('underline');
2498
- const textBlockStyle = {
2499
- // Font formatting styles
2500
- fontFamily: schema.fontName ? `'${schema.fontName}'` : 'inherit',
2501
- fontWeight: schema.bold ? 'bold' : 'normal',
2502
- fontStyle: schema.italic ? 'italic' : 'normal',
2503
- color: schema.fontColor ? schema.fontColor : DEFAULT_FONT_COLOR,
2504
- fontSize: `${dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE}pt`,
2505
- letterSpacing: `${schema.characterSpacing ?? DEFAULT_CHARACTER_SPACING}pt`,
2506
- lineHeight: `${schema.lineHeight ?? DEFAULT_LINE_HEIGHT}em`,
2507
- textAlign: schema.alignment ?? DEFAULT_ALIGNMENT,
2508
- whiteSpace: 'pre-wrap',
2509
- wordBreak: 'break-word',
2510
- // Block layout styles
2511
- resize: 'none',
2512
- border: 'none',
2513
- outline: 'none',
2514
- marginBottom: `${bottomAdjustment}px`,
2515
- paddingTop: `${topAdjustment}px`,
2516
- backgroundColor: 'transparent',
2517
- textDecoration: textDecorations.join(' '),
2518
- };
2519
- const textBlock = document.createElement('div');
2520
- textBlock.id = 'text-' + String(schema.id);
2521
- Object.assign(textBlock.style, textBlockStyle);
2522
- container.appendChild(textBlock);
2523
- return textBlock;
2466
+ return added;
2524
2467
  };
2525
- /**
2526
- * Firefox doesn't support 'plaintext-only' contentEditable mode, which we want to avoid mark-up.
2527
- * This function adds a workaround for Firefox to make the contentEditable element behave like 'plaintext-only'.
2528
- */
2529
- const makeElementPlainTextContentEditable = (element) => {
2530
- if (!isFirefox()) {
2531
- element.contentEditable = 'plaintext-only';
2532
- return;
2468
+ const updateVariablesFromText = (text, variables, defaults) => {
2469
+ const blockRegex = /\{([^{}]+)\}/g;
2470
+ const allPaths = new Set();
2471
+ let blockMatch;
2472
+ while ((blockMatch = blockRegex.exec(text)) !== null) {
2473
+ for (const path of extractDotPaths(blockMatch[1])) {
2474
+ allPaths.add(path);
2475
+ }
2533
2476
  }
2534
- element.contentEditable = 'true';
2535
- element.addEventListener('keydown', (e) => {
2536
- if (e.key === 'Enter' && !e.shiftKey) {
2537
- e.preventDefault();
2538
- document.execCommand('insertLineBreak', false, undefined);
2477
+ const rootToPaths = new Map();
2478
+ for (const path of allPaths) {
2479
+ const root = path.split('.')[0];
2480
+ if (!rootToPaths.has(root))
2481
+ rootToPaths.set(root, []);
2482
+ rootToPaths.get(root).push(path);
2483
+ }
2484
+ const allRoots = new Set(rootToPaths.keys());
2485
+ let changed = false;
2486
+ for (const [root, paths] of rootToPaths) {
2487
+ const hasNested = paths.some((p) => p.includes('.'));
2488
+ if (hasNested) {
2489
+ let obj = {};
2490
+ if (root in variables) {
2491
+ try {
2492
+ const parsed = JSON.parse(variables[root]);
2493
+ if (typeof parsed === 'object' && parsed !== null) {
2494
+ obj = parsed;
2495
+ }
2496
+ }
2497
+ catch {
2498
+ /* not JSON, will rebuild */
2499
+ }
2500
+ }
2501
+ const added = buildNestedDefault(obj, paths, defaults);
2502
+ if (!(root in variables) || added) {
2503
+ variables[root] = JSON.stringify(obj);
2504
+ changed = true;
2505
+ }
2506
+ }
2507
+ else {
2508
+ const existingVal = variables[root];
2509
+ const isStaleObject = typeof existingVal === 'string' && (() => {
2510
+ try {
2511
+ const p = JSON.parse(existingVal);
2512
+ return typeof p === 'object' && p !== null;
2513
+ }
2514
+ catch {
2515
+ return false;
2516
+ }
2517
+ })();
2518
+ const isOldFormat = typeof existingVal === 'string' && (/^[A-Z][A-Z0-9_]*$/.test(existingVal) || /^\{\{.*\}\}$/.test(existingVal));
2519
+ if (!(root in variables) || isStaleObject || isOldFormat) {
2520
+ variables[root] = defaults?.[root] ?? root;
2521
+ changed = true;
2522
+ }
2539
2523
  }
2540
- });
2541
- element.addEventListener('paste', (e) => {
2542
- e.preventDefault();
2543
- const paste = e.clipboardData?.getData('text');
2544
- const selection = window.getSelection();
2545
- if (!selection?.rangeCount)
2546
- return;
2547
- selection.deleteFromDocument();
2548
- selection.getRangeAt(0).insertNode(document.createTextNode(paste || ''));
2549
- selection.collapseToEnd();
2550
- });
2551
- };
2552
- const mapVerticalAlignToFlex = (verticalAlignmentValue) => {
2553
- switch (verticalAlignmentValue) {
2554
- case VERTICAL_ALIGN_TOP:
2555
- return 'flex-start';
2556
- case VERTICAL_ALIGN_MIDDLE:
2557
- return 'center';
2558
- case VERTICAL_ALIGN_BOTTOM:
2559
- return 'flex-end';
2560
2524
  }
2561
- return 'flex-start';
2562
- };
2563
- const getBackgroundColor = (value, schema) => {
2564
- if (!value || !schema.backgroundColor)
2565
- return 'transparent';
2566
- return schema.backgroundColor;
2525
+ for (const varName of Object.keys(variables)) {
2526
+ if (!allRoots.has(varName)) {
2527
+ delete variables[varName];
2528
+ changed = true;
2529
+ }
2530
+ }
2531
+ return changed;
2567
2532
  };
2568
2533
 
2569
- const uiRender = async (arg) => {
2534
+ /** Format {expr} {{ expr with dots as spaces }} for display */
2535
+ const formatTemplateDisplay$1 = (text) => text.replace(/\{([^{}]+)\}/g, (_, expr) => '{{ ' + expr.trim().replace(/\./g, ' ') + ' }}');
2536
+ const uiRender$1 = async (arg) => {
2570
2537
  const { value, schema, rootElement, mode, onChange, pageContext, ...rest } = arg;
2571
2538
  // Static mode: no template text → delegate to plain text behavior
2572
2539
  if (!schema.text) {
2573
- await uiRender$1(arg);
2540
+ await uiRender$2({ ...arg, value: value || '' });
2574
2541
  return;
2575
2542
  }
2576
2543
  // Dynamic mode: template with optional variables
@@ -2580,8 +2547,8 @@ const uiRender = async (arg) => {
2580
2547
  await formUiRender(arg);
2581
2548
  return;
2582
2549
  }
2583
- await uiRender$1({
2584
- value: isEditable(mode, schema) ? text : substituteVariables(text, value, pageContext),
2550
+ await uiRender$2({
2551
+ value: isEditable(mode, schema) ? text : formatTemplateDisplay$1(text),
2585
2552
  schema,
2586
2553
  mode: mode === 'form' ? 'viewer' : mode, // if no variables for form it's just a viewer
2587
2554
  rootElement,
@@ -2791,7 +2758,6 @@ const countUniqueVariableNames = (content) => {
2791
2758
  /**
2792
2759
  * An optimisation to try to minimise jank while typing.
2793
2760
  * Only check whether variables were modified based on certain key presses.
2794
- * Regex would otherwise be performed on every key press (which isn't terrible, but this code helps).
2795
2761
  */
2796
2762
  const keyPressShouldBeChecked = (event) => {
2797
2763
  if (event.key === 'ArrowUp' ||
@@ -2813,14 +2779,191 @@ const keyPressShouldBeChecked = (event) => {
2813
2779
  return true;
2814
2780
  };
2815
2781
 
2782
+ const schema$1 = {
2783
+ pdf: pdfRender,
2784
+ ui: uiRender$1,
2785
+ propPanel: propPanel$1,
2786
+ icon: createSvgStr(FileText),
2787
+ uninterruptedEditMode: true,
2788
+ };
2789
+
2790
+ /** Replaces any existing {var} in the template with {newVar}, or appends if none exists */
2791
+ const replaceOrAppendVariable = (currentText, varName) => {
2792
+ // Replace any existing {identifier} block with the new variable
2793
+ const replaced = currentText.replace(/\{[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*\}/g, `{${varName}}`);
2794
+ if (replaced !== currentText)
2795
+ return replaced;
2796
+ // No existing variable — append with a space separator
2797
+ return currentText ? `${currentText} {${varName}}` : `{${varName}}`;
2798
+ };
2799
+ const singleVariablePickerWidget = (props) => {
2800
+ const { rootElement, changeSchemas, activeSchema, options } = props;
2801
+ const schema = activeSchema;
2802
+ const variables = options.variables?.textVariables ?? [];
2803
+ if (variables.length === 0)
2804
+ return;
2805
+ const container = document.createElement('div');
2806
+ container.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:10px;';
2807
+ const label = document.createElement('span');
2808
+ label.textContent = 'Variable:';
2809
+ label.style.cssText = 'font-size:12px;color:#666;white-space:nowrap;';
2810
+ const select = document.createElement('select');
2811
+ select.style.cssText =
2812
+ 'flex:1;height:30px;border:1px solid #E0E0E0;border-radius:4px;padding:0 8px;font-size:13px;background:#fff;cursor:pointer;';
2813
+ const blank = document.createElement('option');
2814
+ blank.value = '';
2815
+ blank.textContent = '— pick variable —';
2816
+ select.appendChild(blank);
2817
+ for (const v of variables) {
2818
+ const opt = document.createElement('option');
2819
+ opt.value = v.value;
2820
+ opt.textContent = v.label;
2821
+ select.appendChild(opt);
2822
+ }
2823
+ select.onchange = (e) => {
2824
+ const varName = e.target.value;
2825
+ if (!varName)
2826
+ return;
2827
+ const current = String(schema.text ?? '');
2828
+ const next = replaceOrAppendVariable(current, varName);
2829
+ changeSchemas([{ key: 'text', value: next, schemaId: activeSchema.id }]);
2830
+ select.value = '';
2831
+ };
2832
+ container.appendChild(label);
2833
+ container.appendChild(select);
2834
+ rootElement.appendChild(container);
2835
+ };
2836
+ const mapDynamicVariables = (props) => {
2837
+ const { rootElement, changeSchemas, activeSchema, options } = props;
2838
+ const svtSchema = activeSchema;
2839
+ const text = svtSchema.text ?? '';
2840
+ if (!text) {
2841
+ rootElement.style.display = 'none';
2842
+ return;
2843
+ }
2844
+ // Extract all simple variable references from the template
2845
+ const varMatches = [...text.matchAll(/\{([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\}/g)].map(m => m[1]);
2846
+ // Keep only the first unique variable (enforce single-variable restriction)
2847
+ const uniqueVars = [...new Set(varMatches)];
2848
+ const variable = uniqueVars[0];
2849
+ const varNames = variable ? [variable] : [];
2850
+ const variables = {};
2851
+ if (variable) {
2852
+ const existing = JSON.parse(svtSchema.content && svtSchema.content !== '' ? svtSchema.content : '{}');
2853
+ const existingVal = existing[variable];
2854
+ const isOldFormat = typeof existingVal === 'string' && (/^[A-Z][A-Z0-9_]*$/.test(existingVal) || /^\{\{.*\}\}$/.test(existingVal));
2855
+ const isStaleObject = typeof existingVal === 'string' && (() => {
2856
+ try {
2857
+ const p = JSON.parse(existingVal);
2858
+ return typeof p === 'object' && p !== null;
2859
+ }
2860
+ catch {
2861
+ return false;
2862
+ }
2863
+ })();
2864
+ const textVars = options?.variables?.textVariables ?? [];
2865
+ const apiDefault = textVars.find((v) => v.value === variable)?.defaultValue;
2866
+ const fallbackDefault = variable.replace(/\./g, ' ');
2867
+ variables[variable] = (!existingVal || isOldFormat || isStaleObject) ? (apiDefault ?? fallbackDefault) : existingVal;
2868
+ }
2869
+ const content = varNames.length > 0 ? JSON.stringify(variables) : '';
2870
+ const changed = JSON.stringify(svtSchema.variables ?? []) !== JSON.stringify(varNames) ||
2871
+ (svtSchema.content ?? '') !== content;
2872
+ if (changed) {
2873
+ changeSchemas([
2874
+ { key: 'content', value: content, schemaId: activeSchema.id },
2875
+ { key: 'variables', value: varNames, schemaId: activeSchema.id },
2876
+ { key: 'readOnly', value: varNames.length === 0, schemaId: activeSchema.id },
2877
+ ]);
2878
+ }
2879
+ rootElement.style.display = 'none';
2880
+ };
2881
+ const propPanel = {
2882
+ schema: (propPanelProps) => {
2883
+ if (typeof propPanel$1.schema !== 'function') {
2884
+ throw new Error('richText propPanel schema is not a function');
2885
+ }
2886
+ const parentSchema = propPanel$1.schema(propPanelProps);
2887
+ // Strip out richText-specific structural keys; we'll re-add our own versions
2888
+ const { insertVariablePicker: _ip, '----': _div, dynamicVariables: _dv, ...textFields } = parentSchema;
2889
+ return {
2890
+ insertVariablePicker: {
2891
+ type: 'void',
2892
+ widget: 'singleVariablePickerWidget',
2893
+ bind: false,
2894
+ span: 24,
2895
+ },
2896
+ '----': { type: 'void', widget: 'Divider' },
2897
+ ...textFields,
2898
+ dynamicVariables: {
2899
+ type: 'object',
2900
+ widget: 'mapDynamicVariables',
2901
+ bind: false,
2902
+ span: 0,
2903
+ },
2904
+ };
2905
+ },
2906
+ widgets: {
2907
+ ...(propPanel$1.widgets || {}),
2908
+ singleVariablePickerWidget,
2909
+ mapDynamicVariables,
2910
+ },
2911
+ defaultSchema: {
2912
+ ...propPanel$1.defaultSchema,
2913
+ type: 'singleVariableText',
2914
+ text: '',
2915
+ content: '',
2916
+ height: 7,
2917
+ variables: [],
2918
+ },
2919
+ };
2920
+
2921
+ /** Format {expr} → {{ expr with dots as spaces }} */
2922
+ const formatTemplateDisplay = (text) => text.replace(/\{([^{}]+)\}/g, (_, expr) => '{{ ' + expr.trim().replace(/\./g, ' ') + ' }}');
2923
+ const uiRender = async (arg) => {
2924
+ const { mode, schema, options, _cache } = arg;
2925
+ if (mode === 'designer') {
2926
+ // ReadOnly preview — show actual default value if available, else {{ variable }}
2927
+ const text = schema.text ?? '';
2928
+ let displayText = '';
2929
+ if (text) {
2930
+ try {
2931
+ const content = JSON.parse(schema.content || '{}');
2932
+ const firstValue = Object.values(content)[0];
2933
+ displayText = (typeof firstValue === 'string' && firstValue) ? firstValue : formatTemplateDisplay(text);
2934
+ }
2935
+ catch {
2936
+ displayText = formatTemplateDisplay(text);
2937
+ }
2938
+ }
2939
+ const font = options?.font || getDefaultFont();
2940
+ const fontKitFont = await getFontKitFont(schema.fontName, font, _cache);
2941
+ const textBlock = buildStyledTextContainer(arg, fontKitFont, displayText);
2942
+ textBlock.textContent = displayText;
2943
+ textBlock.style.cursor = 'default';
2944
+ return;
2945
+ }
2946
+ // Viewer/form modes: delegate to richText render
2947
+ await uiRender$1(arg);
2948
+ };
2816
2949
  const schema = {
2817
2950
  pdf: pdfRender,
2818
2951
  ui: uiRender,
2819
2952
  propPanel,
2820
- icon: createSvgStr(Type),
2821
- uninterruptedEditMode: true,
2953
+ icon: createSvgStr(BetweenHorizontalStart),
2954
+ };
2955
+
2956
+ const textSchema = {
2957
+ pdf: pdfRender$1,
2958
+ ui: uiRender$2,
2959
+ propPanel: propPanel$2,
2960
+ icon: createSvgStr(TextCursorInput),
2822
2961
  };
2823
2962
 
2824
- const builtInPlugins = { Text: schema };
2963
+ const builtInPlugins = {
2964
+ Text: textSchema,
2965
+ 'Dynamic Text': schema,
2966
+ 'Rich Text': schema$1,
2967
+ };
2825
2968
 
2826
- export { getExtraFormatterSchema as A, DEFAULT_LINE_HEIGHT as B, mapVerticalAlignToFlex as C, DEFAULT_OPACITY as D, Formatter as F, HEX_COLOR_PATTERN as H, VERTICAL_ALIGN_MIDDLE as V, addAlphaToHex as a, builtInPlugins as b, createSvgStr as c, convertForPdfLayoutProps as d, rotatePoint as e, getDefaultCellStyles as f, getDynamicHeightsForTable as g, hex2PrintingColor as h, isEditable as i, substituteVariables as j, createErrorElm as k, createInsertVariableWidget as l, propPanel$1 as m, getBodyWithRange as n, createSingleTable as o, pdfRender$1 as p, getBody as q, readFile as r, schema as s, getColumnStylesPropPanelSchema as t, uiRender$1 as u, getCellPropPanelSchema as v, DEFAULT_FONT_COLOR as w, DEFAULT_CHARACTER_SPACING as x, DEFAULT_FONT_SIZE as y, DEFAULT_ALIGNMENT as z };
2969
+ export { DEFAULT_CHARACTER_SPACING as A, DEFAULT_FONT_SIZE as B, DEFAULT_ALIGNMENT as C, DEFAULT_OPACITY as D, getExtraFormatterSchema as E, Formatter as F, DEFAULT_LINE_HEIGHT as G, HEX_COLOR_PATTERN as H, mapVerticalAlignToFlex as I, VERTICAL_ALIGN_MIDDLE as V, schema as a, builtInPlugins as b, createSvgStr as c, addAlphaToHex as d, convertForPdfLayoutProps as e, rotatePoint as f, getDynamicHeightsForTable as g, hex2PrintingColor as h, isEditable as i, getDefaultCellStyles as j, createErrorElm as k, propPanel$2 as l, getFontKitFont as m, buildStyledTextContainer as n, makeElementPlainTextContentEditable as o, pdfRender$1 as p, getBodyWithRange as q, readFile as r, schema$1 as s, textSchema as t, uiRender$2 as u, createSingleTable as v, getBody as w, getColumnStylesPropPanelSchema as x, getCellPropPanelSchema as y, DEFAULT_FONT_COLOR as z };