sunsama-api 0.12.0 → 0.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.
Files changed (204) hide show
  1. package/README.md +27 -0
  2. package/dist/cjs/{client → src/client}/index.js +320 -24
  3. package/dist/cjs/src/client/index.js.map +1 -0
  4. package/dist/cjs/src/errors/index.js.map +1 -0
  5. package/dist/cjs/src/index.js.map +1 -0
  6. package/dist/cjs/src/queries/fragments/index.js.map +1 -0
  7. package/dist/cjs/src/queries/fragments/mutation-responses.js.map +1 -0
  8. package/dist/cjs/src/queries/fragments/stream.js.map +1 -0
  9. package/dist/cjs/src/queries/fragments/task.js.map +1 -0
  10. package/dist/cjs/src/queries/index.js.map +1 -0
  11. package/dist/cjs/src/queries/streams/index.js.map +1 -0
  12. package/dist/cjs/src/queries/streams/queries.js.map +1 -0
  13. package/dist/cjs/src/queries/tasks/index.js.map +1 -0
  14. package/dist/cjs/{queries → src/queries}/tasks/mutations.js +74 -1
  15. package/dist/cjs/src/queries/tasks/mutations.js.map +1 -0
  16. package/dist/cjs/src/queries/tasks/queries.js.map +1 -0
  17. package/dist/cjs/src/queries/user/index.js.map +1 -0
  18. package/dist/cjs/{queries → src/queries}/user/queries.js.map +1 -1
  19. package/dist/{esm → cjs/src}/types/api.js.map +1 -1
  20. package/dist/cjs/src/types/client.js.map +1 -0
  21. package/dist/cjs/src/types/common.js.map +1 -0
  22. package/dist/cjs/src/types/index.js.map +1 -0
  23. package/dist/cjs/src/utils/conversion.js +693 -0
  24. package/dist/cjs/src/utils/conversion.js.map +1 -0
  25. package/dist/cjs/src/utils/index.js.map +1 -0
  26. package/dist/cjs/src/utils/validation.js.map +1 -0
  27. package/dist/cjs/vitest.config.js +32 -0
  28. package/dist/cjs/vitest.config.js.map +1 -0
  29. package/dist/esm/{client → src/client}/index.js +322 -26
  30. package/dist/esm/src/client/index.js.map +1 -0
  31. package/dist/esm/src/errors/index.js.map +1 -0
  32. package/dist/esm/src/index.js.map +1 -0
  33. package/dist/esm/src/queries/fragments/index.js.map +1 -0
  34. package/dist/esm/src/queries/fragments/mutation-responses.js.map +1 -0
  35. package/dist/esm/src/queries/fragments/stream.js.map +1 -0
  36. package/dist/esm/src/queries/fragments/task.js.map +1 -0
  37. package/dist/esm/src/queries/index.js.map +1 -0
  38. package/dist/esm/src/queries/streams/index.js.map +1 -0
  39. package/dist/esm/src/queries/streams/queries.js.map +1 -0
  40. package/dist/esm/src/queries/tasks/index.js.map +1 -0
  41. package/dist/esm/{queries → src/queries}/tasks/mutations.js +73 -0
  42. package/dist/esm/src/queries/tasks/mutations.js.map +1 -0
  43. package/dist/esm/src/queries/tasks/queries.js.map +1 -0
  44. package/dist/esm/src/queries/user/index.js.map +1 -0
  45. package/dist/esm/{queries → src/queries}/user/queries.js.map +1 -1
  46. package/dist/{cjs → esm/src}/types/api.js.map +1 -1
  47. package/dist/esm/src/types/client.js.map +1 -0
  48. package/dist/esm/src/types/common.js.map +1 -0
  49. package/dist/esm/src/types/index.js.map +1 -0
  50. package/dist/esm/src/utils/conversion.js +684 -0
  51. package/dist/esm/src/utils/conversion.js.map +1 -0
  52. package/dist/esm/src/utils/index.js.map +1 -0
  53. package/dist/esm/src/utils/validation.js.map +1 -0
  54. package/dist/esm/vitest.config.js +30 -0
  55. package/dist/esm/vitest.config.js.map +1 -0
  56. package/dist/types/{client → src/client}/index.d.ts +117 -0
  57. package/dist/types/src/client/index.d.ts.map +1 -0
  58. package/dist/types/src/errors/index.d.ts.map +1 -0
  59. package/dist/types/src/index.d.ts.map +1 -0
  60. package/dist/types/src/queries/fragments/index.d.ts.map +1 -0
  61. package/dist/types/src/queries/fragments/mutation-responses.d.ts.map +1 -0
  62. package/dist/types/src/queries/fragments/stream.d.ts.map +1 -0
  63. package/dist/types/src/queries/fragments/task.d.ts.map +1 -0
  64. package/dist/types/src/queries/index.d.ts.map +1 -0
  65. package/dist/types/src/queries/streams/index.d.ts.map +1 -0
  66. package/dist/types/src/queries/streams/queries.d.ts.map +1 -0
  67. package/dist/types/src/queries/tasks/index.d.ts.map +1 -0
  68. package/dist/types/{queries → src/queries}/tasks/mutations.d.ts +41 -0
  69. package/dist/types/src/queries/tasks/mutations.d.ts.map +1 -0
  70. package/dist/types/src/queries/tasks/queries.d.ts.map +1 -0
  71. package/dist/types/src/queries/user/index.d.ts.map +1 -0
  72. package/dist/types/src/queries/user/queries.d.ts.map +1 -0
  73. package/dist/types/{types → src/types}/api.d.ts +57 -0
  74. package/dist/types/src/types/api.d.ts.map +1 -0
  75. package/dist/types/src/types/client.d.ts.map +1 -0
  76. package/dist/types/src/types/common.d.ts.map +1 -0
  77. package/dist/types/src/types/index.d.ts.map +1 -0
  78. package/dist/types/{utils → src/utils}/conversion.d.ts +102 -0
  79. package/dist/types/src/utils/conversion.d.ts.map +1 -0
  80. package/dist/types/src/utils/index.d.ts.map +1 -0
  81. package/dist/types/src/utils/validation.d.ts.map +1 -0
  82. package/dist/types/vitest.config.d.ts +3 -0
  83. package/dist/types/vitest.config.d.ts.map +1 -0
  84. package/package.json +1 -1
  85. package/dist/cjs/client/index.js.map +0 -1
  86. package/dist/cjs/errors/index.js.map +0 -1
  87. package/dist/cjs/index.js.map +0 -1
  88. package/dist/cjs/queries/fragments/index.js.map +0 -1
  89. package/dist/cjs/queries/fragments/mutation-responses.js.map +0 -1
  90. package/dist/cjs/queries/fragments/stream.js.map +0 -1
  91. package/dist/cjs/queries/fragments/task.js.map +0 -1
  92. package/dist/cjs/queries/index.js.map +0 -1
  93. package/dist/cjs/queries/streams/index.js.map +0 -1
  94. package/dist/cjs/queries/streams/queries.js.map +0 -1
  95. package/dist/cjs/queries/tasks/index.js.map +0 -1
  96. package/dist/cjs/queries/tasks/mutations.js.map +0 -1
  97. package/dist/cjs/queries/tasks/queries.js.map +0 -1
  98. package/dist/cjs/queries/user/index.js.map +0 -1
  99. package/dist/cjs/types/client.js.map +0 -1
  100. package/dist/cjs/types/common.js.map +0 -1
  101. package/dist/cjs/types/index.js.map +0 -1
  102. package/dist/cjs/utils/conversion.js +0 -236
  103. package/dist/cjs/utils/conversion.js.map +0 -1
  104. package/dist/cjs/utils/index.js.map +0 -1
  105. package/dist/cjs/utils/validation.js.map +0 -1
  106. package/dist/esm/client/index.js.map +0 -1
  107. package/dist/esm/errors/index.js.map +0 -1
  108. package/dist/esm/index.js.map +0 -1
  109. package/dist/esm/queries/fragments/index.js.map +0 -1
  110. package/dist/esm/queries/fragments/mutation-responses.js.map +0 -1
  111. package/dist/esm/queries/fragments/stream.js.map +0 -1
  112. package/dist/esm/queries/fragments/task.js.map +0 -1
  113. package/dist/esm/queries/index.js.map +0 -1
  114. package/dist/esm/queries/streams/index.js.map +0 -1
  115. package/dist/esm/queries/streams/queries.js.map +0 -1
  116. package/dist/esm/queries/tasks/index.js.map +0 -1
  117. package/dist/esm/queries/tasks/mutations.js.map +0 -1
  118. package/dist/esm/queries/tasks/queries.js.map +0 -1
  119. package/dist/esm/queries/user/index.js.map +0 -1
  120. package/dist/esm/types/client.js.map +0 -1
  121. package/dist/esm/types/common.js.map +0 -1
  122. package/dist/esm/types/index.js.map +0 -1
  123. package/dist/esm/utils/conversion.js +0 -229
  124. package/dist/esm/utils/conversion.js.map +0 -1
  125. package/dist/esm/utils/index.js.map +0 -1
  126. package/dist/esm/utils/validation.js.map +0 -1
  127. package/dist/types/client/index.d.ts.map +0 -1
  128. package/dist/types/errors/index.d.ts.map +0 -1
  129. package/dist/types/index.d.ts.map +0 -1
  130. package/dist/types/queries/fragments/index.d.ts.map +0 -1
  131. package/dist/types/queries/fragments/mutation-responses.d.ts.map +0 -1
  132. package/dist/types/queries/fragments/stream.d.ts.map +0 -1
  133. package/dist/types/queries/fragments/task.d.ts.map +0 -1
  134. package/dist/types/queries/index.d.ts.map +0 -1
  135. package/dist/types/queries/streams/index.d.ts.map +0 -1
  136. package/dist/types/queries/streams/queries.d.ts.map +0 -1
  137. package/dist/types/queries/tasks/index.d.ts.map +0 -1
  138. package/dist/types/queries/tasks/mutations.d.ts.map +0 -1
  139. package/dist/types/queries/tasks/queries.d.ts.map +0 -1
  140. package/dist/types/queries/user/index.d.ts.map +0 -1
  141. package/dist/types/queries/user/queries.d.ts.map +0 -1
  142. package/dist/types/types/api.d.ts.map +0 -1
  143. package/dist/types/types/client.d.ts.map +0 -1
  144. package/dist/types/types/common.d.ts.map +0 -1
  145. package/dist/types/types/index.d.ts.map +0 -1
  146. package/dist/types/utils/conversion.d.ts.map +0 -1
  147. package/dist/types/utils/index.d.ts.map +0 -1
  148. package/dist/types/utils/validation.d.ts.map +0 -1
  149. /package/dist/cjs/{errors → src/errors}/index.js +0 -0
  150. /package/dist/cjs/{index.js → src/index.js} +0 -0
  151. /package/dist/cjs/{queries → src/queries}/fragments/index.js +0 -0
  152. /package/dist/cjs/{queries → src/queries}/fragments/mutation-responses.js +0 -0
  153. /package/dist/cjs/{queries → src/queries}/fragments/stream.js +0 -0
  154. /package/dist/cjs/{queries → src/queries}/fragments/task.js +0 -0
  155. /package/dist/cjs/{queries → src/queries}/index.js +0 -0
  156. /package/dist/cjs/{queries → src/queries}/streams/index.js +0 -0
  157. /package/dist/cjs/{queries → src/queries}/streams/queries.js +0 -0
  158. /package/dist/cjs/{queries → src/queries}/tasks/index.js +0 -0
  159. /package/dist/cjs/{queries → src/queries}/tasks/queries.js +0 -0
  160. /package/dist/cjs/{queries → src/queries}/user/index.js +0 -0
  161. /package/dist/cjs/{queries → src/queries}/user/queries.js +0 -0
  162. /package/dist/cjs/{types → src/types}/api.js +0 -0
  163. /package/dist/cjs/{types → src/types}/client.js +0 -0
  164. /package/dist/cjs/{types → src/types}/common.js +0 -0
  165. /package/dist/cjs/{types → src/types}/index.js +0 -0
  166. /package/dist/cjs/{utils → src/utils}/index.js +0 -0
  167. /package/dist/cjs/{utils → src/utils}/validation.js +0 -0
  168. /package/dist/esm/{errors → src/errors}/index.js +0 -0
  169. /package/dist/esm/{index.js → src/index.js} +0 -0
  170. /package/dist/esm/{queries → src/queries}/fragments/index.js +0 -0
  171. /package/dist/esm/{queries → src/queries}/fragments/mutation-responses.js +0 -0
  172. /package/dist/esm/{queries → src/queries}/fragments/stream.js +0 -0
  173. /package/dist/esm/{queries → src/queries}/fragments/task.js +0 -0
  174. /package/dist/esm/{queries → src/queries}/index.js +0 -0
  175. /package/dist/esm/{queries → src/queries}/streams/index.js +0 -0
  176. /package/dist/esm/{queries → src/queries}/streams/queries.js +0 -0
  177. /package/dist/esm/{queries → src/queries}/tasks/index.js +0 -0
  178. /package/dist/esm/{queries → src/queries}/tasks/queries.js +0 -0
  179. /package/dist/esm/{queries → src/queries}/user/index.js +0 -0
  180. /package/dist/esm/{queries → src/queries}/user/queries.js +0 -0
  181. /package/dist/esm/{types → src/types}/api.js +0 -0
  182. /package/dist/esm/{types → src/types}/client.js +0 -0
  183. /package/dist/esm/{types → src/types}/common.js +0 -0
  184. /package/dist/esm/{types → src/types}/index.js +0 -0
  185. /package/dist/esm/{utils → src/utils}/index.js +0 -0
  186. /package/dist/esm/{utils → src/utils}/validation.js +0 -0
  187. /package/dist/types/{errors → src/errors}/index.d.ts +0 -0
  188. /package/dist/types/{index.d.ts → src/index.d.ts} +0 -0
  189. /package/dist/types/{queries → src/queries}/fragments/index.d.ts +0 -0
  190. /package/dist/types/{queries → src/queries}/fragments/mutation-responses.d.ts +0 -0
  191. /package/dist/types/{queries → src/queries}/fragments/stream.d.ts +0 -0
  192. /package/dist/types/{queries → src/queries}/fragments/task.d.ts +0 -0
  193. /package/dist/types/{queries → src/queries}/index.d.ts +0 -0
  194. /package/dist/types/{queries → src/queries}/streams/index.d.ts +0 -0
  195. /package/dist/types/{queries → src/queries}/streams/queries.d.ts +0 -0
  196. /package/dist/types/{queries → src/queries}/tasks/index.d.ts +0 -0
  197. /package/dist/types/{queries → src/queries}/tasks/queries.d.ts +0 -0
  198. /package/dist/types/{queries → src/queries}/user/index.d.ts +0 -0
  199. /package/dist/types/{queries → src/queries}/user/queries.d.ts +0 -0
  200. /package/dist/types/{types → src/types}/client.d.ts +0 -0
  201. /package/dist/types/{types → src/types}/common.d.ts +0 -0
  202. /package/dist/types/{types → src/types}/index.d.ts +0 -0
  203. /package/dist/types/{utils → src/utils}/index.d.ts +0 -0
  204. /package/dist/types/{utils → src/utils}/validation.d.ts +0 -0
