@aerogel/core 0.0.0-next.71f28064caa2ea968f0e99396b672de218176260 → 0.0.0-next.7eac6e53dcf9fe93d7abdc4bc97e5ce69970efa7

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aerogel/core",
3
3
  "description": "The Lightest Solid",
4
- "version": "0.0.0-next.71f28064caa2ea968f0e99396b672de218176260",
4
+ "version": "0.0.0-next.7eac6e53dcf9fe93d7abdc4bc97e5ce69970efa7",
5
5
  "main": "dist/aerogel-core.cjs.js",
6
6
  "module": "dist/aerogel-core.esm.js",
7
7
  "types": "dist/aerogel-core.d.ts",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@headlessui/vue": "^1.7.14",
38
- "@noeldemartin/utils": "0.5.1-next.49cc6c9b4a20930cbf922a949135981791acc5c3",
38
+ "@noeldemartin/utils": "0.5.1-next.4fd89de2cbde6c7e1cfa4d5f9bdac234e9cd3d98",
39
39
  "dompurify": "^3.0.3",
40
40
  "marked": "^5.0.4",
41
41
  "pinia": "^2.1.6",
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div aria-live="assertive" class="z-60 pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:p-6">
2
+ <div aria-live="assertive" class="pointer-events-none fixed inset-0 z-50 flex items-end px-4 py-6 sm:p-6">
3
3
  <div class="flex w-full flex-col items-end space-y-4">
4
4
  <component
5
5
  :is="snackbar.component"
@@ -1,26 +1,25 @@
1
1
  <template>
2
- <form @submit.prevent="submit">
2
+ <form @submit.prevent="form?.submit()">
3
3
  <slot />
4
4
  </form>
5
5
  </template>
6
6
 
7
7
  <script setup lang="ts">
8
- import { provide } from 'vue';
8
+ import { provide, watchEffect } from 'vue';
9
9
 
10
10
  import { objectProp } from '@/utils/vue';
11
11
  import type Form from '@/forms/Form';
12
12
 
13
+ let offSubmit: (() => void) | undefined;
13
14
  const props = defineProps({ form: objectProp<Form>() });
14
-
15
15
  const emit = defineEmits<{ submit: [] }>();
16
16
 
17
- provide('form', props.form);
17
+ watchEffect((onCleanup) => {
18
+ offSubmit?.();
19
+ offSubmit = props.form?.on('submit', () => emit('submit'));
18
20
 
19
- function submit() {
20
- if (props.form && !props.form.submit()) {
21
- return;
22
- }
21
+ onCleanup(() => offSubmit?.());
22
+ });
23
23
 
24
- emit('submit');
25
- }
24
+ provide('form', props.form);
26
25
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <component :is="component.tag" v-bind="component.props">
2
+ <component :is="component.as" v-bind="component.props">
3
3
  <slot />
4
4
  </component>
5
5
  </template>
@@ -11,6 +11,7 @@ import { objectWithoutEmpty } from '@noeldemartin/utils';
11
11
  import { booleanProp, objectProp, stringProp } from '@/utils/vue';
12
12
 
