@soleil-se/app-util 5.12.3 → 5.13.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file.
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
+ ## [5.13.0] - 2026-01-16
11
+
12
+ - Add comparator functions `localizedCompare` and `localizedCompareBy` to sort strings and objects
13
+ by a specific property using localized string comparison since String.localeCompare does not
14
+ work properly with nordic characters in Rhino.
15
+
10
16
  ## [5.12.3] - 2025-12-03
11
17
 
12
18
  - Even better type definitions for Svelte render functions.
package/common/index.d.ts CHANGED
@@ -79,3 +79,6 @@ export const isOffline: boolean;
79
79
  * @constant {boolean}
80
80
  */
81
81
  export const isOnline: boolean;
82
+ import { localizedCompare } from "./localized-compare";
83
+ import { localizedCompareBy } from "./localized-compare";
84
+ export { localizedCompare, localizedCompareBy };
package/common/index.js CHANGED
@@ -3,6 +3,7 @@ import app from '@sitevision/api/common/app';
3
3
 
4
4
  import { nativeRequire } from '../server';
5
5
  import getLegacyRouteUri from './legacy/getRouteUri';
6
+ import { localizedCompare, localizedCompareBy } from './localized-compare';
6
7
 
7
8
  /**
8
9
  * Regex for selecting leading slashes
@@ -150,7 +151,7 @@ export function getRouteUri(route = '', params = undefined) {
150
151
  */
