spooder 4.1.0 → 4.2.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/README.md CHANGED
@@ -431,8 +431,7 @@ In addition to the information provided by the developer, `spooder` also include
431
431
  - [`caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>`](#api-error-handling-caution)
432
432
  - [`panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>`](#api-error-handling-panic)
433
433
  - [API > Content](#api-content)
434
- - [`template_sub(template: string, replacements: Record<string, string>): string`](#api-content-template-sub)
435
- - [`template_sub_file(template_file: string, replacements: Record<string, string>): Promise<string>`](#api-content-template-sub-file)
434
+ - [`parse_template(template: string, replacements: Record<string, string>): string`](#api-content-parse-template)
436
435
  - [`generate_hash_subs(length: number, prefix: string): Promise<Record<string, string>>`](#api-content-generate-hash-subs)
437
436
  - [`apply_range(file: BunFile, request: Request): HandlerReturnType`](#api-content-apply-range)
438
437
  - [API > State Management](#api-state-management)
@@ -607,7 +606,7 @@ server.default((req, status_code) => {
607
606
  Register a handler for uncaught errors.
608
607
 
609
608
  > [!NOTE]
610
- > This handler does not accept asynchronous functions and must return a `Response` object.
609
+ > Unlike other handlers, this should only return `Response` or `Promise<Response>`.
611
610
  ```ts
612
611
  server.error((err, req, url) => {
613
612
  return new Response('Custom Internal Server Error Message', { status: 500 });
@@ -876,8 +875,8 @@ try {
876
875
  <a id="api-content"></a>
877
876
  ## API > Content
878
877
 
879
- <a id="api-content-template-sub"></a>
880
- ### 🔧 `template_sub(template: string, replacements: Record<string, string>): string`
878
+ <a id="api-content-parse-template"></a>
879
+ ### 🔧 `parse_template(template: string, replacements: Record<string, string>): string`
881
880
 
882
881
  Replace placeholders in a template string with values from a replacement object.
883
882
 
@@ -888,12 +887,12 @@ Replace placeholders in a template string with values from a replacement object.
888
887
  const template = `
889
888
  <html>
890
889
  <head>
891
- <title>{title}</title>
890
+ <title>{$title}</title>
892
891
  </head>
893
892
  <body>
894
- <h1>{title}</h1>
895
- <p>{content}</p>
896
- <p>{ignored}</p>
893
+ <h1>{$title}</h1>
894
+ <p>{$content}</p>
895
+ <p>{$ignored}</p>
897
896
  </body>
898
897
  </html>
899
898
  `;
@@ -903,7 +902,7 @@ const replacements = {
903
902
  content: 'This is a test.'
904
903
  };
905
904
 
906
- const html = template_sub(template, replacements);
905
+ const html = parse_template(template, replacements);
907
906
  ```
908
907
 
909
908
  ```html
@@ -914,28 +913,40 @@ const html = template_sub(template, replacements);
914
913
  <body>
915
914
  <h1>Hello, world!</h1>
916
915
  <p>This is a test.</p>
917
- <p>{ignored}</p>
916
+ <p>{$ignored}</p>
918
917
  </body>
919
918
  </html>
920
919
  ```
921
920
 
922
- <a id="api-content-template-sub-file"></a>
923
- ### 🔧 `template_sub_file(template_file: string, replacements: Record<string, string>): Promise<string>`
921
+ `parse_template` supports looping arrays with the following syntax.
924
922
 
925
- Replace placeholders in a template file with values from a replacement object.
923
+ ```html
924
+ {$for:foo}My colour is %s{/for}
925
+ ```
926
+ ```ts
927
+ const template = `
928
+ <ul>
929
+ {$for:foo}<li>%s</li>{/for}
930
+ </ul>
931
+ `;
926
932
 
927
- > [!NOTE]
928
- > This function is a convenience wrapper around `template_sub` and `Bun.file().text()` to reduce boilerplate. See `template_sub` for more information.
933
+ const replacements = {
934
+ foo: ['red', 'green', 'blue']
935
+ };
929
936
 
930
- ```ts
931
- const html = await template_sub_file('./template.html', replacements);
937
+ const html = parse_template(template, replacements);
938
+ ```
932
939
 
933
- // Is equivalent to:
934
- const file = Bun.file('./template.html');
935
- const file_contents = await file.text();
936
- const html = await template_sub(file_contents, replacements);
940
+ ```html
941
+ <ul>
942
+ <li>red</li>
943
+ <li>green</li>
944
+ <li>blue</li>
945
+ </ul>
937
946
  ```
938
947
 
948
+ > [!WARNING]
949
+ > Nested loops are not supported.
939
950
 
940
951
  <a id="api-content-generate-hash-subs"></a>
941
952
  ### 🔧 `generate_hash_subs(prefix: string): Promise<Record<string, string>>`
@@ -951,7 +962,7 @@ let hash_sub_table = {};
951
962
  generate_hash_subs().then(subs => hash_sub_table = subs).catch(caution);
952
963
 
953
964
  server.route('/test', (req, url) => {
954
- return template_sub('Hello world {hash=docs/project-logo.png}', hash_sub_table);
965
+ return parse_template('Hello world {hash=docs/project-logo.png}', hash_sub_table);
955
966
  });
956
967
  ```
957
968
 
@@ -978,7 +989,7 @@ Use a different prefix other than `hash=` by passing it as the first parameter.
978
989
  generate_hash_subs(7, '#').then(subs => hash_sub_table = subs).catch(caution);
979
990
 
980
991
  server.route('/test', (req, url) => {
981
- return template_sub('Hello world {#docs/project-logo.png}', hash_sub_table);
992
+ return parse_template('Hello world {#docs/project-logo.png}', hash_sub_table);
982
993
  });
983
994
  ```
984
995
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "4.1.0",
4
+ "version": "4.2.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "bun": "./src/api.ts",
package/src/api.d.ts CHANGED
@@ -15,8 +15,7 @@ export declare class ErrorWithMetadata extends Error {
15
15
  }
16
16
  export declare function panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>;
17
17
  export declare function caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>;
18
- export declare function template_sub_file(template_file: string, replacements: Record<string, string>): Promise<string>;
19
- export declare function template_sub(template: string, replacements: Record<string, string>): string;
18
+ export declare function parse_template(template: string, replacements: Record<string, string | Array<string>>): string;
20
19
  export declare function generate_hash_subs(length?: number, prefix?: string): Promise<Record<string, string>>;
21
20
  type CookieOptions = {
22
21
  same_site?: 'Strict' | 'Lax' | 'None';
@@ -43,7 +42,7 @@ type JsonSerializable = JsonPrimitive | JsonObject | JsonArray | ToJson;
43
42
  type HandlerReturnType = Resolvable<string | number | BunFile | Response | JsonSerializable | Blob>;
44
43
  type RequestHandler = (req: Request, url: URL) => HandlerReturnType;
45
44
  type WebhookHandler = (payload: JsonSerializable) => HandlerReturnType;
46
- type ErrorHandler = (err: Error, req: Request, url: URL) => Response;
45
+ type ErrorHandler = (err: Error, req: Request, url: URL) => Resolvable<Response>;
47
46
  type DefaultHandler = (req: Request, status_code: number) => HandlerReturnType;
48
47
  type StatusCodeHandler = (req: Request) => HandlerReturnType;
49
48
  type ServerSentEventClient = {
package/src/api.ts CHANGED
@@ -89,14 +89,7 @@ export async function caution(err_message_or_obj: string | object, ...err: objec
89
89
  await handle_error('caution: ', err_message_or_obj, ...err);
90
90
  }
91
91
 
92
- export async function template_sub_file(template_file: string, replacements: Record<string, string>): Promise<string> {
93
- const file = Bun.file(template_file);
94
- const file_contents = await file.text();
95
-
96
- return template_sub(file_contents, replacements);
97
- }
98
-
99
- export function template_sub(template: string, replacements: Record<string, string>): string {
92
+ export function parse_template(template: string, replacements: Record<string, string | Array<string>>): string {
100
93
  let result = '';
101
94
  let buffer = '';
102
95
  let buffer_active = false;
@@ -105,14 +98,41 @@ export function template_sub(template: string, replacements: Record<string, stri
105
98
  for (let i = 0; i < template_length; i++) {
106
99
  const char = template[i];
107
100
 
108
- if (char === '{') {
101
+ if (char === '{' && template[i + 1] === '$') {
102
+ i++;
109
103
  buffer_active = true;
110
104
  buffer = '';
111
- } else if (char === '}') {
105
+ } else if (char === '}' && buffer_active) {
112
106
  buffer_active = false;
113
107
 
114
- result += replacements[buffer] ?? '{' + buffer + '}';
108
+ if (buffer.startsWith('for:')) {
109
+ const loop_key = buffer.substring(4);
110
+
111
+ const loop_entries = replacements[loop_key];
112
+ if (loop_entries !== undefined) {
113
+ const loop_content_start_index = i + 1;
114
+ const loop_close_index = template.indexOf('{/for}', loop_content_start_index);
115
+ const loop_content = template.substring(loop_content_start_index, loop_close_index);
116
+
117
+ // More performat than replaceAll on larger arrays (and equal on tiny arrays).
118
+ const content_parts = loop_content.split('%s');
119
+ const indicies = [] as Array<number>;
115
120
 
121
+ for (let j = 0; j < content_parts.length; j++)
122
+ if (content_parts[j] === '%s')
123
+ indicies.push(j);
124
+
125
+ for (const loop_entry of loop_entries)
126
+ for (const index of indicies)
127
+ content_parts[index] = loop_entry;
128
+
129
+ i += loop_content.length + 6;
130
+ } else {
131
+ result += '{$' + buffer + '}';
132
+ }
133
+ } else {
134
+ result += replacements[buffer] ?? '{$' + buffer + '}';
135
+ }
116
136
  buffer = '';
117
137
  } else if (buffer_active) {
118
138
  buffer += char;
@@ -249,7 +269,7 @@ type JsonSerializable = JsonPrimitive | JsonObject | JsonArray | ToJson;
249
269
  type HandlerReturnType = Resolvable<string | number | BunFile | Response | JsonSerializable | Blob>;
250
270
  type RequestHandler = (req: Request, url: URL) => HandlerReturnType;
251
271
  type WebhookHandler = (payload: JsonSerializable) => HandlerReturnType;
252
- type ErrorHandler = (err: Error, req: Request, url: URL) => Response;
272
+ type ErrorHandler = (err: Error, req: Request, url: URL) => Resolvable<Response>;
253
273
  type DefaultHandler = (req: Request, status_code: number) => HandlerReturnType;
254
274
  type StatusCodeHandler = (req: Request) => HandlerReturnType;
255
275
 
@@ -413,7 +433,7 @@ export function serve(port: number) {
413
433
  return new Response(http.STATUS_CODES[status_code], { status: status_code });
414
434
  } catch (e) {
415
435
  if (error_handler !== undefined)
416
- return error_handler(e as Error, req, url);
436
+ return await error_handler(e as Error, req, url);
417
437
 
418
438
  return new Response(http.STATUS_CODES[500], { status: 500 });
419
439
  }