13
13
  const props = defineProps({
14
+ as: objectProp(),
14
15
  href: stringProp(),
15
16
  url: stringProp(),
16
17
  route: stringProp(),
@@ -20,9 +21,13 @@ const props = defineProps({
20
21
  });
21
22
 
22
23
  const component = computed(() => {
24
+ if (props.as) {
25
+ return { as: props.as, props: {} };
26
+ }
27
+
23
28
  if (props.route) {
24
29
  return {
25
- tag: 'router-link',
30
+ as: 'router-link',
26
31
  props: {
27
32
  to: objectWithoutEmpty({
28
33
  name: props.route,
@@ -35,7 +40,7 @@ const component = computed(() => {
35
40
 
36
41
  if (props.href || props.url) {
37
42
  return {
38
- tag: 'a',
43
+ as: 'a',
39
44
  props: {
40
45
  target: '_blank',
41
46
  href: props.href || props.url,
@@ -44,7 +49,7 @@ const component = computed(() => {
44
49
  }
45
50
 
46
51
  return {
47
- tag: 'button',
52
+ as: 'button',
48
53
  props: { type: props.submit ? 'submit' : 'button' },
49
54
  };
50
55
  });
@@ -2,22 +2,23 @@ import type { ComputedRef, DeepReadonly, ExtractPropTypes, Ref } from 'vue';
2
2
 
3
3
  import { mixedProp, stringProp } from '@/utils';
4
4
  import { extractComponentProps } from '@/components/utils';
5
+ import type { FormFieldValue } from '@/forms/Form';
5
6
 
6
7
  export interface IAGHeadlessInput {
7
8
  id: string;
8
9
  name: ComputedRef<string | null>;
9
10
  label: ComputedRef<string | null>;
10
11
  description: ComputedRef<string | boolean | null>;
11
- value: ComputedRef<string | number | boolean | null>;
12
+ value: ComputedRef<FormFieldValue | null>;
12
13
  errors: DeepReadonly<Ref<string[] | null>>;
13
- update(value: string | number | boolean | null): void;
14
+ update(value: FormFieldValue | null): void;
14
15
  }
15
16
 
16
17
  export const inputProps = {
17
18
  name: stringProp(),
18
19
  label: stringProp(),
19
20
  description: stringProp(),
20
- modelValue: mixedProp<string | number | boolean>([String, Number, Boolean]),
21
+ modelValue: mixedProp<FormFieldValue>([String, Number, Boolean]),
21
22
  };
22
23
 
23
24
  export function useInputProps(): typeof inputProps {
@@ -35,7 +35,7 @@ const api: IAGHeadlessInput = {
35
35
  description: computed(() => props.description),
36
36
  value: computed(() => {
37
37
  if (form && props.name) {
38
- return form.getFieldValue(props.name) as string | number | boolean | null;
38
+ return form.getFieldValue(props.name);
39
39
  }
40
40
 
41
41
  return props.modelValue;
@@ -4,7 +4,6 @@
4
4
  ref="$input"
5
5
  :name="name"
6
6
  :type="type"
7
- :value="value"
8
7
  :aria-invalid="input.errors ? 'true' : 'false'"
9
8
  :aria-describedby="
10
9
  input.errors ? `${input.id}-error` : input.description ? `${input.id}-description` : undefined
@@ -15,10 +14,11 @@
15
14
  </template>
16
15
 
17
16
  <script setup lang="ts">
18
- import { computed, ref } from 'vue';
17
+ import { computed, ref, watchEffect } from 'vue';
19
18
 
20
19
  import { injectReactiveOrFail, stringProp } from '@/utils';
21
20
  import type { IAGHeadlessInput } from '@/components/headless/forms/AGHeadlessInput';
21
+ import type { FormFieldValue } from '@/forms/Form';
22
22
 
23
23
  import { onFormFocus } from './composition';
24
24
 
@@ -46,8 +46,36 @@ function update() {
46
46
  return;
47
47
  }
48
48
 
49
- input.update(props.type === 'checkbox' ? $input.value.checked : $input.value.value);
49
+ input.update(getValue());
50
+ }
51
+
52
+ function getValue(): FormFieldValue | null {
53
+ if (!$input.value) {
54
+ return null;
55
+ }
56
+
57
+ switch (props.type) {
58
+ case 'checkbox':
59
+ return $input.value.checked;
60
+ case 'date':
61
+ return $input.value.valueAsDate;
62
+ default:
63
+ return $input.value.value;
64
+ }
50
65
  }
51
66
 
52
67
  onFormFocus(input, () => $input.value?.focus());
68
+ watchEffect(() => {
69
+ if (!$input.value) {
70
+ return;
71
+ }
72
+
73
+ if (props.type === 'date') {
74
+ $input.value.valueAsDate = value.value as Date;
75
+
76
+ return;
77
+ }
78
+
79
+ $input.value.value = value.value as string;
80
+ });
53
81
  </script>
@@ -8,6 +8,7 @@ export { default as AGHeadlessInputDescription } from './AGHeadlessInputDescript
8
8
  export { default as AGHeadlessInputError } from './AGHeadlessInputError.vue';
9
9
  export { default as AGHeadlessInputInput } from './AGHeadlessInputInput.vue';
10
10
  export { default as AGHeadlessInputLabel } from './AGHeadlessInputLabel.vue';
11
+ export { default as AGHeadlessInputTextArea } from './AGHeadlessInputTextArea.vue';
11
12
  export { default as AGHeadlessSelect } from './AGHeadlessSelect.vue';
12
13
  export { default as AGHeadlessSelectButton } from './AGHeadlessSelectButton.vue';
13
14
  export { default as AGHeadlessSelectError } from './AGHeadlessSelectError.vue';
@@ -6,14 +6,14 @@
6
6
  import { computed, h, useAttrs } from 'vue';
7
7
 
8
8
  import { renderMarkdown } from '@/utils/markdown';
9
- import { booleanProp, objectProp, stringProp } from '@/utils/vue';
9
+ import { booleanProp, mixedProp, stringProp } from '@/utils/vue';
10
10
  import { translate } from '@/lang';
11
11
 
12
12
  const props = defineProps({
13
13
  as: stringProp(),
14
14
  inline: booleanProp(),
15
15
  langKey: stringProp(),
16
- langParams: objectProp<Record<string, unknown>>(),
16
+ langParams: mixedProp<number | Record<string, unknown>>(),
17
17
  text: stringProp(),
18
18
  });
19
19
 
@@ -34,8 +34,8 @@ const html = computed(() => {
34
34
  });
35
35
  const root = () =>
36
36
  h(props.as ?? (props.inline ? 'span' : 'div'), {
37
- class: props.inline ? '' : 'prose',
38
37
  innerHTML: html.value,
39
38
  ...attrs,
39
+ class: `${attrs.class ?? ''} ${props.inline ? '' : 'prose'}`,
40
40
  });
41
41
  </script>
@@ -1,18 +1,24 @@
1
1
  import { computed } from 'vue';
2
2
  import type { ExtractPropTypes } from 'vue';
3
- import type { ObjectWithoutEmpty } from '@noeldemartin/utils';
3
+ import type { ObjectWithoutEmpty, SubPartial } from '@noeldemartin/utils';
4
4
 
5
- import { requiredStringProp, stringProp } from '@/utils';
5
+ import { Colors } from '@/components/constants';
6
+ import { enumProp, requiredStringProp, stringProp } from '@/utils';
6
7
  import { translateWithDefault } from '@/lang';
7
8
 
8
9
  export const confirmModalProps = {
9
10
  title: stringProp(),
10
11
  message: requiredStringProp(),
11
12
  acceptText: stringProp(),
13
+ acceptColor: enumProp(Colors, Colors.Primary),
12
14
  cancelText: stringProp(),
15
+ cancelColor: enumProp(Colors, Colors.Clear),
13
16
  };
14
17
 
15
- export type AGConfirmModalProps = ObjectWithoutEmpty<ExtractPropTypes<typeof confirmModalProps>>;
18
+ export type AGConfirmModalProps = SubPartial<
19
+ ObjectWithoutEmpty<ExtractPropTypes<typeof confirmModalProps>>,
20
+ 'acceptColor' | 'cancelColor'
21
+ >;
16
22
 
17
23
  export function useConfirmModalProps(): typeof confirmModalProps {
18
24
  return confirmModalProps;
@@ -3,10 +3,10 @@
3
3
  <AGMarkdown :text="message" />
4
4
 
5
5
  <div class="mt-2 flex flex-row-reverse gap-2">
6
- <AGButton @click="close(true)">
6
+ <AGButton :color="acceptColor" @click="close(true)">
7
7
  {{ renderedAcceptText }}
8
8
  </AGButton>
9
- <AGButton color="secondary" @click="close()">
9
+ <AGButton :color="cancelColor" @click="close()">
10
10
  {{ renderedCancelText }}
11
11
  </AGButton>
12
12
  </div>
@@ -1,8 +1,9 @@
1
1
  import { computed } from 'vue';
2
2
  import type { ExtractPropTypes } from 'vue';
3
- import type { ObjectWithoutEmpty } from '@noeldemartin/utils';
3
+ import type { ObjectWithoutEmpty, SubPartial } from '@noeldemartin/utils';
4
4
 
5
- import { requiredStringProp, stringProp } from '@/utils';
5
+ import { Colors } from '@/components/constants';
6
+ import { enumProp, requiredStringProp, stringProp } from '@/utils';
6
7
  import { translateWithDefault } from '@/lang';
7
8
 
8
9
  export const promptModalProps = {
@@ -12,10 +13,15 @@ export const promptModalProps = {
12
13
  defaultValue: stringProp(),
13
14
  placeholder: stringProp(),
14
15
  acceptText: stringProp(),
16
+ acceptColor: enumProp(Colors, Colors.Primary),
15
17
  cancelText: stringProp(),
18
+ cancelColor: enumProp(Colors, Colors.Clear),
16
19
  };
17
20
 
18
- export type AGPromptModalProps = ObjectWithoutEmpty<ExtractPropTypes<typeof promptModalProps>>;
21
+ export type AGPromptModalProps = SubPartial<
22
+ ObjectWithoutEmpty<ExtractPropTypes<typeof promptModalProps>>,
23
+ 'acceptColor' | 'cancelColor'
24
+ >;
19
25
 
20
26
  export function usePromptModalProps(): typeof promptModalProps {
21
27
  return promptModalProps;
@@ -6,10 +6,10 @@
6
6
  <AGInput name="draft" :placeholder="placeholder" :label="label" />
7
7
 
8
8
  <div class="mt-2 flex flex-row-reverse gap-2">
9
- <AGButton submit>
9
+ <AGButton :color="acceptColor" submit>
10
10
  {{ renderedAcceptText }}
11
11
  </AGButton>
12
- <AGButton color="secondary" @click="close()">
12
+ <AGButton :color="cancelColor" @click="close()">
13
13
  {{ renderedCancelText }}
14
14
  </AGButton>
15
15
  </div>
package/src/forms/Form.ts CHANGED
@@ -8,6 +8,7 @@ export const FormFieldTypes = {
8
8
  Number: 'number',
9
9
  Boolean: 'boolean',
10
10
  Object: 'object',
11
+ Date: 'date',
11
12
  } as const;
12
13
 
13
14
  export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
@@ -40,11 +41,14 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
40
41
  ? boolean
41
42
  : TType extends typeof FormFieldTypes.Object
42
43
  ? object
44
+ : TType extends typeof FormFieldTypes.Date
45
+ ? Date
43
46
  : never;
44
47
 
45
48
  const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
46
49
 
47
- export type FormListener = (input: string) => unknown;
50
+ export type SubmitFormListener = () => unknown;
51
+ export type FocusFormListener = (input: string) => unknown;
48
52
 
49
53
  export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
50
54
 
@@ -54,7 +58,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
54
58
  private _data: FormData<Fields>;
55
59
  private _submitted: Ref<boolean>;
56
60
  private _errors: FormErrors<Fields>;
57
- private _listeners: Record<string, FormListener[]> = {};
61
+ private _listeners: { focus?: FocusFormListener[]; submit?: SubmitFormListener[] } = {};
58
62
 
59
63
  constructor(fields: Fields) {
60
64
  super();
@@ -92,6 +96,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
92
96
  return this._data[field] as unknown as GetFormFieldValue<Fields[T]['type']>;
93
97
  }
94
98
 
99
+ public data(): FormData<Fields> {
100
+ return { ...this._data };
101
+ }
102
+
95
103
  public validate(): boolean {
96
104
  const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
97
105
  formErrors[name] = this.getFieldErrors(name, definition);
@@ -114,17 +122,28 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
114
122
  public submit(): boolean {
115
123
  this._submitted.value = true;
116
124
 
117
- return this.validate();
125
+ const valid = this.validate();
126
+
127
+ valid && this._listeners['submit']?.forEach((listener) => listener());
128
+
129
+ return valid;
118
130
  }
119
131
 
120
- public on(event: string, listener: FormListener): () => void {
132
+ public on(event: 'focus', listener: FocusFormListener): () => void;
133
+ public on(event: 'submit', listener: SubmitFormListener): () => void;
134
+ public on(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): () => void {
121
135
  this._listeners[event] ??= [];
122
- this._listeners[event]?.push(listener);
123
136
 
124
- return () => this.off(event, listener);
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ this._listeners[event]?.push(listener as any);
139
+
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ return () => this.off(event as any, listener);
125
142
  }
126
143
 
127
- public off(event: string, listener: FormListener): void {
144
+ public off(event: 'focus', listener: FocusFormListener): void;
145
+ public off(event: 'submit', listener: SubmitFormListener): void;
146
+ public off(event: 'focus' | 'submit', listener: FocusFormListener | SubmitFormListener): void {
128
147
  arrayRemove(this._listeners[event] ?? [], listener);
129
148
  }
130
149
 
@@ -1,3 +1,4 @@
1
1
  export * from './Form';
2
2
  export * from './composition';
3
3
  export * from './utils';
4
+ export { default as Form } from './Form';
@@ -8,6 +8,13 @@ export function booleanInput(defaultValue?: boolean): FormFieldDefinition<typeof
8
8
  };
9
9
  }
10
10
 
11
+ export function dateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
12
+ return {
13
+ default: defaultValue,
14
+ type: FormFieldTypes.Date,
15
+ };
16
+ }
17
+
11
18
  export function requiredBooleanInput(
12
19
  defaultValue?: boolean,
13
20
  ): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
@@ -18,6 +25,14 @@ export function requiredBooleanInput(
18
25
  };
19
26
  }
20
27
 
28
+ export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
29
+ return {
30
+ default: defaultValue,
31
+ type: FormFieldTypes.Date,
32
+ rules: 'required',
33
+ };
34
+ }
35
+
21
36
  export function requiredNumberInput(
22
37
  defaultValue?: number,
23
38
  ): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
package/src/lang/Lang.ts CHANGED
@@ -4,8 +4,8 @@ import App from '@/services/App';
4
4
  import Service from '@/services/Service';
5
5
 
6
6
  export interface LangProvider {
7
- translate(key: string, parameters?: Record<string, unknown>): string;
8
- translateWithDefault(key: string, defaultMessage: string, parameters?: Record<string, unknown>): string;
7
+ translate(key: string, parameters?: Record<string, unknown> | number): string;
8
+ translateWithDefault(key: string, defaultMessage: string, parameters?: Record<string, unknown> | number): string;
9
9
  }
10
10
 
11
11
  export class LangService extends Service {
@@ -35,11 +35,15 @@ export class LangService extends Service {
35
35
  this.provider = provider;
36
36
  }
37
37
 
38
- public translate(key: string, parameters?: Record<string, unknown>): string {
38
+ public translate(key: string, parameters?: Record<string, unknown> | number): string {
39
39
  return this.provider.translate(key, parameters) ?? key;
40
40
  }
41
41
 
42
- public translateWithDefault(key: string, defaultMessage: string, parameters: Record<string, unknown> = {}): string {
42
+ public translateWithDefault(
43
+ key: string,
44
+ defaultMessage: string,
45
+ parameters: Record<string, unknown> | number = {},
46
+ ): string {
43
47
  return this.provider.translateWithDefault(key, defaultMessage, parameters);
44
48
  }
45
49
 
@@ -0,0 +1,43 @@
1
+ import { PromisedValue, facade, tap } from '@noeldemartin/utils';
2
+
3
+ import Service from '@/services/Service';
4
+
5
+ export class CacheService extends Service {
6
+
7
+ private cache?: PromisedValue<Cache> = undefined;
8
+
9
+ public async get(url: string): Promise<Response | null> {
10
+ const cache = await this.open();
11
+ const response = await cache.match(url);
12
+
13
+ return response ?? null;
14
+ }
15
+
16
+ public async store(url: string, response: Response): Promise<void> {
17
+ const cache = await this.open();
18
+
19
+ await cache.put(url, response);
20
+ }
21
+
22
+ public async replace(url: string, response: Response): Promise<void> {
23
+ const cache = await this.open();
24
+ const keys = await cache.keys(url);
25
+
26
+ if (keys.length === 0) {
27
+ return;
28
+ }
29
+
30
+ await cache.put(url, response);
31
+ }
32
+
33
+ protected async open(): Promise<Cache> {
34
+ return (this.cache =
35
+ this.cache ??
36
+ tap(new PromisedValue<Cache>(), (cache) => {
37
+ caches.open('app').then((instance) => cache.resolve(instance));
38
+ }));
39
+ }
40
+
41
+ }
42
+
43
+ export default facade(CacheService);
@@ -3,16 +3,18 @@ import type { App as VueApp } from 'vue';
3
3
  import { definePlugin } from '@/plugins';
4
4
 
5
5
  import App from './App';
6
+ import Cache from './Cache';
6
7
  import Events from './Events';
7
8
  import Service from './Service';
8
9
  import { getPiniaStore } from './store';
9
10
 
10
11
  export * from './App';
12
+ export * from './Cache';
11
13
  export * from './Events';
12
14
  export * from './Service';
13
15
  export * from './store';
14
16
 
15
- export { App, Events, Service };
17
+ export { App, Cache, Events, Service };
16
18
 
17
19
  const defaultServices = {
18
20
  $app: App,
package/src/ui/UI.ts CHANGED
@@ -4,6 +4,7 @@ import type { Component } from 'vue';
4
4
  import type { ObjectValues } from '@noeldemartin/utils';
5
5
 
6
6
  import Events from '@/services/Events';
7
+ import type { Color } from '@/components/constants';
7
8
  import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
8
9
  import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps, AGPromptModalProps } from '@/components';
9
10
 
@@ -34,7 +35,9 @@ export type UIComponent = ObjectValues<typeof UIComponents>;
34
35
 
35
36
  export interface ConfirmOptions {
36
37
  acceptText?: string;
38
+ acceptColor?: Color;
37
39
  cancelText?: string;
40
+ cancelColor?: Color;
38
41
  }
39
42
 
40
43
  export interface PromptOptions {
@@ -42,7 +45,10 @@ export interface PromptOptions {
42
45
  defaultValue?: string;
43
46
  placeholder?: string;
44
47
  acceptText?: string;
48
+ acceptColor?: Color;
45
49
  cancelText?: string;
50
+ cancelColor?: Color;
51
+ trim?: boolean;
46
52
  }
47
53
 
48
54
  export interface ShowSnackbarOptions {
@@ -115,6 +121,7 @@ export class UIService extends Service {
115
121
  messageOrOptions?: string | PromptOptions,
116
122
  options?: PromptOptions,
117
123
  ): Promise<string | null> {
124
+ const trim = options?.trim ?? true;
118
125
  const getProperties = (): AGPromptModalProps => {
119
126
  if (typeof messageOrOptions !== 'string') {
120
127
  return {
@@ -134,7 +141,8 @@ export class UIService extends Service {
134
141
  this.requireComponent(UIComponents.PromptModal),
135
142
  getProperties(),
136
143
  );
137
- const result = await modal.beforeClose;
144
+ const rawResult = await modal.beforeClose;
145
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
138
146
 
139
147
  return result ?? null;
140
148
  }