151
152
  export function getViewUri(route = '', params = undefined) {
152
153
  if (!router?.getUrl) {
153
- console.warn('[@soleil-api/webapp-util] getViewUri requires router.getUrl support.');
154
+ console.warn('[@soleil-se/app-util] getViewUri requires router.getUrl support.');
154
155
  return undefined;
155
156
  }
156
157
 
@@ -183,3 +184,5 @@ export function setAppProps(data) {
183
184
  export function getAppProps(key) {
184
185
  return key ? appProps[key] : appProps;
185
186
  }
187
+
188
+ export { localizedCompare, localizedCompareBy };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Compares two strings in a localized manner, taking into account special characters.
3
+ * The order is defined as: a-z å ä æ ö ø (case-insensitive, with lowercase before uppercase).
4
+ * Characters not in this set are compared based on their Unicode code points after removing accents.
5
+ * @param {string} a - The first string to compare.
6
+ * @param {string} b - The second string to compare.
7
+ * @returns {number} Negative if a < b, positive if a > b, zero if equal.
8
+ */
9
+ export function localizedCompare(a: string, b: string): number;
10
+ /**
11
+ * Creates a comparator function for sorting objects by a specific property or properties.
12
+ * The property values are compared using localized string comparison.
13
+ * When given an array of properties, sorts by the first property, then by the second if equal, etc.
14
+ * @param {string|string[]} prop - The property name(s) to compare by.
15
+ * @returns {(a: Object, b: Object) => number} A comparator function that accepts two objects and returns their sort order.
16
+ * @example
17
+ * arr.sort(localizedCompareBy('title'))
18
+ * arr.sort(localizedCompareBy(['lastName', 'firstName']))
19
+ */
20
+ export function localizedCompareBy(prop: string | string[]): (a: any, b: any) => number;
@@ -0,0 +1,103 @@
1
+ const charOrder = 'abcdefghijklmnopqrstuvwxyzåäæöø';
2
+
3
+ function normalizeChar(char) {
4
+ // Remove accents from unknown characters (é → e, ñ → n)
5
+ return char.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
6
+ }
7
+
8
+ function getCharInfo(char) {
9
+ const lowerChar = char.toLowerCase();
10
+ let index = charOrder.indexOf(lowerChar);
11
+ let isAccented = false;
12
+
13
+ // If not found, try normalized version
14
+ if (index === -1) {
15
+ const normalized = normalizeChar(lowerChar);
16
+ index = charOrder.indexOf(normalized);
17
+
18
+ if (index !== -1) {
19
+ isAccented = true;
20
+ } else {
21
+ // Completely unknown character
22
+ index = charOrder.length + char.charCodeAt(0);
23
+ }
24
+ }
25
+
26
+ return {
27
+ baseIndex: index,
28
+ isAccented,
29
+ isLower: char === char.toLowerCase() && char !== char.toUpperCase(),
30
+ accentCode: isAccented ? char.normalize('NFD').charCodeAt(1) || 0 : 0,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Compares two strings in a localized manner, taking into account special characters.
36
+ * The order is defined as: a-z å ä æ ö ø (case-insensitive, with lowercase before uppercase).
37
+ * Characters not in this set are compared based on their Unicode code points after removing accents.
38
+ * @param {string} a - The first string to compare.
39
+ * @param {string} b - The second string to compare.
40
+ * @returns {number} Negative if a < b, positive if a > b, zero if equal.
41
+ */
42
+ export function localizedCompare(a, b) {
43
+ // Handle null/undefined
44
+ if (a == null) return b == null ? 0 : -1;
45
+ if (b == null) return 1;
46
+
47
+ // Ensure strings
48
+ const strA = String(a);
49
+ const strB = String(b);
50
+
51
+ const minLength = Math.min(strA.length, strB.length);
52
+
53
+ for (let i = 0; i < minLength; i += 1) {
54
+ const infoA = getCharInfo(strA[i]);
55
+ const infoB = getCharInfo(strB[i]);
56
+
57
+ // 1. Compare base character (case and accent insensitive)
58
+ if (infoA.baseIndex !== infoB.baseIndex) {
59
+ return infoA.baseIndex - infoB.baseIndex;
60
+ }
61
+
62
+ // 2. Non-accented before accented (e.g., 'e' < 'é')
63
+ if (infoA.isAccented !== infoB.isAccented) {
64
+ return infoA.isAccented ? 1 : -1;
65
+ }
66
+
67
+ // 3. If both accented, compare accent types (é vs è)
68
+ if (infoA.isAccented && infoA.accentCode !== infoB.accentCode) {
69
+ return infoA.accentCode - infoB.accentCode;
70
+ }
71
+
72
+ // 4. Lowercase before uppercase (e.g., 'e' < 'E')
73
+ if (infoA.isLower !== infoB.isLower) {
74
+ return infoA.isLower ? -1 : 1;
75
+ }
76
+ }
77
+
78
+ return strA.length - strB.length;
79
+ }
80
+
81
+ /**
82
+ * Creates a comparator function for sorting objects by a specific property or properties.
83
+ * The property values are compared using localized string comparison.
84
+ * When given an array of properties, sorts by the first property, then by the second if equal, etc.
85
+ * @param {string|string[]} prop - The property name(s) to compare by.
86
+ * @returns {(a: Object, b: Object) => number} A comparator function that accepts two objects and returns their sort order.
87
+ * @example
88
+ * arr.sort(localizedCompareBy('title'))
89
+ * arr.sort(localizedCompareBy(['lastName', 'firstName']))
90
+ */
91
+ export function localizedCompareBy(prop) {
92
+ const props = Array.isArray(prop) ? prop : [prop];
93
+
94
+ return (a, b) => {
95
+ for (let i = 0; i < props.length; i += 1) {
96
+ const result = localizedCompare(a[props[i]], b[props[i]]);
97
+ if (result !== 0) {
98
+ return result;
99
+ }
100
+ }
101
+ return 0;
102
+ };
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleil-se/app-util",
3
- "version": "5.12.3",
3
+ "version": "5.13.0",
4
4
  "description": "Utility functions for WebApps, RESTApps and Widgets in Sitevision.",
5
5
  "main": "./common/index.js",
6
6
  "author": "Soleil AB",
@@ -25,5 +25,5 @@
25
25
  "scripts": {
26
26
  "create-type-definitions": "node ../../utils/createTypeDefinitions.js ./common/index.js ./client/index.js ./client/svelte/index.js ./client/svelte/3/index.js ./client/svelte/4/index.js ./client/svelte/5/index.js ./server/index.js ./server/svelte/index.js ./server/svelte/3/index.js ./server/svelte/4/index.js ./server/svelte/5/index.js ./server/app-data/index.js ./server/global-app-data/index.js"
27
27
  },
28
- "gitHead": "9b1f44728b2ba84db68acdad31699822067ced13"
28
+ "gitHead": "3c3ef92d604eeed89887a94964edf2146e2a746a"
29
29
  }
@@ -1,6 +0,0 @@
1
- /**
2
- * Require a module natively.
3
- * @param {string} module
4
- * @returns any
5
- */
6
- export function nativeRequire(module: string): any;