@doist/todoist-api-typescript 5.8.0 → 6.0.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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/authentication.js +158 -0
  3. package/dist/cjs/consts/endpoints.js +74 -0
  4. package/dist/{index.js → cjs/index.js} +1 -1
  5. package/dist/cjs/package.json +1 -0
  6. package/dist/cjs/rest-client.js +124 -0
  7. package/dist/{testUtils → cjs/test-utils}/asserts.js +1 -1
  8. package/dist/{testUtils → cjs/test-utils}/mocks.js +8 -4
  9. package/dist/cjs/test-utils/msw-setup.js +27 -0
  10. package/dist/{testUtils/testDefaults.js → cjs/test-utils/test-defaults.js} +41 -52
  11. package/dist/cjs/todoist-api.js +1235 -0
  12. package/dist/{types → cjs/types}/entities.js +78 -31
  13. package/dist/cjs/types/errors.js +22 -0
  14. package/dist/cjs/types/http.js +22 -0
  15. package/dist/cjs/utils/case-conversion.js +69 -0
  16. package/dist/{utils → cjs/utils}/colors.js +3 -3
  17. package/dist/cjs/utils/fetch-with-retry.js +150 -0
  18. package/dist/{utils → cjs/utils}/index.js +4 -4
  19. package/dist/cjs/utils/multipart-upload.js +126 -0
  20. package/dist/{utils → cjs/utils}/processing-helpers.js +3 -3
  21. package/dist/{utils → cjs/utils}/sanitization.js +17 -28
  22. package/dist/{utils/urlHelpers.js → cjs/utils/url-helpers.js} +15 -15
  23. package/dist/{utils → cjs/utils}/validators.js +28 -1
  24. package/dist/esm/authentication.js +151 -0
  25. package/dist/esm/consts/endpoints.js +65 -0
  26. package/dist/esm/index.js +4 -0
  27. package/dist/esm/rest-client.js +119 -0
  28. package/dist/esm/test-utils/asserts.js +8 -0
  29. package/dist/esm/test-utils/mocks.js +10 -0
  30. package/dist/esm/test-utils/msw-setup.js +22 -0
  31. package/dist/esm/test-utils/test-defaults.js +198 -0
  32. package/dist/esm/todoist-api.js +1231 -0
  33. package/dist/esm/types/entities.js +366 -0
  34. package/dist/esm/types/errors.js +18 -0
  35. package/dist/esm/types/http.js +18 -0
  36. package/dist/esm/types/index.js +3 -0
  37. package/dist/esm/types/requests.js +1 -0
  38. package/dist/esm/types/sync.js +1 -0
  39. package/dist/esm/utils/activity-helpers.js +36 -0
  40. package/dist/esm/utils/case-conversion.js +61 -0
  41. package/dist/esm/utils/colors.js +215 -0
  42. package/dist/esm/utils/fetch-with-retry.js +147 -0
  43. package/dist/esm/utils/index.js +3 -0
  44. package/dist/esm/utils/multipart-upload.js +120 -0
  45. package/dist/esm/utils/processing-helpers.js +12 -0
  46. package/dist/esm/utils/sanitization.js +112 -0
  47. package/dist/esm/utils/url-helpers.js +68 -0
  48. package/dist/esm/utils/validators.js +97 -0
  49. package/dist/{authentication.d.ts → types/authentication.d.ts} +6 -1
  50. package/dist/{consts → types/consts}/endpoints.d.ts +11 -0
  51. package/dist/types/index.d.ts +4 -3
  52. package/dist/types/rest-client.d.ts +15 -0
  53. package/dist/types/test-utils/msw-setup.d.ts +3 -0
  54. package/dist/{TodoistApi.d.ts → types/todoist-api.d.ts} +91 -2
  55. package/dist/types/{entities.d.ts → types/entities.d.ts} +119 -0
  56. package/dist/types/types/http.d.ts +68 -0
  57. package/dist/types/types/index.d.ts +3 -0
  58. package/dist/types/{requests.d.ts → types/requests.d.ts} +137 -0
  59. package/dist/types/utils/case-conversion.d.ts +12 -0
  60. package/dist/types/utils/fetch-with-retry.d.ts +11 -0
  61. package/dist/types/utils/index.d.ts +3 -0
  62. package/dist/types/utils/multipart-upload.d.ts +50 -0
  63. package/dist/{utils → types/utils}/validators.d.ts +7 -1
  64. package/package.json +24 -8
  65. package/dist/TodoistApi.js +0 -1209
  66. package/dist/authentication.js +0 -199
  67. package/dist/consts/endpoints.js +0 -50
  68. package/dist/index.d.ts +0 -4
  69. package/dist/restClient.d.ts +0 -5
  70. package/dist/restClient.js +0 -170
  71. package/dist/types/errors.js +0 -39
  72. package/dist/types/http.d.ts +0 -1
  73. package/dist/types/http.js +0 -2
  74. package/dist/utils/index.d.ts +0 -3
  75. /package/dist/{types → cjs/types}/index.js +0 -0
  76. /package/dist/{types → cjs/types}/requests.js +0 -0
  77. /package/dist/{types → cjs/types}/sync.js +0 -0
  78. /package/dist/{utils → cjs/utils}/activity-helpers.js +0 -0
  79. /package/dist/{testUtils → types/test-utils}/asserts.d.ts +0 -0
  80. /package/dist/{testUtils → types/test-utils}/mocks.d.ts +0 -0
  81. /package/dist/{testUtils/testDefaults.d.ts → types/test-utils/test-defaults.d.ts} +0 -0
  82. /package/dist/types/{errors.d.ts → types/errors.d.ts} +0 -0
  83. /package/dist/types/{sync.d.ts → types/sync.d.ts} +0 -0
  84. /package/dist/{utils → types/utils}/activity-helpers.d.ts +0 -0
  85. /package/dist/{utils → types/utils}/colors.d.ts +0 -0
  86. /package/dist/{utils → types/utils}/processing-helpers.d.ts +0 -0
  87. /package/dist/{utils → types/utils}/sanitization.d.ts +0 -0
  88. /package/dist/{utils/urlHelpers.d.ts → types/utils/url-helpers.d.ts} +0 -0
