jtlt 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGES.md +9 -0
  2. package/README.md +46 -78
  3. package/demo/codemirror.esm.js +28242 -0
  4. package/demo/codemirror.js +94 -0
  5. package/demo/index.css +7 -0
  6. package/demo/index.html +11 -14
  7. package/demo/index.js +206 -26
  8. package/demo/vendor/jamilih/dist/jml.mjs +2341 -0
  9. package/demo/vendor/jhtml/src/SAJJ/SAJJ.ObjectArrayDelegator.js +356 -0
  10. package/demo/vendor/jhtml/src/SAJJ/SAJJ.Stringifier.js +186 -0
  11. package/demo/vendor/jhtml/src/SAJJ/SAJJ.js +746 -0
  12. package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.html +33 -0
  13. package/demo/vendor/jhtml/src/SAJJ/testing/SAJJ.testing.js +25 -0
  14. package/demo/vendor/jhtml/src/jhtml-browser.js +5 -0
  15. package/demo/vendor/jhtml/src/jhtml-node.cts +3 -0
  16. package/demo/vendor/jhtml/src/jhtml-node.js +8 -0
  17. package/demo/vendor/jhtml/src/jhtml-node.mts +1 -0
  18. package/demo/vendor/jhtml/src/jhtml.cts +3 -0
  19. package/demo/vendor/jhtml/src/jhtml.js +602 -0
  20. package/demo/vendor/jhtml/src/jhtml.mts +1 -0
  21. package/demo/vendor/jsonpath-plus/dist/index-browser-esm.js +2158 -0
  22. package/demo/vendor/simple-get-json/dist/index-es.js +151 -0
  23. package/dist/JSONPathTransformerContext.d.ts +88 -4
  24. package/dist/JSONPathTransformerContext.d.ts.map +1 -1
  25. package/dist/XPathTransformer.d.ts +2 -2
  26. package/dist/XPathTransformerContext.d.ts +92 -8
  27. package/dist/XPathTransformerContext.d.ts.map +1 -1
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.d.ts.map +1 -1
  30. package/docs/API.expanded.md +5 -3
  31. package/docs/API.md +1 -1
  32. package/docs/TO-DO.md +55 -31
  33. package/eslint.config.js +3 -1
  34. package/package.json +24 -4
  35. package/rollup.config.js +13 -0
  36. package/src/JSONPathTransformerContext.js +422 -4
  37. package/src/XPathTransformer.js +1 -1
  38. package/src/XPathTransformerContext.js +462 -11
  39. package/src/index.js +9 -6
  40. package/tsconfig.json +5 -2
@@ -1,6 +1,18 @@
1
1
  import {JSONPath as jsonpath} from 'jsonpath-plus';
2
2
  import JSONPathTransformer from './JSONPathTransformer.js';
3
3
 
4
+ /**
5
+ * @typedef {number|string|{
6
+ * value?: number|string,
7
+ * count?: string,
8
+ * format?: string,
9
+ * groupingSeparator?: string,
10
+ * groupingSize?: number,
11
+ * lang?: string,
12
+ * letterValue?: string
13
+ * }} NumberValue
14
+ */
15
+
4
16
  /**
5
17
  * Sort spec types used by applyTemplates() and forEach().
6
18
  * @typedef {{
@@ -46,6 +58,11 @@ import JSONPathTransformer from './JSONPathTransformer.js';
46
58
  * @template [T = "json"]
47
59
  */
