@soleil-se/app-util 5.12.2 → 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,16 @@ 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
+
16
+ ## [5.12.3] - 2025-12-03
17
+
18
+ - Even better type definitions for Svelte render functions.
19
+
10
20
  ## [5.12.2] - 2025-12-03
11
21
 
12
22
  - Better type definitions for Svelte render functions.
@@ -1,9 +1,9 @@
1
1
  /// <reference types="svelte" />
2
2
  /**
3
- * @template {Record<string, unknown>} TProps
3
+ * @template {import('svelte').Component<any, any>} TComponent
4
4
  * @typedef {object} RenderSettings
5
5
  * @property {HTMLElement} [target] Target where app should be mounted.
6
- * @property {TProps} [props] Root component props.
6
+ * @property {import('svelte').ComponentProps<TComponent>} [props] Root component props.
7
7
  * @property {boolean} [hydrate=target.hasChildNodes()] Instructs Svelte to upgrade existing DOM
8
8
  * (usually from server-side rendering) rather than creating new elements. By default the app will
9
9
  * hydrate when the target has any child nodes.
@@ -12,12 +12,12 @@
12
12
  */
13
13
  /**
14
14
  * Renders a client side Svelte application.
15
- * @template {Record<string, unknown>} [TProps=Record<string, unknown>]
16
- * @param {import('svelte').Component<TProps>} App Svelte app root component.
17
- * @param {RenderSettings<TProps>} [settings={}] Settings object.
15
+ * @template {import('svelte').Component<any, any>} TComponent
16
+ * @param {TComponent} App Svelte app root component.
17
+ * @param {RenderSettings<TComponent>} [settings={}] Settings object.
18
18
  */
19
- export function render<TProps extends Record<string, unknown> = Record<string, unknown>>(App: import("svelte").Component<TProps, any, string>, { target, props, hydrate, intro, }?: RenderSettings<TProps>): void;
20
- export type RenderSettings<TProps extends Record<string, unknown>> = {
19
+ export function render<TComponent extends import("svelte").Component<any, any, string>>(App: TComponent, { target, props, hydrate, intro, }?: RenderSettings<TComponent>): void;
20
+ export type RenderSettings<TComponent extends import("svelte").Component<any, any, string>> = {
21
21
  /**
22
22
  * Target where app should be mounted.
23
23
  */
@@ -25,7 +25,7 @@ export type RenderSettings<TProps extends Record<string, unknown>> = {
25
25
  /**
26
26
  * Root component props.
27
27
  */
28
- props?: TProps;
28
+ props?: import('svelte').ComponentProps<TComponent>;
29
29
  /**
30
30
  * Instructs Svelte to upgrade existing DOM
31
31
  * (usually from server-side rendering) rather than creating new elements. By default the app will
@@ -4,10 +4,10 @@ import { mount as svelteMount, hydrate as svelteHydrate } from 'svelte';
4
4
  import { setAppProps } from '../../../common';
5
5
 
6
6
  /**
7
- * @template {Record<string, unknown>} TProps
7
+ * @template {import('svelte').Component<any, any>} TComponent
8
8
  * @typedef {object} RenderSettings
9
9
  * @property {HTMLElement} [target] Target where app should be mounted.
10
- * @property {TProps} [props] Root component props.
10
+ * @property {import('svelte').ComponentProps<TComponent>} [props] Root component props.
11
11
  * @property {boolean} [hydrate=target.hasChildNodes()] Instructs Svelte to upgrade existing DOM
12
12
  * (usually from server-side rendering) rather than creating new elements. By default the app will
13
13
  * hydrate when the target has any child nodes.
@@ -17,9 +17,9 @@ import { setAppProps } from '../../../common';
17
17
 
18
18
  /**
19
19
  * Renders a client side Svelte application.
20
- * @template {Record<string, unknown>} [TProps=Record<string, unknown>]
21
- * @param {import('svelte').Component<TProps>} App Svelte app root component.
22
- * @param {RenderSettings<TProps>} [settings={}] Settings object.
20
+ * @template {import('svelte').Component<any, any>} TComponent
21
+ * @param {TComponent} App Svelte app root component.
22
+ * @param {RenderSettings<TComponent>} [settings={}] Settings object.
23
23
  */
24
24
  export function render(App, {
25
25
  target,
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.2",
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": "090c22c4eee4dc2f68096f8f50cca8015a91acf7"
28
+ "gitHead": "3c3ef92d604eeed89887a94964edf2146e2a746a"
29
29
  }
@@ -1,9 +1,9 @@
1
1
  /// <reference types="svelte" />
2
2
  /**
3
3
  * Returns HTML for a server rendered Svelte app.
4
- * @template {Record<string, unknown>} [TProps=Record<string, unknown>]
5
- * @param {import('svelte').Component<TProps>} App Svelte component that is root of app.
6
- * @param {TProps} props Props passed to root component.
4
+ * @template {import('svelte').Component<any, any>} TComponent
5
+ * @param {TComponent} App Svelte component that is root of app.
6
+ * @param {import('svelte').ComponentProps<TComponent>} props Props passed to root component.
7
7
  * @return {string} HTML for the server rendered app.
8
8
  */
9
- export function render<TProps extends Record<string, unknown> = Record<string, unknown>>(App: import("svelte").Component<TProps, any, string>, props: TProps): string;
9
+ export function render<TComponent extends import("svelte").Component<any, any, string>>(App: TComponent, props: import("svelte").ComponentProps<TComponent>): string;
@@ -3,9 +3,9 @@ import { appId, setAppProps } from '../../../common';
3
3
 
4
4
  /**
5
5
  * Returns HTML for a server rendered Svelte app.
6
- * @template {Record<string, unknown>} [TProps=Record<string, unknown>]
7
- * @param {import('svelte').Component<TProps>} App Svelte component that is root of app.
8
- * @param {TProps} props Props passed to root component.
6
+ * @template {import('svelte').Component<any, any>} TComponent
7
+ * @param {TComponent} App Svelte component that is root of app.
8
+ * @param {import('svelte').ComponentProps<TComponent>} props Props passed to root component.
9
9
  * @return {string} HTML for the server rendered app.
10
10
  */
11
11
  export function render(App, props) {
@@ -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;