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