@@ -0,0 +1,684 @@
1
+ /**
2
+ * HTML ↔ Markdown Conversion Utilities
3
+ *
4
+ * This module provides utilities for converting between HTML and Markdown formats.
5
+ * It uses specialized libraries for optimal performance:
6
+ * - Turndown for HTML → Markdown conversion
7
+ * - Marked for Markdown → HTML conversion
8
+ *
9
+ * These utilities are particularly useful for Sunsama API task notes and comments
10
+ * where content can be provided in either format and needs conversion to the other.
11
+ */
12
+ import { marked } from 'marked';
13
+ import TurndownService from 'turndown';
14
+ import { z } from 'zod';
15
+ import { SunsamaAuthError } from '../errors/index.js';
16
+ /**
17
+ * Decodes common HTML entities back to their original characters.
18
+ * This is needed because marked's lexer HTML-encodes some characters.
19
+ */
20
+ function decodeHtmlEntities(text) {
21
+ return text
22
+ .replace(/&/g, '&')
23
+ .replace(/&lt;/g, '<')
24
+ .replace(/&gt;/g, '>')
25
+ .replace(/&quot;/g, '"')
26
+ .replace(/&#39;/g, "'")
27
+ .replace(/&#x27;/g, "'")
28
+ .replace(/&apos;/g, "'")
29
+ .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)))
30
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
31
+ }
32
+ /**
33
+ * Validation schema for HTML input
34
+ */
35
+ const htmlInputSchema = z.string().trim().min(1, 'HTML content cannot be empty');
36
+ /**
37
+ * Validation schema for Markdown input
38
+ */
39
+ const markdownInputSchema = z.string().trim().min(1, 'Markdown content cannot be empty');
40
+ /**
41
+ * Default configuration for Turndown (HTML → Markdown)
42
+ */
43
+ const defaultTurndownOptions = {
44
+ preserveHtml: false,
45
+ gfm: true,
46
+ linkStyle: 'inlined',
47
+ br: '\n',
48
+ };
49
+ /**
50
+ * Default configuration for Marked (Markdown → HTML)
51
+ */
52
+ const defaultMarkedOptions = {
53
+ sanitize: true,
54
+ gfm: true,
55
+ breaks: true,
56
+ };
57
+ /**
58
+ * Initialize Turndown service with configuration
59
+ */
60
+ function createTurndownService(options = {}) {
61
+ const config = { ...defaultTurndownOptions, ...options };
62
+ const turndownService = new TurndownService({
63
+ headingStyle: 'atx',
64
+ hr: '---',
65
+ bulletListMarker: '-',
66
+ codeBlockStyle: 'fenced',
67
+ fence: '```',
68
+ emDelimiter: '*',
69
+ strongDelimiter: '**',
70
+ linkStyle: config.linkStyle,
71
+ linkReferenceStyle: 'full',
72
+ br: config.br,
73
+ });
74
+ // Add GitHub Flavored Markdown support
75
+ if (config.gfm) {
76
+ // Support for strikethrough
77
+ turndownService.addRule('strikethrough', {
78
+ filter: ['del', 's'],
79
+ replacement: function (content) {
80
+ return '~~' + content + '~~';
81
+ },
82
+ });
83
+ // Support for task lists
84
+ turndownService.addRule('taskListItems', {
85
+ filter: function (node) {
86
+ return (node.nodeName === 'LI' &&
87
+ node.querySelector &&
88
+ node.querySelector('input[type="checkbox"]') !== null);
89
+ },
90
+ replacement: function (content, node) {
91
+ const checkbox = node.querySelector
92
+ ? node.querySelector('input[type="checkbox"]')
93
+ : null;
94
+ const isChecked = checkbox && checkbox.checked;
95
+ return (isChecked ? '- [x] ' : '- [ ] ') + content;
96
+ },
97
+ });
98
+ }
99
+ // Apply custom rules if provided
100
+ if (config.customRules) {
101
+ Object.entries(config.customRules).forEach(([name, rule]) => {
102
+ turndownService.addRule(name, rule);
103
+ });
104
+ }
105
+ return turndownService;
106
+ }
107
+ /**
108
+ * Initialize Marked with configuration
109
+ */
110
+ function configureMarked(options = {}) {
111
+ const config = { ...defaultMarkedOptions, ...options };
112
+ marked.setOptions({
113
+ gfm: config.gfm,
114
+ breaks: config.breaks,
115
+ // Note: sanitize option is deprecated in newer versions of marked
116
+ // We'll handle sanitization separately if needed
117
+ });
118
+ if (config.renderer) {
119
+ marked.use({ renderer: config.renderer });
120
+ }
121
+ }
122
+ /**
123
+ * Converts HTML content to Markdown format
124
+ *
125
+ * @param html - The HTML content to convert
126
+ * @param options - Configuration options for conversion
127
+ * @returns The converted Markdown content
128
+ * @throws SunsamaAuthError if input validation fails
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const html = '<h1>Hello World</h1><p>This is <strong>bold</strong> text.</p>';
133
+ * const markdown = htmlToMarkdown(html);
134
+ * console.log(markdown); // "# Hello World\n\nThis is **bold** text."
135
+ * ```
136
+ */
137
+ export function htmlToMarkdown(html, options = {}) {
138
+ try {
139
+ // Validate input
140
+ htmlInputSchema.parse(html);
141
+ // Create Turndown service with options
142
+ const turndownService = createTurndownService(options);
143
+ // Convert HTML to Markdown
144
+ const markdown = turndownService.turndown(html);
145
+ // Clean up the result (remove excessive whitespace)
146
+ return markdown.trim().replace(/\n{3,}/g, '\n\n');
147
+ }
148
+ catch (error) {
149
+ if (error instanceof z.ZodError) {
150
+ throw new SunsamaAuthError(`HTML to Markdown conversion failed: ${error.message}`);
151
+ }
152
+ throw new SunsamaAuthError(`HTML to Markdown conversion failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
153
+ }
154
+ }
155
+ /**
156
+ * Converts Markdown content to HTML format
157
+ *
158
+ * @param markdown - The Markdown content to convert
159
+ * @param options - Configuration options for conversion
160
+ * @returns The converted HTML content
161
+ * @throws SunsamaAuthError if input validation fails
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const markdown = '# Hello World\n\nThis is **bold** text.';
166
+ * const html = markdownToHtml(markdown);
167
+ * console.log(html); // "<h1>Hello World</h1>\n<p>This is <strong>bold</strong> text.</p>"
168
+ * ```
169
+ */
170
+ export function markdownToHtml(markdown, options = {}) {
171
+ try {
172
+ // Validate input
173
+ markdownInputSchema.parse(markdown);
174
+ // Configure Marked with options
175
+ configureMarked(options);
176
+ // Convert Markdown to HTML
177
+ const html = marked.parse(markdown);
178
+ // Return the result (marked.parse returns a Promise<string> in some versions, but string in others)
179
+ return typeof html === 'string' ? html : html.toString();
180
+ }
181
+ catch (error) {
182
+ if (error instanceof z.ZodError) {
183
+ throw new SunsamaAuthError(`Markdown to HTML conversion failed: ${error.message}`);
184
+ }
185
+ throw new SunsamaAuthError(`Markdown to HTML conversion failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
186
+ }
187
+ }
188
+ /**
189
+ * Sanitizes HTML content to prevent XSS attacks
190
+ * This is a basic implementation - consider using a dedicated library like DOMPurify for production
191
+ *
192
+ * @param html - The HTML content to sanitize
193
+ * @returns Sanitized HTML content
194
+ */
195
+ export function sanitizeHtml(html) {
196
+ if (!html)
197
+ return '';
198
+ // Basic HTML sanitization - remove script tags and dangerous attributes
199
+ let sanitized = html
200
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
201
+ .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
202
+ .replace(/on\w+\s*=\s*"[^"]*"/gi, '')
203
+ .replace(/on\w+\s*=\s*'[^']*'/gi, '')
204
+ .replace(/on\w+\s*=\s*[^\s>]+/gi, '')
205
+ .replace(/javascript:/gi, '')
206
+ .replace(/vbscript:/gi, '')
207
+ .replace(/data:/gi, '');
208
+ // Clean up any extra spaces left behind after removing attributes
209
+ sanitized = sanitized.replace(/\s+>/g, '>').replace(/<\s+/g, '<');
210
+ return sanitized;
211
+ }
212
+ /**
213
+ * Utility function to safely convert between HTML and Markdown with validation
214
+ *
215
+ * @param content - The content to convert
216
+ * @param fromFormat - Source format ('html' or 'markdown')
217
+ * @param toFormat - Target format ('html' or 'markdown')
218
+ * @param options - Conversion options
219
+ * @returns Converted content
220
+ * @throws SunsamaAuthError if conversion fails or formats are invalid
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * const html = '<p>Hello <strong>world</strong></p>';
225
+ * const markdown = convertContent(html, 'html', 'markdown');
226
+ * console.log(markdown); // "Hello **world**"
227
+ *
228
+ * const convertedBack = convertContent(markdown, 'markdown', 'html');
229
+ * console.log(convertedBack); // "<p>Hello <strong>world</strong></p>"
230
+ * ```
231
+ */
232
+ export function convertContent(content, fromFormat, toFormat, options = {}) {
233
+ if (fromFormat === toFormat) {
234
+ return content; // No conversion needed
235
+ }
236
+ if (fromFormat === 'html' && toFormat === 'markdown') {
237
+ return htmlToMarkdown(content, options.htmlToMarkdown);
238
+ }
239
+ if (fromFormat === 'markdown' && toFormat === 'html') {
240
+ const html = markdownToHtml(content, options.markdownToHtml);
241
+ return options.markdownToHtml?.sanitize !== false ? sanitizeHtml(html) : html;
242
+ }
243
+ throw new SunsamaAuthError(`Invalid conversion format: ${fromFormat} to ${toFormat}`);
244
+ }
245
+ /**
246
+ * Parses inline tokens from marked and converts them to formatted segments
247
+ * with Yjs-compatible attributes
248
+ *
249
+ * @param tokens - Array of marked inline tokens
250
+ * @param inheritedAttributes - Attributes inherited from parent elements
251
+ * @returns Array of formatted segments with text and attributes
252
+ * @internal
253
+ */
254
+ function parseInlineTokens(tokens, inheritedAttributes = {}) {
255
+ const segments = [];
256
+ for (const token of tokens) {
257
+ switch (token.type) {
258
+ case 'text': {
259
+ const textToken = token;
260
+ // Handle text tokens that may have nested tokens (from inline parsing)
261
+ if ('tokens' in textToken && textToken.tokens && textToken.tokens.length > 0) {
262
+ segments.push(...parseInlineTokens(textToken.tokens, inheritedAttributes));
263
+ }
264
+ else {
265
+ const attrs = Object.keys(inheritedAttributes).length > 0 ? inheritedAttributes : undefined;
266
+ segments.push({ text: decodeHtmlEntities(textToken.text), attributes: attrs });
267
+ }
268
+ break;
269
+ }
270
+ case 'strong': {
271
+ const strongToken = token;
272
+ const newAttrs = { ...inheritedAttributes, bold: true };
273
+ if ('tokens' in strongToken && strongToken.tokens && strongToken.tokens.length > 0) {
274
+ segments.push(...parseInlineTokens(strongToken.tokens, newAttrs));
275
+ }
276
+ else {
277
+ segments.push({ text: decodeHtmlEntities(strongToken.text), attributes: newAttrs });
278
+ }
279
+ break;
280
+ }
281
+ case 'em': {
282
+ const emToken = token;
283
+ const newAttrs = { ...inheritedAttributes, italic: true };
284
+ if ('tokens' in emToken && emToken.tokens && emToken.tokens.length > 0) {
285
+ segments.push(...parseInlineTokens(emToken.tokens, newAttrs));
286
+ }
287
+ else {
288
+ segments.push({ text: decodeHtmlEntities(emToken.text), attributes: newAttrs });
289
+ }
290
+ break;
291
+ }
292
+ case 'link': {
293
+ const linkToken = token;
294
+ // Sunsama expects link as a nested object with href property
295
+ const newAttrs = { ...inheritedAttributes, link: { href: linkToken.href } };
296
+ if ('tokens' in linkToken && linkToken.tokens && linkToken.tokens.length > 0) {
297
+ segments.push(...parseInlineTokens(linkToken.tokens, newAttrs));
298
+ }
299
+ else {
300
+ segments.push({ text: decodeHtmlEntities(linkToken.text), attributes: newAttrs });
301
+ }
302
+ break;
303
+ }
304
+ case 'codespan': {
305
+ const codeToken = token;
306
+ const newAttrs = { ...inheritedAttributes, code: true };
307
+ segments.push({ text: decodeHtmlEntities(codeToken.text), attributes: newAttrs });
308
+ break;
309
+ }
310
+ case 'del': {
311
+ // Note: Sunsama's editor doesn't support strikethrough marks
312
+ // So we render it as plain text with ~~ delimiters preserved
313
+ const delToken = token;
314
+ if ('tokens' in delToken && delToken.tokens && delToken.tokens.length > 0) {
315
+ segments.push({ text: '~~' });
316
+ segments.push(...parseInlineTokens(delToken.tokens, inheritedAttributes));
317
+ segments.push({ text: '~~' });
318
+ }
319
+ else {
320
+ segments.push({
321
+ text: `~~${decodeHtmlEntities(delToken.text)}~~`,
322
+ attributes: inheritedAttributes,
323
+ });
324
+ }
325
+ break;
326
+ }
327
+ case 'br': {
328
+ segments.push({ text: '\n' });
329
+ break;
330
+ }
331
+ case 'escape': {
332
+ const escapeToken = token;
333
+ const attrs = Object.keys(inheritedAttributes).length > 0 ? inheritedAttributes : undefined;
334
+ segments.push({ text: decodeHtmlEntities(escapeToken.text), attributes: attrs });
335
+ break;
336
+ }
337
+ default: {
338
+ // For any other token types, try to extract text
339
+ if ('text' in token && typeof token.text === 'string') {
340
+ const attrs = Object.keys(inheritedAttributes).length > 0 ? inheritedAttributes : undefined;
341
+ segments.push({
342
+ text: decodeHtmlEntities(token.text),
343
+ attributes: attrs,
344
+ });
345
+ }
346
+ else if ('raw' in token && typeof token.raw === 'string') {
347
+ const attrs = Object.keys(inheritedAttributes).length > 0 ? inheritedAttributes : undefined;
348
+ segments.push({ text: token.raw, attributes: attrs });
349
+ }
350
+ break;
351
+ }
352
+ }
353
+ }
354
+ return segments;
355
+ }
356
+ /**
357
+ * Parses markdown content into formatted segments suitable for Yjs XmlText insertion.
358
+ *
359
+ * This function converts markdown text into an array of segments, where each segment
360
+ * contains the text content and optional formatting attributes (bold, italic, link, etc.).
361
+ * The segments can be used to insert rich text into a Yjs document with proper formatting.
362
+ *
363
+ * @param markdown - The markdown content to parse
364
+ * @returns Array of formatted segments with text and Yjs-compatible attributes
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * const segments = parseMarkdownToSegments('This is **bold** and *italic* text');
369
+ * // Returns:
370
+ * // [
371
+ * // { text: 'This is ' },
372
+ * // { text: 'bold', attributes: { bold: true } },
373
+ * // { text: ' and ' },
374
+ * // { text: 'italic', attributes: { italic: true } },
375
+ * // { text: ' text' }
376
+ * // ]
377
+ *
378
+ * const linkSegments = parseMarkdownToSegments('Visit [Google](https://google.com)');
379
+ * // Returns:
380
+ * // [
381
+ * // { text: 'Visit ' },
382
+ * // { text: 'Google', attributes: { link: 'https://google.com' } }
383
+ * // ]
384
+ * ```
385
+ */
386
+ export function parseMarkdownToSegments(markdown) {
387
+ if (!markdown || markdown.trim() === '') {
388
+ return [];
389
+ }
390
+ const segments = [];
391
+ // Configure marked for GFM (GitHub Flavored Markdown) support
392
+ marked.setOptions({
393
+ gfm: true,
394
+ breaks: true,
395
+ });
396
+ // Tokenize the markdown
397
+ const tokens = marked.lexer(markdown);
398
+ for (let i = 0; i < tokens.length; i++) {
399
+ const token = tokens[i];
400
+ switch (token.type) {
401
+ case 'paragraph': {
402
+ const paragraphToken = token;
403
+ if (paragraphToken.tokens) {
404
+ segments.push(...parseInlineTokens(paragraphToken.tokens));
405
+ }
406
+ // Add newline after paragraph if not the last token
407
+ if (i < tokens.length - 1) {
408
+ segments.push({ text: '\n' });
409
+ }
410
+ break;
411
+ }
412
+ case 'heading': {
413
+ const headingToken = token;
414
+ if (headingToken.tokens) {
415
+ // Apply bold formatting to headings
416
+ segments.push(...parseInlineTokens(headingToken.tokens, { bold: true }));
417
+ }
418
+ segments.push({ text: '\n' });
419
+ break;
420
+ }
421
+ case 'list': {
422
+ const listToken = token;
423
+ for (let j = 0; j < listToken.items.length; j++) {
424
+ const item = listToken.items[j];
425
+ // Add list marker
426
+ const marker = listToken.ordered ? `${j + 1}. ` : '• ';
427
+ segments.push({ text: marker });
428
+ if (item.tokens) {
429
+ // Process list item content
430
+ for (const itemToken of item.tokens) {
431
+ if (itemToken.type === 'text' &&
432
+ 'tokens' in itemToken &&
433
+ itemToken.tokens) {
434
+ segments.push(...parseInlineTokens(itemToken.tokens));
435
+ }
436
+ else if ('tokens' in itemToken && itemToken.tokens) {
437
+ segments.push(...parseInlineTokens(itemToken.tokens));
438
+ }
439
+ else if ('text' in itemToken) {
440
+ segments.push({ text: decodeHtmlEntities(itemToken.text) });
441
+ }
442
+ }
443
+ }
444
+ segments.push({ text: '\n' });
445
+ }
446
+ break;
447
+ }
448
+ case 'code': {
449
+ const codeToken = token;
450
+ segments.push({ text: decodeHtmlEntities(codeToken.text), attributes: { code: true } });
451
+ segments.push({ text: '\n' });
452
+ break;
453
+ }
454
+ case 'blockquote': {
455
+ const blockquoteToken = token;
456
+ if (blockquoteToken.tokens) {
457
+ for (const innerToken of blockquoteToken.tokens) {
458
+ if (innerToken.type === 'paragraph' && 'tokens' in innerToken) {
459
+ segments.push({ text: '> ' });
460
+ segments.push(...parseInlineTokens(innerToken.tokens));
461
+ segments.push({ text: '\n' });
462
+ }
463
+ }
464
+ }
465
+ break;
466
+ }
467
+ case 'hr': {
468
+ segments.push({ text: '---\n' });
469
+ break;
470
+ }
471
+ case 'space': {
472
+ segments.push({ text: '\n' });
473
+ break;
474
+ }
475
+ case 'text': {
476
+ const textToken = token;
477
+ if ('tokens' in textToken && textToken.tokens) {
478
+ segments.push(...parseInlineTokens(textToken.tokens));
479
+ }
480
+ else {
481
+ segments.push({ text: decodeHtmlEntities(textToken.text) });
482
+ }
483
+ break;
484
+ }
485
+ default: {
486
+ // Handle any other token types by extracting raw text
487
+ if ('raw' in token && typeof token.raw === 'string') {
488
+ segments.push({ text: decodeHtmlEntities(token.raw) });
489
+ }
490
+ break;
491
+ }
492
+ }
493
+ }
494
+ // Merge adjacent segments with the same attributes for efficiency
495
+ return mergeAdjacentSegments(segments);
496
+ }
497
+ /**
498
+ * Merges adjacent segments that have the same attributes
499
+ * @param segments - Array of formatted segments
500
+ * @returns Merged array of segments
501
+ * @internal
502
+ */
503
+ function mergeAdjacentSegments(segments) {
504
+ if (segments.length === 0)
505
+ return [];
506
+ const merged = [];
507
+ let current = segments[0];
508
+ for (let i = 1; i < segments.length; i++) {
509
+ const next = segments[i];
510
+ // Check if attributes are the same (or both undefined/empty)
511
+ const currentAttrs = current.attributes || {};
512
+ const nextAttrs = next.attributes || {};
513
+ const attrsEqual = JSON.stringify(currentAttrs) === JSON.stringify(nextAttrs);
514
+ if (attrsEqual) {
515
+ // Merge the text
516
+ current = {
517
+ text: current.text + next.text,
518
+ attributes: Object.keys(currentAttrs).length > 0 ? currentAttrs : undefined,
519
+ };
520
+ }
521
+ else {
522
+ merged.push(current);
523
+ current = next;
524
+ }
525
+ }
526
+ merged.push(current);
527
+ return merged;
528
+ }
529
+ /**
530
+ * Parses markdown content into a document structure with block-level elements.
531
+ *
532
+ * This function converts markdown text into an array of document blocks, where each block
533
+ * represents a block-level element (paragraph, blockquote, horizontal rule, etc.).
534
+ * This structure is suitable for creating proper Yjs XmlElement hierarchies that match
535
+ * Sunsama's rich text editor format.
536
+ *
537
+ * @param markdown - The markdown content to parse
538
+ * @returns Array of document blocks representing the document structure
539
+ *
540
+ * @example
541
+ * ```typescript
542
+ * const blocks = parseMarkdownToBlocks('Hello **world**\n\n> A quote\n\n---');
543
+ * // Returns:
544
+ * // [
545
+ * // { type: 'paragraph', segments: [{ text: 'Hello ' }, { text: 'world', attributes: { bold: true } }] },
546
+ * // { type: 'blockquote', children: [{ type: 'paragraph', segments: [{ text: 'A quote' }] }] },
547
+ * // { type: 'horizontalRule' }
548
+ * // ]
549
+ * ```
550
+ */
551
+ export function parseMarkdownToBlocks(markdown) {
552
+ if (!markdown || markdown.trim() === '') {
553
+ return [];
554
+ }
555
+ const blocks = [];
556
+ // Configure marked for GFM support
557
+ marked.setOptions({
558
+ gfm: true,
559
+ breaks: true,
560
+ });
561
+ // Tokenize the markdown
562
+ const tokens = marked.lexer(markdown);
563
+ for (const token of tokens) {
564
+ switch (token.type) {
565
+ case 'paragraph': {
566
+ const paragraphToken = token;
567
+ if (paragraphToken.tokens) {
568
+ const segments = parseInlineTokens(paragraphToken.tokens);
569
+ if (segments.length > 0) {
570
+ blocks.push({ type: 'paragraph', segments: mergeAdjacentSegments(segments) });
571
+ }
572
+ }
573
+ break;
574
+ }
575
+ case 'heading': {
576
+ // Convert headings to bold paragraphs (Sunsama may not support native headings)
577
+ const headingToken = token;
578
+ if (headingToken.tokens) {
579
+ const segments = parseInlineTokens(headingToken.tokens, { bold: true });
580
+ if (segments.length > 0) {
581
+ blocks.push({ type: 'paragraph', segments: mergeAdjacentSegments(segments) });
582
+ }
583
+ }
584
+ break;
585
+ }
586
+ case 'blockquote': {
587
+ const blockquoteToken = token;
588
+ const children = [];
589
+ if (blockquoteToken.tokens) {
590
+ for (const innerToken of blockquoteToken.tokens) {
591
+ if (innerToken.type === 'paragraph' && 'tokens' in innerToken) {
592
+ const segments = parseInlineTokens(innerToken.tokens);
593
+ if (segments.length > 0) {
594
+ children.push({ type: 'paragraph', segments: mergeAdjacentSegments(segments) });
595
+ }
596
+ }
597
+ }
598
+ }
599
+ if (children.length > 0) {
600
+ blocks.push({ type: 'blockquote', children });
601
+ }
602
+ break;
603
+ }
604
+ case 'hr': {
605
+ blocks.push({ type: 'horizontalRule' });
606
+ break;
607
+ }
608
+ case 'code': {
609
+ const codeToken = token;
610
+ blocks.push({
611
+ type: 'codeBlock',
612
+ segments: [{ text: decodeHtmlEntities(codeToken.text) }],
613
+ });
614
+ break;
615
+ }
616
+ case 'list': {
617
+ // Create proper list structure with listItem elements
618
+ const listToken = token;
619
+ const items = [];
620
+ for (const item of listToken.items) {
621
+ const segments = [];
622
+ if (item.tokens) {
623
+ for (const itemToken of item.tokens) {
624
+ if (itemToken.type === 'text' &&
625
+ 'tokens' in itemToken &&
626
+ itemToken.tokens) {
627
+ segments.push(...parseInlineTokens(itemToken.tokens));
628
+ }
629
+ else if ('tokens' in itemToken && itemToken.tokens) {
630
+ segments.push(...parseInlineTokens(itemToken.tokens));
631
+ }
632
+ else if ('text' in itemToken) {
633
+ segments.push({ text: decodeHtmlEntities(itemToken.text) });
634
+ }
635
+ }
636
+ }
637
+ if (segments.length > 0) {
638
+ items.push({ segments: mergeAdjacentSegments(segments) });
639
+ }
640
+ }
641
+ if (items.length > 0) {
642
+ blocks.push({
643
+ type: listToken.ordered ? 'orderedList' : 'bulletList',
644
+ items,
645
+ start: listToken.ordered && typeof listToken.start === 'number'
646
+ ? listToken.start
647
+ : undefined,
648
+ });
649
+ }
650
+ break;
651
+ }
652
+ case 'space': {
653
+ // Skip pure space tokens - they're handled by paragraph breaks
654
+ break;
655
+ }
656
+ case 'text': {
657
+ const textToken = token;
658
+ const segments = [];
659
+ if ('tokens' in textToken && textToken.tokens) {
660
+ segments.push(...parseInlineTokens(textToken.tokens));
661
+ }
662
+ else {
663
+ segments.push({ text: decodeHtmlEntities(textToken.text) });
664
+ }
665
+ if (segments.length > 0) {
666
+ blocks.push({ type: 'paragraph', segments: mergeAdjacentSegments(segments) });
667
+ }
668
+ break;
669
+ }
670
+ default: {
671
+ // Handle any other token types by extracting raw text
672
+ if ('raw' in token && typeof token.raw === 'string') {
673
+ const raw = token.raw.trim();
674
+ if (raw) {
675
+ blocks.push({ type: 'paragraph', segments: [{ text: raw }] });
676
+ }
677
+ }
678
+ break;
679
+ }
680
+ }
681
+ }
682
+ return blocks;
683
+ }
684
+ //# sourceMappingURL=conversion.js.map