48
60
  class JSONPathTransformerContext {
61
+ /**
62
+ * Holds the current iteration state (for position calculations).
63
+ * @type {{ index?: number } | undefined}
64
+ */
65
+ iterationState;
49
66
  /**
50
67
  * @param {JSONPathTransformerContextConfig<T>} config
51
68
  * @param {import('./index.js').JSONPathTemplateObject<T>[]} templates - Array
@@ -627,6 +644,146 @@ class JSONPathTransformerContext {
627
644
  return this;
628
645
  }
629
646
 
647
+ /**
648
+ * Analyze a string with a regular expression, equivalent to
649
+ * xsl:analyze-string. Processes matching and non-matching substrings
650
+ * with separate callbacks.
651
+ * @param {string} str - The string to analyze
652
+ * @param {string|RegExp} regex - Regular expression to match against
653
+ * @param {{
654
+ * matchingSubstring?: (
655
+ * this: JSONPathTransformerContext<T>,
656
+ * substring: string,
657
+ * groups: string[],
658
+ * regexGroup: (n: number) => string
659
+ * ) => void,
660
+ * nonMatchingSubstring?: (
661
+ * this: JSONPathTransformerContext<T>,
662
+ * substring: string
663
+ * ) => void,
664
+ * flags?: string
665
+ * }} options - Options object
666
+ * @returns {this}
667
+ */
668
+ analyzeString (str, regex, options = {}) {
669
+ // Ensure we have a string
670
+ const inputString = String(str || '');
671
+
672
+ // If empty string, do nothing
673
+ if (inputString.length === 0) {
674
+ return this;
675
+ }
676
+
677
+ const {
678
+ matchingSubstring,
679
+ nonMatchingSubstring,
680
+ flags = ''
681
+ } = options;
682
+
683
+ // Convert regex to RegExp if it's a string
684
+ let regexObj;
685
+ if (typeof regex === 'string') {
686
+ // Ensure 'g' flag is present for global matching
687
+ const actualFlags = flags.includes('g') ? flags : flags + 'g';
688
+ regexObj = new RegExp(regex, actualFlags);
689
+ } else {
690
+ regexObj = regex;
691
+ // Ensure global flag is set
692
+ if (!regexObj.global) {
693
+ regexObj = new RegExp(
694
+ regexObj.source,
695
+ regexObj.flags + 'g'
696
+ );
697
+ }
698
+ }
699
+
700
+ // Check for zero-length matches (error condition in XSLT)
701
+ if (regexObj.test('')) {
702
+ throw new Error(
703
+ 'Regular expression matches zero-length string'
704
+ );
705
+ }
706
+
707
+ // Store captured groups for access during callback
708
+ /** @type {string[] | undefined} */
709
+ let currentCapturedGroups;
710
+
711
+ /**
712
+ * Get captured group by index.
713
+ * @param {number} groupNumber - Group index
714
+ * @returns {string} - Captured group or empty string
715
+ */
716
+ const getRegexGroup = (groupNumber) => {
717
+ if (!currentCapturedGroups ||
718
+ groupNumber < 0 ||
719
+ groupNumber >= currentCapturedGroups.length) {
720
+ return '';
721
+ }
722
+ return currentCapturedGroups[groupNumber] || '';
723
+ };
724
+
725
+ // Save previous context to restore later
726
+ const prevContext = this._contextObj;
727
+
728
+ let lastIndex = 0;
729
+ let match;
730
+
731
+ // Bind callbacks to this context
732
+ const boundMatchingSubstring = matchingSubstring
733
+ ? matchingSubstring.bind(this)
734
+ : undefined;
735
+ const boundNonMatchingSubstring = nonMatchingSubstring
736
+ ? nonMatchingSubstring.bind(this)
737
+ : undefined;
738
+
739
+ // Find all matches
740
+ while ((match = regexObj.exec(inputString)) !== null) {
741
+ // Process non-matching substring before this match
742
+ if (match.index > lastIndex) {
743
+ const nonMatchingStr = inputString.slice(lastIndex, match.index);
744
+ if (boundNonMatchingSubstring) {
745
+ this._contextObj = nonMatchingStr;
746
+ boundNonMatchingSubstring(nonMatchingStr);
747
+ }
748
+ }
749
+
750
+ // Process matching substring
751
+ if (boundMatchingSubstring) {
752
+ const matchingStr = match[0];
753
+ // Store captured groups: [full match, group1, group2, ...]
754
+ currentCapturedGroups = [...match];
755
+ this._contextObj = matchingStr;
756
+ boundMatchingSubstring(
757
+ matchingStr, currentCapturedGroups, getRegexGroup
758
+ );
759
+ currentCapturedGroups = undefined;
760
+ }
761
+
762
+ const {lastIndex: newLastIndex} = regexObj;
763
+ lastIndex = newLastIndex;
764
+
765
+ // Prevent infinite loop on zero-length matches (shouldn't happen
766
+ // due to earlier check, but defensive)
767
+ if (match.index === regexObj.lastIndex) {
768
+ regexObj.lastIndex++;
769
+ }
770
+ }
771
+
772
+ // Process final non-matching substring
773
+ if (lastIndex < inputString.length) {
774
+ const nonMatchingStr = inputString.slice(lastIndex);
775
+ if (boundNonMatchingSubstring) {
776
+ this._contextObj = nonMatchingStr;
777
+ boundNonMatchingSubstring(nonMatchingStr);
778
+ }
779
+ }
780
+
781
+ // Restore previous context
782
+ this._contextObj = prevContext;
783
+
784
+ return this;
785
+ }
786
+
630
787
  /**
631
788
  * Deep copy selection or current context when omitted.
632
789
  * @param {string} [select] - JSONPath selector
@@ -735,16 +892,277 @@ class JSONPathTransformerContext {
735
892
  }
736
893
 
737
894
  /**
738
- * Append a number to JSON output. Mirrors the joining transformer API so
739
- * templates can call `this.number()`.
740
- * @param {number} num - Number value to append
895
+ * Append a number to JSON output with xsl:number-like formatting.
896
+ * @param {NumberValue} num - Number value, "position()" string, or
897
+ * options object
741
898
  * @returns {this}
742
899
  */
743
900
  number (num) {
744
- this._getJoiningTransformer().number(num);
901
+ // Handle xsl:number-like functionality
902
+ if (typeof num === 'object' && num !== null) {
903
+ const opts = num;
904
+ let {value} = opts;
905
+
906
+ // Handle position() and hierarchical numbering
907
+ if (value === 'position()' || value === undefined) {
908
+ const {count} = opts;
909
+ // @ts-expect-error: dynamic property access
910
+ const level = opts.level || 'single';
911
+
912
+ switch (level) {
913
+ case 'single': {
914
+ value = this.calculatePosition(count);
915
+ // If count is set, simulate count by returning total items
916
+ // in current context
917
+ if (count) {
918
+ const arr = Array.isArray(this.get(count, true))
919
+ ? this.get(count, true)
920
+ /* c8 ignore next -- defensive: get(wrap) always returns array */
921
+ : [];
922
+ value = arr.length;
923
+ }
924
+
925
+ break;
926
+ }
927
+ case 'multiple': {
928
+ // Hierarchical numbering: get position for each ancestor up to root
929
+ const positions = [];
930
+ let state = /** @type {any} */ (this._config).iterationState;
931
+ while (state) {
932
+ // If count is set, use count for each ancestor if possible
933
+ if (count) {
934
+ const arr = Array.isArray(this.get(count, true))
935
+ ? this.get(count, true)
936
+ /* c8 ignore next -- defensive: get(wrap) returns array */
937
+ : [];
938
+ positions.unshift(arr.length);
939
+ } else {
940
+ positions.unshift(
941
+ state.index !== undefined
942
+ ? state.index + 1
943
+ /* c8 ignore next -- defensive: state always has index */
944
+ : 1
945
+ );
946
+ }
947
+ state = state.parentState;
948
+ }
949
+ value = positions.join('.');
950
+
951
+ break;
952
+ }
953
+ case 'any': {
954
+ // Count all matching items up to current
955
+ value = this.calculatePosition(count);
956
+
957
+ break;
958
+ }
959
+ // No default
960
+ }
961
+ }
962
+
963
+ // Determine format string and locale
964
+ let format = opts.format || '1';
965
+ const locale = opts.lang || 'en';
966
+ const {letterValue} = opts;
967
+
968
+ // If letterValue is 'alphabetic', force alphabetic format
969
+ if (letterValue === 'alphabetic') {
970
+ format = (opts.format && (/^[aA]$/v).test(opts.format)) ? opts.format : 'a';
971
+ }
972
+
973
+ // Ensure value is a number or string for formatting
974
+ let numValue = value;
975
+ // If value is undefined, fallback to opts.value
976
+ if (typeof numValue === 'undefined') {
977
+ numValue = opts.value;
978
+ }
979
+ if (typeof numValue === 'string') {
980
+ numValue = Number(numValue);
981
+ }
982
+ if (typeof numValue !== 'number' || Number.isNaN(numValue)) {
983
+ numValue = 1;
984
+ }
985
+ const formatted = this._formatNumber(
986
+ numValue,
987
+ format,
988
+ opts.groupingSeparator,
989
+ opts.groupingSize,
990
+ locale
991
+ );
992
+
993
+ // Output as string if formatted, otherwise as number
994
+ if (format && format !== '1') {
995
+ this._getJoiningTransformer().plainText(formatted);
996
+ } else {
997
+ this._getJoiningTransformer().number(Number(formatted));
998
+ }
999
+ } else if (num === 'position()') {
1000
+ // Simple position() call
1001
+ const pos = this.calculatePosition();
1002
+ this._getJoiningTransformer().number(pos);
1003
+ } else {
1004
+ // Simple number
1005
+ this._getJoiningTransformer().number(
1006
+ typeof num === 'string' ? Number(num) : num
1007
+ );
1008
+ }
745
1009
  return this;
746
1010
  }
747
1011
 
1012
+ /**
1013
+ * Calculate position in current iteration context.
1014
+ * @param {string} [count] - JSONPath expression to match
1015
+ * @returns {number}
1016
+ */
1017
+ calculatePosition (count) {
1018
+ // If count is provided, return the length of the matched array from
1019
+ // the root data
1020
+ if (count) {
1021
+ const result = jsonpath({
1022
+ path: count, json: this._origObj, resultType: 'value', wrap: true
1023
+ });
1024
+ if (Array.isArray(result)) {
1025
+ if (result.length === 0) {
1026
+ return 0;
1027
+ }
1028
+ // If the first item is an array, return its length
1029
+ if (Array.isArray(result[0])) {
1030
+ return result[0].length;
1031
+ }
1032
+ // Otherwise, return the number of matches
1033
+ return result.length;
1034
+ }
1035
+ /* c8 ignore next 3 -- defensive:
1036
+ jsonpath-plus with wrap:true always returns arrays */
1037
+ return 0;
1038
+ }
1039
+ // Get current index from iteration state
1040
+ const state = this.iterationState;
1041
+ if (state && typeof state.index === 'number') {
1042
+ return state.index + 1; // 1-indexed
1043
+ }
1044
+ return 1;
1045
+ }
1046
+
1047
+ /**
1048
+ * Format a number according to format string.
1049
+ * @param {number} num - Number to format
1050
+ * @param {string} format - Format string (1, a, A, i, I, 01, etc.)
1051
+ * @param {string} [groupingSeparator] - Separator for grouping
1052
+ * @param {number} [groupingSize] - Size of groups
1053
+ * @param {string} [locale]
1054
+ * @returns {string}
1055
+ */
1056
+ _formatNumber (num, format, groupingSeparator, groupingSize, locale = 'en') {
1057
+ if (Number.isNaN(num)) {
1058
+ return String(num);
1059
+ }
1060
+
1061
+ let result;
1062
+ const formatChar = format.charAt(0);
1063
+
1064
+ switch (formatChar) {
1065
+ case 'i': {
1066
+ result = this._toRoman(num).toLowerCase();
1067
+
1068
+ break;
1069
+ }
1070
+ case 'I': {
1071
+ result = this._toRoman(num);
1072
+
1073
+ break;
1074
+ }
1075
+ case 'a': {
1076
+ result = this._toAlphabetic(num, false);
1077
+
1078
+ break;
1079
+ }
1080
+ case 'A': {
1081
+ result = this._toAlphabetic(num, true);
1082
+
1083
+ break;
1084
+ }
1085
+ case '0': {
1086
+ const width = format.length;
1087
+ result = String(num).padStart(width, '0');
1088
+
1089
+ break;
1090
+ }
1091
+ default: {
1092
+ // Use Intl.NumberFormat for decimal formatting if grouping/locale
1093
+ // options are provided
1094
+ let options = {};
1095
+ if (groupingSeparator || groupingSize) {
1096
+ options = {
1097
+ useGrouping: true
1098
+ };
1099
+ }
1100
+
1101
+ try {
1102
+ result = new Intl.NumberFormat(locale, options).format(num);
1103
+ if (groupingSeparator) {
1104
+ result = result.replaceAll(',', groupingSeparator);
1105
+ }
1106
+ } catch (e) {
1107
+ result = String(num);
1108
+ }
1109
+ }
1110
+ }
1111
+ return result;
1112
+ }
1113
+
1114
+ /**
1115
+ * Convert number to Roman numerals.
1116
+ * @param {number} num - Number to convert (1-3999)
1117
+ * @returns {string}
1118
+ * @private
1119
+ */
1120
+ // eslint-disable-next-line class-methods-use-this -- Avoid for now
1121
+ _toRoman (num) {
1122
+ if (num < 1 || num > 3999) {
1123
+ return String(num);
1124
+ }
1125
+
1126
+ const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
1127
+ const syms = [
1128
+ 'M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'
1129
+ ];
1130
+
1131
+ let result = '';
1132
+ for (const [i, val] of vals.entries()) {
1133
+ while (num >= val) {
1134
+ result += syms[i];
1135
+ num -= val;
1136
+ }
1137
+ }
1138
+ return result;
1139
+ }
1140
+
1141
+ /**
1142
+ * Convert number to alphabetic sequence.
1143
+ * @param {number} num - Number to convert
1144
+ * @param {boolean} uppercase - Use uppercase letters
1145
+ * @returns {string}
1146
+ * @private
1147
+ */
1148
+ // eslint-disable-next-line class-methods-use-this -- Avoid for now
1149
+ _toAlphabetic (num, uppercase) {
1150
+ if (num < 1) {
1151
+ return String(num);
1152
+ }
1153
+
1154
+ let result = '';
1155
+ const base = uppercase ? 65 : 97; // 'A' or 'a'
1156
+
1157
+ while (num > 0) {
1158
+ num--; // Make 0-indexed
1159
+ result = String.fromCodePoint(base + (num % 26)) + result;
1160
+ num = Math.floor(num / 26);
1161
+ }
1162
+
1163
+ return result;
1164
+ }
1165
+
748
1166
  /**
749
1167
  * Append plain text directly to the output without escaping or JSON
750
1168
  * stringification. Mirrors the joining transformer API so templates can
@@ -6,7 +6,7 @@ import XPathTransformerContext from './XPathTransformerContext.js';
6
6
  * @property {boolean} [errorOnEqualPriority] Throw on equal priority
7
7
  * @property {import('./index.js').
8
8
  * XPathTemplateArray<T>} templates Template objects
9
- * @property {number} [xpathVersion] XPath version (1|2)
9
+ * @property {number} [xpathVersion] XPath version (1|2|3.1)
10
10
  */
11
11
 
12
12
  /**