@@ -0,0 +1,215 @@
1
+ export const berryRed = {
2
+ id: 30,
3
+ key: 'berry_red',
4
+ displayName: 'Berry Red',
5
+ name: 'Berry Red',
6
+ hexValue: '#b8255f',
7
+ value: '#b8255f',
8
+ };
9
+ export const red = {
10
+ id: 31,
11
+ key: 'red',
12
+ displayName: 'Red',
13
+ name: 'Red',
14
+ hexValue: '#db4035',
15
+ value: '#db4035',
16
+ };
17
+ export const orange = {
18
+ id: 32,
19
+ key: 'orange',
20
+ displayName: 'Orange',
21
+ name: 'Orange',
22
+ hexValue: '#ff9933',
23
+ value: '#ff9933',
24
+ };
25
+ export const yellow = {
26
+ id: 33,
27
+ key: 'yellow',
28
+ displayName: 'Yellow',
29
+ name: 'Yellow',
30
+ hexValue: '#fad000',
31
+ value: '#fad000',
32
+ };
33
+ export const oliveGreen = {
34
+ id: 34,
35
+ key: 'olive_green',
36
+ displayName: 'Olive Green',
37
+ name: 'Olive Green',
38
+ hexValue: '#afb83b',
39
+ value: '#afb83b',
40
+ };
41
+ export const limeGreen = {
42
+ id: 35,
43
+ key: 'lime_green',
44
+ displayName: 'Lime Green',
45
+ name: 'Lime Green',
46
+ hexValue: '#7ecc49',
47
+ value: '#7ecc49',
48
+ };
49
+ export const green = {
50
+ id: 36,
51
+ key: 'green',
52
+ displayName: 'Green',
53
+ name: 'Green',
54
+ hexValue: '#299438',
55
+ value: '#299438',
56
+ };
57
+ export const mintGreen = {
58
+ id: 37,
59
+ key: 'mint_green',
60
+ displayName: 'Mint Green',
61
+ name: 'Mint Green',
62
+ hexValue: '#6accbc',
63
+ value: '#6accbc',
64
+ };
65
+ export const turquoise = {
66
+ id: 38,
67
+ key: 'turquoise',
68
+ displayName: 'Turquoise',
69
+ name: 'Turquoise',
70
+ hexValue: '#158fad',
71
+ value: '#158fad',
72
+ };
73
+ export const skyBlue = {
74
+ id: 39,
75
+ key: 'sky_blue',
76
+ displayName: 'Sky Blue',
77
+ name: 'Sky Blue',
78
+ hexValue: '#14aaf5',
79
+ value: '#14aaf5',
80
+ };
81
+ export const lightBlue = {
82
+ id: 40,
83
+ key: 'light_blue',
84
+ displayName: 'Light Blue',
85
+ name: 'Light Blue',
86
+ hexValue: '#96c3eb',
87
+ value: '#96c3eb',
88
+ };
89
+ export const blue = {
90
+ id: 41,
91
+ key: 'blue',
92
+ displayName: 'Blue',
93
+ name: 'Blue',
94
+ hexValue: '#4073ff',
95
+ value: '#4073ff',
96
+ };
97
+ export const grape = {
98
+ id: 42,
99
+ key: 'grape',
100
+ displayName: 'Grape',
101
+ name: 'Grape',
102
+ hexValue: '#884dff',
103
+ value: '#884dff',
104
+ };
105
+ export const violet = {
106
+ id: 43,
107
+ key: 'violet',
108
+ displayName: 'Violet',
109
+ name: 'Violet',
110
+ hexValue: '#af38eb',
111
+ value: '#af38eb',
112
+ };
113
+ export const lavender = {
114
+ id: 44,
115
+ key: 'lavender',
116
+ displayName: 'Lavender',
117
+ name: 'Lavender',
118
+ hexValue: '#eb96eb',
119
+ value: '#eb96eb',
120
+ };
121
+ export const magenta = {
122
+ id: 45,
123
+ key: 'magenta',
124
+ displayName: 'Magenta',
125
+ name: 'Magenta',
126
+ hexValue: '#e05194',
127
+ value: '#e05194',
128
+ };
129
+ export const salmon = {
130
+ id: 46,
131
+ key: 'salmon',
132
+ displayName: 'Salmon',
133
+ name: 'Salmon',
134
+ hexValue: '#ff8d85',
135
+ value: '#ff8d85',
136
+ };
137
+ export const charcoal = {
138
+ id: 47,
139
+ key: 'charcoal',
140
+ displayName: 'Charcoal',
141
+ name: 'Charcoal',
142
+ hexValue: '#808080',
143
+ value: '#808080',
144
+ };
145
+ export const gray = {
146
+ id: 48,
147
+ key: 'gray',
148
+ displayName: 'Gray',
149
+ name: 'Gray',
150
+ hexValue: '#b8b8b8',
151
+ value: '#b8b8b8',
152
+ };
153
+ export const taupe = {
154
+ id: 49,
155
+ key: 'taupe',
156
+ displayName: 'Taupe',
157
+ name: 'Taupe',
158
+ hexValue: '#ccac93',
159
+ value: '#ccac93',
160
+ };
161
+ export const colors = [
162
+ berryRed,
163
+ red,
164
+ orange,
165
+ yellow,
166
+ oliveGreen,
167
+ limeGreen,
168
+ green,
169
+ mintGreen,
170
+ turquoise,
171
+ skyBlue,
172
+ lightBlue,
173
+ blue,
174
+ grape,
175
+ violet,
176
+ lavender,
177
+ magenta,
178
+ salmon,
179
+ charcoal,
180
+ gray,
181
+ taupe,
182
+ ];
183
+ export const defaultColor = charcoal;
184
+ /**
185
+ * @private
186
+ * @deprecated Use {@link getColorByKey} instead
187
+ */
188
+ export function getColorById(colorId) {
189
+ const color = colors.find((color) => color.id === colorId);
190
+ return color !== null && color !== void 0 ? color : defaultColor;
191
+ }
192
+ /**
193
+ * @private
194
+ * @deprecated Use {@link getColorByKey} instead
195
+ */
196
+ export function getColorByName(colorName) {
197
+ const color = colors.find((color) => color.name === colorName);
198
+ return color !== null && color !== void 0 ? color : defaultColor;
199
+ }
200
+ /**
201
+ * Retrieves a {@link Color} object by its key identifier.
202
+ *
203
+ * @param colorKey - The unique key identifier of the color to find (e.g., 'berry_red', 'sky_blue')
204
+ * @returns The matching Color object if found, otherwise returns the default color (charcoal)
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * const color = getColorByKey('berry_red');
209
+ * console.log(color.hexValue); // '#b8255f'
210
+ * ```
211
+ */
212
+ export function getColorByKey(colorKey) {
213
+ const color = colors.find((color) => color.key === colorKey);
214
+ return color !== null && color !== void 0 ? color : defaultColor;
215
+ }
@@ -0,0 +1,147 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { isNetworkError } from '../types/http.js';
13
+ /**
14
+ * Default retry configuration matching the original axios-retry behavior
15
+ */
16
+ const DEFAULT_RETRY_CONFIG = {
17
+ retries: 3,
18
+ retryCondition: isNetworkError,
19
+ retryDelay: (retryNumber) => {
20
+ // First retry: immediate (0ms delay)
21
+ // Subsequent retries: 500ms delay
22
+ return retryNumber === 1 ? 0 : 500;
23
+ },
24
+ };
25
+ /**
26
+ * Converts Headers object to a plain object
27
+ */
28
+ function headersToObject(headers) {
29
+ const result = {};
30
+ headers.forEach((value, key) => {
31
+ result[key] = value;
32
+ });
33
+ return result;
34
+ }
35
+ /**
36
+ * Creates an AbortSignal that times out after the specified duration
37
+ */
38
+ function createTimeoutSignal(timeoutMs, existingSignal) {
39
+ const controller = new AbortController();
40
+ // Timeout logic
41
+ const timeoutId = setTimeout(() => {
42
+ controller.abort(new Error(`Request timeout after ${timeoutMs}ms`));
43
+ }, timeoutMs);
44
+ // If there's an existing signal, forward its abort
45
+ if (existingSignal) {
46
+ if (existingSignal.aborted) {
47
+ clearTimeout(timeoutId);
48
+ controller.abort(existingSignal.reason);
49
+ }
50
+ else {
51
+ existingSignal.addEventListener('abort', () => {
52
+ clearTimeout(timeoutId);
53
+ controller.abort(existingSignal.reason);
54
+ }, { once: true });
55
+ }
56
+ }
57
+ // Clean up timeout when request completes
58
+ controller.signal.addEventListener('abort', () => {
59
+ clearTimeout(timeoutId);
60
+ });
61
+ return controller.signal;
62
+ }
63
+ /**
64
+ * Performs a fetch request with retry logic and timeout support
65
+ */
66
+ export async function fetchWithRetry(args) {
67
+ const { url, options = {}, retryConfig = {} } = args;
68
+ const config = Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), retryConfig);
69
+ const { timeout, signal: userSignal } = options, fetchOptions = __rest(options, ["timeout", "signal"]);
70
+ let lastError;
71
+ for (let attempt = 0; attempt <= config.retries; attempt++) {
72
+ try {
73
+ // Set up timeout and signal handling
74
+ let requestSignal = userSignal || undefined;
75
+ if (timeout && timeout > 0) {
76
+ requestSignal = createTimeoutSignal(timeout, requestSignal);
77
+ }
78
+ const response = await fetch(url, Object.assign(Object.assign({}, fetchOptions), { signal: requestSignal }));
79
+ // Check if the response is successful
80
+ if (!response.ok) {
81
+ const errorMessage = `HTTP ${response.status}: ${response.statusText}`;
82
+ const error = new Error(errorMessage);
83
+ error.status = response.status;
84
+ error.statusText = response.statusText;
85
+ error.response = {
86
+ data: undefined, // Will be set below if we can parse the response
87
+ status: response.status,
88
+ statusText: response.statusText,
89
+ headers: headersToObject(response.headers),
90
+ };
91
+ // Try to get response body for error details
92
+ try {
93
+ const responseText = await response.text();
94
+ let responseData;
95
+ try {
96
+ responseData = responseText ? JSON.parse(responseText) : undefined;
97
+ }
98
+ catch (_a) {
99
+ responseData = responseText;
100
+ }
101
+ error.data = responseData;
102
+ error.response.data = responseData;
103
+ }
104
+ catch (_b) {
105
+ // If we can't read the response body, that's OK
106
+ }
107
+ throw error;
108
+ }
109
+ // Parse response
110
+ const responseText = await response.text();
111
+ let data;
112
+ try {
113
+ data = responseText ? JSON.parse(responseText) : undefined;
114
+ }
115
+ catch (_c) {
116
+ // If JSON parsing fails, return the raw text
117
+ data = responseText;
118
+ }
119
+ return {
120
+ data,
121
+ status: response.status,
122
+ statusText: response.statusText,
123
+ headers: headersToObject(response.headers),
124
+ };
125
+ }
126
+ catch (error) {
127
+ lastError = error;
128
+ // Check if this error should trigger a retry
129
+ const shouldRetry = attempt < config.retries && config.retryCondition(lastError);
130
+ if (!shouldRetry) {
131
+ // Add network error flag for network errors
132
+ if (isNetworkError(lastError)) {
133
+ const networkError = lastError;
134
+ networkError.isNetworkError = true;
135
+ }
136
+ throw lastError;
137
+ }
138
+ // Wait before retrying
139
+ const delay = config.retryDelay(attempt + 1);
140
+ if (delay > 0) {
141
+ await new Promise((resolve) => setTimeout(resolve, delay));
142
+ }
143
+ }
144
+ }
145
+ // This should never be reached, but just in case
146
+ throw lastError || new Error('Request failed after retries');
147
+ }
@@ -0,0 +1,3 @@
1
+ export * from './colors.js';
2
+ export * from './sanitization.js';
3
+ export { getTaskUrl, getProjectUrl, getSectionUrl } from './url-helpers.js';
@@ -0,0 +1,120 @@
1
+ import FormData from 'form-data';
2
+ import { createReadStream } from 'fs';
3
+ import { basename } from 'path';
4
+ import { fetchWithRetry } from './fetch-with-retry.js';
5
+ /**
6
+ * Helper function to determine content-type from filename extension.
7
+ * @param fileName - The filename to analyze
8
+ * @returns The appropriate MIME type
9
+ */
10
+ function getContentTypeFromFileName(fileName) {
11
+ const extension = fileName.toLowerCase().split('.').pop();
12
+ switch (extension) {
13
+ case 'png':
14
+ return 'image/png';
15
+ case 'jpg':
16
+ case 'jpeg':
17
+ return 'image/jpeg';
18
+ case 'gif':
19
+ return 'image/gif';
20
+ case 'webp':
21
+ return 'image/webp';
22
+ case 'svg':
23
+ return 'image/svg+xml';
24
+ default:
25
+ return 'application/octet-stream';
26
+ }
27
+ }
28
+ /**
29
+ * Uploads a file using multipart/form-data.
30
+ *
31
+ * This is a shared utility for uploading files to Todoist endpoints that require
32
+ * multipart/form-data content type (e.g., file uploads, workspace logo uploads).
33
+ *
34
+ * @param baseUrl - The base API URL (e.g., https://api.todoist.com/api/v1/)
35
+ * @param authToken - The authentication token
36
+ * @param endpoint - The relative endpoint path (e.g., 'uploads', 'workspaces/logo')
37
+ * @param file - The file content (Buffer, ReadableStream, or file system path)
38
+ * @param fileName - Optional file name (required for Buffer/Stream, optional for paths)
39
+ * @param additionalFields - Additional form fields to include (e.g., project_id, workspace_id)
40
+ * @param requestId - Optional request ID for idempotency
41
+ * @returns The response data from the server
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // Upload from a file path
46
+ * const result = await uploadMultipartFile(
47
+ * 'https://api.todoist.com/api/v1/',
48
+ * 'my-token',
49
+ * 'uploads',
50
+ * '/path/to/file.pdf',
51
+ * undefined,
52
+ * { project_id: '12345' }
53
+ * )
54
+ *
55
+ * // Upload from a Buffer
56
+ * const buffer = Buffer.from('file content')
57
+ * const result = await uploadMultipartFile(
58
+ * 'https://api.todoist.com/api/v1/',
59
+ * 'my-token',
60
+ * 'uploads',
61
+ * buffer,
62
+ * 'document.pdf',
63
+ * { project_id: '12345' }
64
+ * )
65
+ * ```
66
+ */
67
+ export async function uploadMultipartFile(args) {
68
+ const { baseUrl, authToken, endpoint, file, fileName, additionalFields, requestId } = args;
69
+ const form = new FormData();
70
+ // Determine file type and add to form data
71
+ if (typeof file === 'string') {
72
+ // File path - create read stream
73
+ const filePath = file;
74
+ const resolvedFileName = fileName || basename(filePath);
75
+ form.append('file', createReadStream(filePath), resolvedFileName);
76
+ }
77
+ else if (Buffer.isBuffer(file)) {
78
+ // Buffer - require fileName
79
+ if (!fileName) {
80
+ throw new Error('fileName is required when uploading from a Buffer');
81
+ }
82
+ // Detect content-type from filename extension
83
+ const contentType = getContentTypeFromFileName(fileName);
84
+ form.append('file', file, {
85
+ filename: fileName,
86
+ contentType: contentType,
87
+ });
88
+ }
89
+ else {
90
+ // Stream - require fileName
91
+ if (!fileName) {
92
+ throw new Error('fileName is required when uploading from a stream');
93
+ }
94
+ form.append('file', file, fileName);
95
+ }
96
+ // Add additional fields to the form
97
+ for (const [key, value] of Object.entries(additionalFields)) {
98
+ if (value !== undefined && value !== null) {
99
+ form.append(key, value.toString());
100
+ }
101
+ }
102
+ // Build the full URL
103
+ const url = `${baseUrl}${endpoint}`;
104
+ // Prepare headers
105
+ const headers = Object.assign({ Authorization: `Bearer ${authToken}` }, form.getHeaders());
106
+ if (requestId) {
107
+ headers['X-Request-Id'] = requestId;
108
+ }
109
+ // Make the request using fetch
110
+ const response = await fetchWithRetry({
111
+ url,
112
+ options: {
113
+ method: 'POST',
114
+ body: form, // FormData from 'form-data' package is compatible with fetch
115
+ headers,
116
+ timeout: 30000, // 30 second timeout for file uploads
117
+ },
118
+ });
119
+ return response.data;
120
+ }
@@ -0,0 +1,12 @@
1
+ import camelcase from 'camelcase';
2
+ import emojiRegex from 'emoji-regex';
3
+ function isEmojiKey(key) {
4
+ const regex = emojiRegex();
5
+ return regex.test(key);
6
+ }
7
+ export function customCamelCase(input) {
8
+ // If the value is a solitary emoji string, return the key as-is
9
+ if (isEmojiKey(input))
10
+ return input;
11
+ return camelcase(input);
12
+ }
@@ -0,0 +1,112 @@
1
+ const BOLD_FORMAT = /(^|[\s!?,;>:]+)(?:\*\*|__|!!)(.+?)(\*\*|__|!!)(?=$|[\s!?,;><:]+)/gi;
2
+ const ITALIC_FORMAT = /(^|[\s!?,;>:]+)(?:\*|_|!)(.+?)(\*|_|!)(?=$|[\s!?,;><:]+)/gi;
3
+ const BOLD_ITALIC_FORMAT = /(^|[\s!?,;>:]+)(?:\*\*\*|___|!!!)(.+?)(\*\*\*|___|!!!)(?=$|[\s!?,;><:]+)/gi;
4
+ const CODE_BLOCK_FORMAT = /```([\s\S]*?)```/gi;
5
+ const CODE_INLINE_FORMAT = /`([^`]+)`/gi;
6
+ const TODOIST_LINK = /((?:(?:onenote:)?[\w-]+):\/\/[^\s]+)\s+[[(]([^)]+)[\])]/gi;
7
+ const MARKDOWN_LINK = /\[(.+?)\]\((.+?)\)/gi;
8
+ const GMAIL_LINK = /\[\[gmail=(.+?),\s*(.+?)\]\]/gi;
9
+ const OUTLOOK_LINK = /\[\[outlook=(.+?),\s*(.+?)\]\]/gi;
10
+ const THUNDERBIRD_LINK = /\[\[thunderbird\n(.+)\n(.+)\n\s*\]\]/gi;
11
+ const FAKE_SECTION_PREFIX = '* ';
12
+ const FAKE_SECTION_SUFFIX = ':';
13
+ function removeStyleFormatting(input) {
14
+ if (!input.includes('!') && !input.includes('*') && !input.includes('_')) {
15
+ return input;
16
+ }
17
+ function removeMarkdown(match, prefix, text) {
18
+ return `${prefix}${text}`;
19
+ }
20
+ input = input.replace(BOLD_ITALIC_FORMAT, removeMarkdown);
21
+ input = input.replace(BOLD_FORMAT, removeMarkdown);
22
+ input = input.replace(ITALIC_FORMAT, removeMarkdown);
23
+ return input;
24
+ }
25
+ function removeCodeFormatting(input) {
26
+ function removeMarkdown(match, text) {
27
+ return text;
28
+ }
29
+ input = input.replace(CODE_BLOCK_FORMAT, removeMarkdown);
30
+ input = input.replace(CODE_INLINE_FORMAT, removeMarkdown);
31
+ return input;
32
+ }
33
+ function removeFakeSectionFormatting(input) {
34
+ if (input.startsWith(FAKE_SECTION_PREFIX)) {
35
+ input = input.slice(FAKE_SECTION_PREFIX.length);
36
+ }
37
+ if (input.endsWith(FAKE_SECTION_SUFFIX)) {
38
+ input = input.slice(0, input.length - FAKE_SECTION_SUFFIX.length);
39
+ }
40
+ return input;
41
+ }
42
+ function removeMarkdownLinks(input) {
43
+ if (!input.includes('[') || !input.includes(']')) {
44
+ return input;
45
+ }
46
+ function removeMarkdown(match, text) {
47
+ return text;
48
+ }
49
+ return input.replace(MARKDOWN_LINK, removeMarkdown);
50
+ }
51
+ function removeTodoistLinks(input) {
52
+ if (!input.includes('(') || !input.includes(')')) {
53
+ return input;
54
+ }
55
+ function removeMarkdown(match, url, text) {
56
+ return text;
57
+ }
58
+ return input.replace(TODOIST_LINK, removeMarkdown);
59
+ }
60
+ function removeAppLinks(input) {
61
+ if (input.includes('gmail')) {
62
+ input = input.replace(GMAIL_LINK, (match, id, text) => text);
63
+ }
64
+ if (input.includes('outlook')) {
65
+ input = input.replace(OUTLOOK_LINK, (match, id, text) => text);
66
+ }
67
+ if (input.includes('thunderbird')) {
68
+ input = input.replace(THUNDERBIRD_LINK, (match, text) => text);
69
+ }
70
+ return input;
71
+ }
72
+ /**
73
+ * Sanitizes a string by removing Todoist's formatting syntax (e.g. bold, italic, code blocks, links).
74
+ *
75
+ * @example
76
+ * // Removes bold/italic formatting
77
+ * getSanitizedContent('Some **bold** and *italic*') // 'Some bold and italic'
78
+ *
79
+ * // Removes markdown links
80
+ * getSanitizedContent('A [markdown](http://url.com) link') // 'A markdown link'
81
+ *
82
+ * // Removes app-specific links
83
+ * getSanitizedContent('A [[gmail=id, link from gmail]]') // 'A link from gmail'
84
+ *
85
+ * @param input - The string to sanitize
86
+ * @returns The sanitized string with all formatting removed
87
+ */
88
+ export function getSanitizedContent(input) {
89
+ input = removeStyleFormatting(input);
90
+ input = removeCodeFormatting(input);
91
+ input = removeFakeSectionFormatting(input);
92
+ input = removeMarkdownLinks(input);
93
+ input = removeTodoistLinks(input);
94
+ input = removeAppLinks(input);
95
+ return input;
96
+ }
97
+ /**
98
+ * Takes an array of tasks and returns a new array with sanitized content
99
+ * added as 'sanitizedContent' property to each task.
100
+ *
101
+ * @see {@link getSanitizedContent}
102
+ *
103
+ * @example
104
+ * const tasks = [{ content: '**Bold** task', ... }]
105
+ * getSanitizedTasks(tasks) // [{ content: '**Bold** task', sanitizedContent: 'Bold task', ... }]
106
+ *
107
+ * @param tasks - Array of Task objects to sanitize
108
+ * @returns Array of tasks with added sanitizedContent property
109
+ */
110
+ export function getSanitizedTasks(tasks) {
111
+ return tasks.map((task) => (Object.assign(Object.assign({}, task), { sanitizedContent: getSanitizedContent(task.content) })));
112
+ }
@@ -0,0 +1,68 @@
1
+ import { TODOIST_WEB_URI } from '../consts/endpoints.js';
2
+ /**
3
+ * Formats a Date object to YYYY-MM-DD string format.
4
+ *
5
+ * @internal
6
+ * @param date The Date object to format.
7
+ * @returns The formatted date string in YYYY-MM-DD format.
8
+ */
9
+ export function formatDateToYYYYMMDD(date) {
10
+ const year = date.getFullYear();
11
+ const month = String(date.getMonth() + 1).padStart(2, '0');
12
+ const day = String(date.getDate()).padStart(2, '0');
13
+ return `${year}-${month}-${day}`;
14
+ }
15
+ /**
16
+ * Generate the URL for a given task.
17
+ *
18
+ * @param taskId The ID of the task.
19
+ * @param content The content of the task.
20
+ * @returns The URL string for the task view.
21
+ */
22
+ export function getTaskUrl(taskId, content) {
23
+ const slug = content ? slugify(content) : undefined;
24
+ const path = slug ? `${slug}-${taskId}` : taskId;
25
+ return `${TODOIST_WEB_URI}/task/${path}`;
26
+ }
27
+ /**
28
+ * Generate the URL for a given project.
29
+ *
30
+ * @param projectId The ID of the project.
31
+ * @param name The name of the project.
32
+ * @returns The URL string for the project view.
33
+ */
34
+ export function getProjectUrl(projectId, name) {
35
+ const slug = name ? slugify(name) : undefined;
36
+ const path = slug ? `${slug}-${projectId}` : projectId;
37
+ return `${TODOIST_WEB_URI}/project/${path}`;
38
+ }
39
+ /**
40
+ * Generate the URL for a given section.
41
+ *
42
+ * @param sectionId The ID of the section.
43
+ * @param name The name of the section.
44
+ * @returns The URL string for the section view.
45
+ */
46
+ export function getSectionUrl(sectionId, name) {
47
+ const slug = name ? slugify(name) : undefined;
48
+ const path = slug ? `${slug}-${sectionId}` : sectionId;
49
+ return `${TODOIST_WEB_URI}/section/${path}`;
50
+ }
51
+ /**
52
+ * Slugify function borrowed from Django.
53
+ *
54
+ * @param value The string to slugify.
55
+ * @returns The slugified string.
56
+ */
57
+ function slugify(value) {
58
+ // Convert to ASCII
59
+ let result = value.normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
60
+ // Remove non-ASCII characters
61
+ result = result.replace(/[^\x20-\x7E]/g, '');
62
+ // Convert to lowercase and replace non-alphanumeric characters with dashes
63
+ result = result.toLowerCase().replace(/[^\w\s-]/g, '');
64
+ // Replace spaces and repeated dashes with single dashes
65
+ result = result.replace(/[-\s]+/g, '-');
66
+ // Strip dashes from the beginning and end
67
+ return result.replace(/^-+|-+$/g, '');
68
+ }