@prairielearn/sanitize 2.0.21 → 2.1.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
@@ -1,5 +1,17 @@
1
1
  # @prairielearn/sanitize
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 789fdfa: Add `truncate()` function
8
+
9
+ ## 2.0.22
10
+
11
+ ### Patch Changes
12
+
13
+ - 0425922: Upgrade all JavaScript dependencies
14
+
3
15
  ## 2.0.21
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -5,7 +5,12 @@ A collection of functions for sanitizing and escaping various values.
5
5
  ## Usage
6
6
 
7
7
  ```ts
8
- import { sanitizeObject, escapeRegExp, recursivelyTruncateStrings } from '@prairielearn/sanitize';
8
+ import {
9
+ sanitizeObject,
10
+ escapeRegExp,
11
+ truncate,
12
+ recursivelyTruncateStrings,
13
+ } from '@prairielearn/sanitize';
9
14
 
10
15
  sanitizeObject({
11
16
  value: 'null \u0000 byte',
@@ -13,6 +18,8 @@ sanitizeObject({
13
18
 
14
19
  escapeRegExp('foo*(bar)');
15
20
 
21
+ truncate('testing testing', 7);
22
+
16
23
  recursivelyTruncateStrings(
17
24
  {
18
25
  foo: {
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export declare function sanitizeObject<T>(value: T): T;
15
15
  * @returns A string literal ready to match
16
16
  */
17
17
  export declare function escapeRegExp(str: string): string;
18
+ export declare function truncate(str: string, maxLength: number): string;
18
19
  /**
19
20
  * Recursively truncates all strings in a value to a maximum length.
20
21
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAkB7C;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,UAEvC;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC,CAkB5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAkB7C;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,UAEvC;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC,CAe5E"}
package/dist/index.js CHANGED
@@ -39,6 +39,12 @@ export function sanitizeObject(value) {
39
39
  export function escapeRegExp(str) {
40
40
  return str.replaceAll(/[.*+\-?^${}()|[\]\\/]/g, '\\$&');
41
41
  }
42
+ export function truncate(str, maxLength) {
43
+ if (str.length <= maxLength) {
44
+ return str;
45
+ }
46
+ return str.slice(0, maxLength) + '...[truncated]';
47
+ }
42
48
  /**
43
49
  * Recursively truncates all strings in a value to a maximum length.
44
50
  */
@@ -47,10 +53,7 @@ export function recursivelyTruncateStrings(value, maxLength) {
47
53
  return null;
48
54
  }
49
55
  else if (typeof value === 'string') {
50
- if (value.length <= maxLength) {
51
- return value;
52
- }
53
- return (value.slice(0, maxLength) + '...[truncated]');
56
+ return truncate(value, maxLength);
54
57
  }
55
58
  else if (Array.isArray(value)) {
56
59
  return value.map((value) => recursivelyTruncateStrings(value, maxLength));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAI,KAAQ;IACxC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,IAAS,CAAC;IACnB,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC,GAAG,CAAC,cAAc,CAAM,CAAC;IACxC,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAM,CAAC;IACpD,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAC3D,OAAO,CAAC,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QACH,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAC5C,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACjB,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAAS,CAAM,CAAC;IACrB,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,OAAO,GAAG,CAAC,UAAU,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC;AAC1D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,0BAA0B,CAAI,KAAQ,EAAE,SAAiB;IACvE,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,IAAS,CAAC;IACnB,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,MAAM,IAAI,SAAS,EAAE,CAAC;YAC9B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,gBAAgB,CAAM,CAAC;IAC7D,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,0BAA0B,CAAC,KAAK,EAAE,SAAS,CAAC,CAAM,CAAC;IACjF,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YACxD,GAAG,CAAC,GAAG,CAAC,GAAG,0BAA0B,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YACxD,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAAS,CAAM,CAAC;IACrB,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC","sourcesContent":["/**\n * Recursively traverse an object and replace null bytes (\\u0000) with the\n * literal string \"\\u0000\". This produces a new object and does not modify the\n * provided object.\n *\n * @param value The object to be sanitized.\n * @returns The sanitized object.\n */\nexport function sanitizeObject<T>(value: T): T {\n if (value === null) {\n return null as T;\n } else if (Array.isArray(value)) {\n return value.map(sanitizeObject) as T;\n } else if (typeof value === 'string') {\n return value.replaceAll('\\u0000', '\\\\u0000') as T;\n } else if (typeof value === 'object') {\n const sanitized = Object.entries(value).map(([key, value]) => {\n return [key, sanitizeObject(value)];\n });\n return sanitized.reduce((acc, [key, value]) => {\n acc[key] = value;\n return acc;\n }, {} as any) as T;\n } else {\n return value;\n }\n}\n\n/**\n * Escape special characters in a RegExp string\n * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters\n *\n * @param str A literal string to act as a match for RegExp objects\n * @returns A string literal ready to match\n */\nexport function escapeRegExp(str: string) {\n return str.replaceAll(/[.*+\\-?^${}()|[\\]\\\\/]/g, '\\\\$&');\n}\n\n/**\n * Recursively truncates all strings in a value to a maximum length.\n */\nexport function recursivelyTruncateStrings<T>(value: T, maxLength: number): T {\n if (value === null) {\n return null as T;\n } else if (typeof value === 'string') {\n if (value.length <= maxLength) {\n return value;\n }\n return (value.slice(0, maxLength) + '...[truncated]') as T;\n } else if (Array.isArray(value)) {\n return value.map((value) => recursivelyTruncateStrings(value, maxLength)) as T;\n } else if (typeof value === 'object') {\n return Object.entries(value).reduce((acc, [key, value]) => {\n acc[key] = recursivelyTruncateStrings(value, maxLength);\n return acc;\n }, {} as any) as T;\n } else {\n return value;\n }\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAI,KAAQ;IACxC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,IAAS,CAAC;IACnB,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC,GAAG,CAAC,cAAc,CAAM,CAAC;IACxC,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAM,CAAC;IACpD,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAC3D,OAAO,CAAC,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QACH,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAC5C,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACjB,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAAS,CAAM,CAAC;IACrB,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,OAAO,GAAG,CAAC,UAAU,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW,EAAE,SAAiB;IACrD,IAAI,GAAG,CAAC,MAAM,IAAI,SAAS,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,gBAAgB,CAAC;AACpD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,0BAA0B,CAAI,KAAQ,EAAE,SAAiB;IACvE,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,IAAS,CAAC;IACnB,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAM,CAAC;IACzC,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,0BAA0B,CAAC,KAAK,EAAE,SAAS,CAAC,CAAM,CAAC;IACjF,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YACxD,GAAG,CAAC,GAAG,CAAC,GAAG,0BAA0B,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YACxD,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAAS,CAAM,CAAC;IACrB,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC","sourcesContent":["/**\n * Recursively traverse an object and replace null bytes (\\u0000) with the\n * literal string \"\\u0000\". This produces a new object and does not modify the\n * provided object.\n *\n * @param value The object to be sanitized.\n * @returns The sanitized object.\n */\nexport function sanitizeObject<T>(value: T): T {\n if (value === null) {\n return null as T;\n } else if (Array.isArray(value)) {\n return value.map(sanitizeObject) as T;\n } else if (typeof value === 'string') {\n return value.replaceAll('\\u0000', '\\\\u0000') as T;\n } else if (typeof value === 'object') {\n const sanitized = Object.entries(value).map(([key, value]) => {\n return [key, sanitizeObject(value)];\n });\n return sanitized.reduce((acc, [key, value]) => {\n acc[key] = value;\n return acc;\n }, {} as any) as T;\n } else {\n return value;\n }\n}\n\n/**\n * Escape special characters in a RegExp string\n * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters\n *\n * @param str A literal string to act as a match for RegExp objects\n * @returns A string literal ready to match\n */\nexport function escapeRegExp(str: string) {\n return str.replaceAll(/[.*+\\-?^${}()|[\\]\\\\/]/g, '\\\\$&');\n}\n\nexport function truncate(str: string, maxLength: number): string {\n if (str.length <= maxLength) {\n return str;\n }\n return str.slice(0, maxLength) + '...[truncated]';\n}\n\n/**\n * Recursively truncates all strings in a value to a maximum length.\n */\nexport function recursivelyTruncateStrings<T>(value: T, maxLength: number): T {\n if (value === null) {\n return null as T;\n } else if (typeof value === 'string') {\n return truncate(value, maxLength) as T;\n } else if (Array.isArray(value)) {\n return value.map((value) => recursivelyTruncateStrings(value, maxLength)) as T;\n } else if (typeof value === 'object') {\n return Object.entries(value).reduce((acc, [key, value]) => {\n acc[key] = recursivelyTruncateStrings(value, maxLength);\n return acc;\n }, {} as any) as T;\n } else {\n return value;\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { assert, describe, it } from 'vitest';
2
- import { recursivelyTruncateStrings, sanitizeObject } from './index.js';
2
+ import { recursivelyTruncateStrings, sanitizeObject, truncate } from './index.js';
3
3
  describe('sanitizeObject', () => {
4
4
  it('sanitizes an empty object', () => {
5
5
  const input = {};
@@ -76,6 +76,17 @@ describe('sanitizeObject', () => {
76
76
  assert.deepEqual(expected, sanitizeObject(input));
77
77
  });
78
78
  });
79
+ describe('truncate', () => {
80
+ it('does not truncate empty string', () => {
81
+ assert.equal(truncate('', 10), '');
82
+ });
83
+ it('does not truncate short string', () => {
84
+ assert.equal(truncate('test', 10), 'test');
85
+ });
86
+ it('truncates long string', () => {
87
+ assert.equal(truncate('testtest', 4), 'test...[truncated]');
88
+ });
89
+ });
79
90
  describe('recursivelyTruncateStrings', () => {
80
91
  it('handles empty object', () => {
81
92
  assert.deepEqual(recursivelyTruncateStrings({}, 10), {});
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { assert, describe, it } from 'vitest';\n\nimport { recursivelyTruncateStrings, sanitizeObject } from './index.js';\n\ndescribe('sanitizeObject', () => {\n it('sanitizes an empty object', () => {\n const input = {};\n const expected = {};\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null byte in top-level string', () => {\n const input = { test: 'test\\u0000ing' };\n const expected = { test: 'test\\\\u0000ing' };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles multiple null bytes in top-level string', () => {\n const input = { test: 'test\\u0000ing\\u0000' };\n const expected = { test: 'test\\\\u0000ing\\\\u0000' };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null byte in nested string', () => {\n const input = {\n test: {\n nestedTest: 'test\\u0000ing',\n },\n };\n const expected = {\n test: {\n nestedTest: 'test\\\\u0000ing',\n },\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null byte in top-level array', () => {\n const input = {\n test: ['testing', 'test\\u0000ing'],\n };\n const expected = {\n test: ['testing', 'test\\\\u0000ing'],\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null byte in nested array', () => {\n const input = {\n test: {\n test2: ['testing', 'test\\u0000ing'],\n },\n };\n const expected = {\n test: {\n test2: ['testing', 'test\\\\u0000ing'],\n },\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles numbers correctly', () => {\n const input = {\n test: 'test\\u0000ing',\n a: 1,\n b: 2.45,\n };\n const expected = {\n test: 'test\\\\u0000ing',\n a: 1,\n b: 2.45,\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null values correctly', () => {\n const input = {\n test: 'test\\u0000ing',\n a: null,\n };\n const expected = {\n test: 'test\\\\u0000ing',\n a: null,\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n});\n\ndescribe('recursivelyTruncateStrings', () => {\n it('handles empty object', () => {\n assert.deepEqual(recursivelyTruncateStrings({}, 10), {});\n });\n\n it('handles null and undefined', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: null }, 10), { test: null });\n assert.deepEqual(recursivelyTruncateStrings({ test: undefined }, 10), { test: undefined });\n });\n\n it('handles legal string', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: 'test' }, 10), { test: 'test' });\n });\n\n it('handles long string', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: 'testtest' }, 4), {\n test: 'test...[truncated]',\n });\n });\n\n it('handles long string in array', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: ['testtest'] }, 4), {\n test: ['test...[truncated]'],\n });\n });\n\n it('handles long string in object in array', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: [{ test: 'testtest' }] }, 4), {\n test: [{ test: 'test...[truncated]' }],\n });\n });\n});\n"]}
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { assert, describe, it } from 'vitest';\n\nimport { recursivelyTruncateStrings, sanitizeObject, truncate } from './index.js';\n\ndescribe('sanitizeObject', () => {\n it('sanitizes an empty object', () => {\n const input = {};\n const expected = {};\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null byte in top-level string', () => {\n const input = { test: 'test\\u0000ing' };\n const expected = { test: 'test\\\\u0000ing' };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles multiple null bytes in top-level string', () => {\n const input = { test: 'test\\u0000ing\\u0000' };\n const expected = { test: 'test\\\\u0000ing\\\\u0000' };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null byte in nested string', () => {\n const input = {\n test: {\n nestedTest: 'test\\u0000ing',\n },\n };\n const expected = {\n test: {\n nestedTest: 'test\\\\u0000ing',\n },\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null byte in top-level array', () => {\n const input = {\n test: ['testing', 'test\\u0000ing'],\n };\n const expected = {\n test: ['testing', 'test\\\\u0000ing'],\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null byte in nested array', () => {\n const input = {\n test: {\n test2: ['testing', 'test\\u0000ing'],\n },\n };\n const expected = {\n test: {\n test2: ['testing', 'test\\\\u0000ing'],\n },\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles numbers correctly', () => {\n const input = {\n test: 'test\\u0000ing',\n a: 1,\n b: 2.45,\n };\n const expected = {\n test: 'test\\\\u0000ing',\n a: 1,\n b: 2.45,\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n\n it('handles null values correctly', () => {\n const input = {\n test: 'test\\u0000ing',\n a: null,\n };\n const expected = {\n test: 'test\\\\u0000ing',\n a: null,\n };\n assert.deepEqual(expected, sanitizeObject(input));\n });\n});\n\ndescribe('truncate', () => {\n it('does not truncate empty string', () => {\n assert.equal(truncate('', 10), '');\n });\n\n it('does not truncate short string', () => {\n assert.equal(truncate('test', 10), 'test');\n });\n\n it('truncates long string', () => {\n assert.equal(truncate('testtest', 4), 'test...[truncated]');\n });\n});\n\ndescribe('recursivelyTruncateStrings', () => {\n it('handles empty object', () => {\n assert.deepEqual(recursivelyTruncateStrings({}, 10), {});\n });\n\n it('handles null and undefined', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: null }, 10), { test: null });\n assert.deepEqual(recursivelyTruncateStrings({ test: undefined }, 10), { test: undefined });\n });\n\n it('handles legal string', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: 'test' }, 10), { test: 'test' });\n });\n\n it('handles long string', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: 'testtest' }, 4), {\n test: 'test...[truncated]',\n });\n });\n\n it('handles long string in array', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: ['testtest'] }, 4), {\n test: ['test...[truncated]'],\n });\n });\n\n it('handles long string in object in array', () => {\n assert.deepEqual(recursivelyTruncateStrings({ test: [{ test: 'testtest' }] }, 4), {\n test: [{ test: 'test...[truncated]' }],\n });\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/sanitize",
3
- "version": "2.0.21",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,10 +15,10 @@
15
15
  },
16
16
  "devDependencies": {
17
17
  "@prairielearn/tsconfig": "^0.0.0",
18
- "@types/node": "^22.18.8",
19
- "@vitest/coverage-v8": "^3.2.4",
18
+ "@types/node": "^22.19.0",
19
+ "@vitest/coverage-v8": "^4.0.7",
20
20
  "tsx": "^4.20.6",
21
21
  "typescript": "^5.9.3",
22
- "vitest": "^3.2.4"
22
+ "vitest": "^4.0.7"
23
23
  }
24
24
  }
package/src/index.test.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { assert, describe, it } from 'vitest';
2
2
 
3
- import { recursivelyTruncateStrings, sanitizeObject } from './index.js';
3
+ import { recursivelyTruncateStrings, sanitizeObject, truncate } from './index.js';
4
4
 
5
5
  describe('sanitizeObject', () => {
6
6
  it('sanitizes an empty object', () => {
@@ -86,6 +86,20 @@ describe('sanitizeObject', () => {
86
86
  });
87
87
  });
88
88
 
89
+ describe('truncate', () => {
90
+ it('does not truncate empty string', () => {
91
+ assert.equal(truncate('', 10), '');
92
+ });
93
+
94
+ it('does not truncate short string', () => {
95
+ assert.equal(truncate('test', 10), 'test');
96
+ });
97
+
98
+ it('truncates long string', () => {
99
+ assert.equal(truncate('testtest', 4), 'test...[truncated]');
100
+ });
101
+ });
102
+
89
103
  describe('recursivelyTruncateStrings', () => {
90
104
  it('handles empty object', () => {
91
105
  assert.deepEqual(recursivelyTruncateStrings({}, 10), {});
package/src/index.ts CHANGED
@@ -37,6 +37,13 @@ export function escapeRegExp(str: string) {
37
37
  return str.replaceAll(/[.*+\-?^${}()|[\]\\/]/g, '\\$&');
38
38
  }
39
39
 
40
+ export function truncate(str: string, maxLength: number): string {
41
+ if (str.length <= maxLength) {
42
+ return str;
43
+ }
44
+ return str.slice(0, maxLength) + '...[truncated]';
45
+ }
46
+
40
47
  /**
41
48
  * Recursively truncates all strings in a value to a maximum length.
42
49
  */
@@ -44,10 +51,7 @@ export function recursivelyTruncateStrings<T>(value: T, maxLength: number): T {
44
51
  if (value === null) {
45
52
  return null as T;
46
53
  } else if (typeof value === 'string') {
47
- if (value.length <= maxLength) {
48
- return value;
49
- }
50
- return (value.slice(0, maxLength) + '...[truncated]') as T;
54
+ return truncate(value, maxLength) as T;
51
55
  } else if (Array.isArray(value)) {
52
56
  return value.map((value) => recursivelyTruncateStrings(value, maxLength)) as T;
53
57
  } else if (typeof value === 'object') {
package/vitest.config.ts CHANGED
@@ -2,10 +2,10 @@ import { defineConfig } from 'vitest/config';
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
+ dir: `${import.meta.dirname}/src`,
5
6
  coverage: {
6
7
  reporter: ['html', 'text-summary', 'cobertura'],
7
- all: true,
8
- include: ['src/**'],
8
+ include: ['src/**/*.{ts,tsx}'],
9
9
  },
10
10
  },
11
11
  });