@oneuptime/common 12.0.14 → 12.0.15

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 (195) hide show
  1. package/Models/DatabaseModels/GlobalOidc.ts +20 -0
  2. package/Models/DatabaseModels/GlobalSso.ts +20 -0
  3. package/Models/DatabaseModels/MarketingConversion.ts +19 -9
  4. package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
  5. package/Models/DatabaseModels/StatusPage.ts +7 -4
  6. package/Server/API/StatusPageAPI.ts +29 -4
  7. package/Server/EnvironmentConfig.ts +7 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  11. package/Server/Middleware/SlackAuthorization.ts +43 -6
  12. package/Server/Middleware/UserAuthorization.ts +236 -26
  13. package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
  14. package/Server/Services/GlobalOidcProjectService.ts +126 -1
  15. package/Server/Services/GlobalOidcService.ts +132 -0
  16. package/Server/Services/GlobalSsoProjectService.ts +126 -1
  17. package/Server/Services/GlobalSsoService.ts +132 -0
  18. package/Server/Services/UserEmailService.ts +4 -4
  19. package/Server/Utils/CronTab.ts +73 -0
  20. package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
  21. package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
  22. package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
  23. package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
  24. package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
  25. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
  26. package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
  27. package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
  28. package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
  29. package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
  30. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
  31. package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
  32. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
  33. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
  34. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
  35. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
  36. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
  37. package/Server/Utils/Monitor/MonitorResource.ts +161 -21
  38. package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
  39. package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
  40. package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
  41. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
  42. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
  43. package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
  44. package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
  45. package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
  46. package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
  47. package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
  48. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
  49. package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
  50. package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
  51. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
  52. package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
  53. package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
  54. package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
  55. package/Tests/Server/Utils/CronTab.test.ts +116 -0
  56. package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
  57. package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
  58. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
  59. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
  60. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
  61. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
  62. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
  63. package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
  64. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
  65. package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
  66. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
  67. package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
  68. package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
  69. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
  70. package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
  71. package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
  72. package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
  73. package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
  74. package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
  75. package/Tests/Utils/Analytics.test.ts +187 -0
  76. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
  77. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
  78. package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
  79. package/Types/Marketing/MarketingConversion.ts +12 -0
  80. package/Types/Monitor/CriteriaFilter.ts +9 -0
  81. package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
  82. package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
  83. package/UI/Components/Forms/BasicForm.tsx +13 -1
  84. package/UI/Components/Forms/BasicModelForm.tsx +2 -1
  85. package/UI/Components/Forms/ModelForm.tsx +10 -1
  86. package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
  87. package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
  88. package/Utils/Analytics.ts +55 -2
  89. package/Utils/Monitor/MonitorMetricType.ts +40 -0
  90. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
  91. package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
  92. package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
  93. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
  95. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
  97. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
  99. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
  101. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  102. package/build/dist/Server/API/StatusPageAPI.js +25 -4
  103. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +6 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
  113. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  114. package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
  115. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  116. package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
  117. package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
  119. package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
  120. package/build/dist/Server/Services/GlobalOidcService.js +127 -0
  121. package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
  122. package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
  123. package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
  124. package/build/dist/Server/Services/GlobalSsoService.js +127 -0
  125. package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
  126. package/build/dist/Server/Services/UserEmailService.js +4 -4
  127. package/build/dist/Server/Utils/CronTab.js +66 -0
  128. package/build/dist/Server/Utils/CronTab.js.map +1 -1
  129. package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
  130. package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
  131. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
  132. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
  133. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
  134. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
  135. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
  136. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
  137. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
  138. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
  139. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
  140. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
  141. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
  142. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
  143. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
  144. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
  145. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
  146. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
  147. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
  148. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
  149. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
  150. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  151. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
  152. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
  154. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  155. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
  156. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  157. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
  158. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  159. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
  160. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  161. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
  162. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  163. package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
  164. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  165. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
  166. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
  167. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
  168. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
  169. package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
  170. package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
  171. package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
  172. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  173. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
  174. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
  175. package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
  176. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  177. package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
  178. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  179. package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
  180. package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
  181. package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
  182. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  183. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
  184. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
  185. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
  186. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  187. package/build/dist/Utils/Analytics.js +50 -2
  188. package/build/dist/Utils/Analytics.js.map +1 -1
  189. package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
  190. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  191. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
  192. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
  193. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
  194. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
  195. package/package.json +1 -1
@@ -0,0 +1,791 @@
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+ import parseMarkdownBlocks, {
3
+ MarkdownBlock,
4
+ MarkdownBlockType,
5
+ MarkdownInline,
6
+ } from "../../../../UI/Utils/AIChatExport/MarkdownBlocks";
7
+
8
+ /*
9
+ * parseMarkdownBlocks lowers a remark/mdast tree into flat, drawable blocks for
10
+ * the PDF exporter. The real parser (remark-parse) is native ESM that this
11
+ * repo's jest transform cannot load, and remark-gfm is already stubbed to a
12
+ * no-op by the shared jest config. So the tree-building step is not what these
13
+ * tests are about — the lowering is. The parser is stubbed here so that
14
+ * .parse(input) simply deserializes a hand-built mdast tree from the input
15
+ * string. Each test therefore feeds an exact tree that matches the MarkdownNode
16
+ * contract the source consumes (type/value/lang/depth/ordered/start/align/
17
+ * children) and asserts the precise blocks that come back, branch by branch.
18
+ *
19
+ * Passing a string that is not valid JSON makes the stubbed .parse throw, which
20
+ * is how the module's parse-failure fallback is exercised.
21
+ */
22
+ jest.mock("remark-parse", () => {
23
+ return { __esModule: true, default: (): void => {} };
24
+ });
25
+
26
+ jest.mock("unified", () => {
27
+ interface Chain {
28
+ use: () => Chain;
29
+ parse: (input: string) => unknown;
30
+ }
31
+ const makeChain: () => Chain = (): Chain => {
32
+ const chain: Chain = {
33
+ use: (): Chain => {
34
+ return chain;
35
+ },
36
+ parse: (input: string): unknown => {
37
+ return JSON.parse(input);
38
+ },
39
+ };
40
+ return chain;
41
+ };
42
+ return { __esModule: true, unified: makeChain };
43
+ });
44
+
45
+ /*
46
+ * Minimal mirror of the private MarkdownNode contract the source reads. Only
47
+ * the fields the lowering touches are modelled.
48
+ */
49
+ interface MdNode {
50
+ type: string;
51
+ value?: string | undefined;
52
+ lang?: string | undefined;
53
+ depth?: number | undefined;
54
+ ordered?: boolean | undefined;
55
+ start?: number | null | undefined;
56
+ align?: Array<string | null> | undefined;
57
+ children?: Array<MdNode> | undefined;
58
+ }
59
+
60
+ function root(children: Array<MdNode>): MdNode {
61
+ return { type: "root", children: children };
62
+ }
63
+
64
+ function blocksOf(tree: MdNode): Array<MarkdownBlock> {
65
+ return parseMarkdownBlocks(JSON.stringify(tree));
66
+ }
67
+
68
+ function text(value: string): MdNode {
69
+ return { type: "text", value: value };
70
+ }
71
+
72
+ function plain(value: string): MarkdownInline {
73
+ return {
74
+ text: value,
75
+ isBold: false,
76
+ isItalic: false,
77
+ isStrikethrough: false,
78
+ };
79
+ }
80
+
81
+ describe("parseMarkdownBlocks", () => {
82
+ describe("input guards and error handling", () => {
83
+ test("returns an empty list for empty string without invoking the parser", () => {
84
+ expect(parseMarkdownBlocks("")).toEqual([]);
85
+ });
86
+
87
+ test("returns an empty list when the tree has no children", () => {
88
+ expect(blocksOf({ type: "root" })).toEqual([]);
89
+ });
90
+
91
+ test("falls back to a single raw paragraph when parsing throws", () => {
92
+ const raw: string = "# not json {";
93
+
94
+ expect(parseMarkdownBlocks(raw)).toEqual([
95
+ {
96
+ type: MarkdownBlockType.Paragraph,
97
+ inlines: [{ text: raw }],
98
+ },
99
+ ]);
100
+ });
101
+
102
+ test("keeps the original text verbatim in the parse-failure fallback", () => {
103
+ const raw: string = "just some **unparseable** text";
104
+ const blocks: Array<MarkdownBlock> = parseMarkdownBlocks(raw);
105
+
106
+ expect(blocks).toHaveLength(1);
107
+ expect(blocks[0]?.type).toBe(MarkdownBlockType.Paragraph);
108
+ expect(blocks[0]?.inlines).toEqual([{ text: raw }]);
109
+ });
110
+ });
111
+
112
+ describe("headings", () => {
113
+ test("maps each heading depth 1-6 onto its level", () => {
114
+ for (let depth: number = 1; depth <= 6; depth++) {
115
+ const blocks: Array<MarkdownBlock> = blocksOf(
116
+ root([{ type: "heading", depth: depth, children: [text("H")] }]),
117
+ );
118
+
119
+ expect(blocks).toEqual([
120
+ {
121
+ type: MarkdownBlockType.Heading,
122
+ level: depth,
123
+ inlines: [plain("H")],
124
+ },
125
+ ]);
126
+ }
127
+ });
128
+
129
+ test("defaults a heading with no depth to level 1", () => {
130
+ expect(
131
+ blocksOf(root([{ type: "heading", children: [text("H")] }])),
132
+ ).toEqual([
133
+ {
134
+ type: MarkdownBlockType.Heading,
135
+ level: 1,
136
+ inlines: [plain("H")],
137
+ },
138
+ ]);
139
+ });
140
+
141
+ test("treats a zero depth as level 1", () => {
142
+ const blocks: Array<MarkdownBlock> = blocksOf(
143
+ root([{ type: "heading", depth: 0, children: [text("H")] }]),
144
+ );
145
+
146
+ expect(blocks[0]?.level).toBe(1);
147
+ });
148
+
149
+ test("carries inline styling inside a heading", () => {
150
+ const blocks: Array<MarkdownBlock> = blocksOf(
151
+ root([
152
+ {
153
+ type: "heading",
154
+ depth: 2,
155
+ children: [{ type: "strong", children: [text("bold")] }],
156
+ },
157
+ ]),
158
+ );
159
+
160
+ expect(blocks[0]?.inlines).toEqual([
161
+ { text: "bold", isBold: true, isItalic: false, isStrikethrough: false },
162
+ ]);
163
+ });
164
+ });
165
+
166
+ describe("paragraphs", () => {
167
+ test("renders a paragraph with plain inline text at indent 0", () => {
168
+ expect(
169
+ blocksOf(root([{ type: "paragraph", children: [text("hi")] }])),
170
+ ).toEqual([
171
+ {
172
+ type: MarkdownBlockType.Paragraph,
173
+ inlines: [plain("hi")],
174
+ indent: 0,
175
+ },
176
+ ]);
177
+ });
178
+ });
179
+
180
+ describe("inline styling (collectInlines)", () => {
181
+ test("captures bold, italic, code, strikethrough and their nesting", () => {
182
+ const blocks: Array<MarkdownBlock> = blocksOf(
183
+ root([
184
+ {
185
+ type: "paragraph",
186
+ children: [
187
+ text("plain "),
188
+ {
189
+ type: "strong",
190
+ children: [text("b"), { type: "inlineCode", value: "c" }],
191
+ },
192
+ { type: "emphasis", children: [text("i")] },
193
+ { type: "delete", children: [text("d")] },
194
+ ],
195
+ },
196
+ ]),
197
+ );
198
+
199
+ expect(blocks[0]?.inlines).toEqual([
200
+ plain("plain "),
201
+ { text: "b", isBold: true, isItalic: false, isStrikethrough: false },
202
+ { text: "c", isCode: true, isBold: true, isItalic: false },
203
+ { text: "i", isBold: false, isItalic: true, isStrikethrough: false },
204
+ { text: "d", isBold: false, isItalic: false, isStrikethrough: true },
205
+ ]);
206
+ });
207
+
208
+ test("emits an empty-string inline for empty inline code", () => {
209
+ const blocks: Array<MarkdownBlock> = blocksOf(
210
+ root([{ type: "paragraph", children: [{ type: "inlineCode" }] }]),
211
+ );
212
+
213
+ expect(blocks[0]?.inlines).toEqual([
214
+ { text: "", isCode: true, isBold: false, isItalic: false },
215
+ ]);
216
+ });
217
+
218
+ test("drops a text node with no value rather than emitting an empty inline", () => {
219
+ const blocks: Array<MarkdownBlock> = blocksOf(
220
+ root([{ type: "paragraph", children: [text(""), text("kept")] }]),
221
+ );
222
+
223
+ expect(blocks[0]?.inlines).toEqual([plain("kept")]);
224
+ });
225
+
226
+ test("turns a hard break into a newline inline with no styling", () => {
227
+ const blocks: Array<MarkdownBlock> = blocksOf(
228
+ root([
229
+ {
230
+ type: "paragraph",
231
+ children: [text("a"), { type: "break" }, text("b")],
232
+ },
233
+ ]),
234
+ );
235
+
236
+ expect(blocks[0]?.inlines).toEqual([
237
+ plain("a"),
238
+ { text: "\n" },
239
+ plain("b"),
240
+ ]);
241
+ });
242
+
243
+ test("keeps link and image text but discards their destinations", () => {
244
+ const blocks: Array<MarkdownBlock> = blocksOf(
245
+ root([
246
+ {
247
+ type: "paragraph",
248
+ children: [
249
+ { type: "link", value: "ignored", children: [text("L")] },
250
+ { type: "image", value: "ignored", children: [text("I")] },
251
+ { type: "linkReference", children: [text("R")] },
252
+ { type: "imageReference", children: [text("IR")] },
253
+ ],
254
+ },
255
+ ]),
256
+ );
257
+
258
+ const serialized: string = JSON.stringify(blocks);
259
+
260
+ expect(blocks[0]?.inlines).toEqual([
261
+ plain("L"),
262
+ plain("I"),
263
+ plain("R"),
264
+ plain("IR"),
265
+ ]);
266
+ expect(serialized).not.toContain("ignored");
267
+ });
268
+
269
+ test("recurses through an unknown inline container", () => {
270
+ const blocks: Array<MarkdownBlock> = blocksOf(
271
+ root([
272
+ {
273
+ type: "paragraph",
274
+ children: [{ type: "superscript", children: [text("nested")] }],
275
+ },
276
+ ]),
277
+ );
278
+
279
+ expect(blocks[0]?.inlines).toEqual([plain("nested")]);
280
+ });
281
+
282
+ test("falls back to text plus bold flag for an unknown valued inline", () => {
283
+ const blocks: Array<MarkdownBlock> = blocksOf(
284
+ root([
285
+ {
286
+ type: "paragraph",
287
+ children: [
288
+ {
289
+ type: "strong",
290
+ children: [{ type: "footnoteReference", value: "fx" }],
291
+ },
292
+ ],
293
+ },
294
+ ]),
295
+ );
296
+
297
+ expect(blocks[0]?.inlines).toEqual([{ text: "fx", isBold: true }]);
298
+ });
299
+ });
300
+
301
+ describe("code blocks", () => {
302
+ test("keeps the code body and language", () => {
303
+ expect(
304
+ blocksOf(root([{ type: "code", lang: "ts", value: "const a = 1;" }])),
305
+ ).toEqual([
306
+ {
307
+ type: MarkdownBlockType.Code,
308
+ code: "const a = 1;",
309
+ language: "ts",
310
+ },
311
+ ]);
312
+ });
313
+
314
+ test("defaults missing language and body to empty strings", () => {
315
+ expect(blocksOf(root([{ type: "code" }]))).toEqual([
316
+ {
317
+ type: MarkdownBlockType.Code,
318
+ code: "",
319
+ language: "",
320
+ },
321
+ ]);
322
+ });
323
+ });
324
+
325
+ describe("blockquotes", () => {
326
+ test("re-labels a quoted paragraph as a Quote while keeping its inlines", () => {
327
+ expect(
328
+ blocksOf(
329
+ root([
330
+ {
331
+ type: "blockquote",
332
+ children: [{ type: "paragraph", children: [text("quoted")] }],
333
+ },
334
+ ]),
335
+ ),
336
+ ).toEqual([
337
+ {
338
+ type: MarkdownBlockType.Quote,
339
+ inlines: [plain("quoted")],
340
+ indent: 0,
341
+ },
342
+ ]);
343
+ });
344
+
345
+ test("re-labels a quoted heading as a Quote but keeps its level", () => {
346
+ expect(
347
+ blocksOf(
348
+ root([
349
+ {
350
+ type: "blockquote",
351
+ children: [{ type: "heading", depth: 3, children: [text("QH")] }],
352
+ },
353
+ ]),
354
+ ),
355
+ ).toEqual([
356
+ {
357
+ type: MarkdownBlockType.Quote,
358
+ level: 3,
359
+ inlines: [plain("QH")],
360
+ },
361
+ ]);
362
+ });
363
+
364
+ test("emits one Quote block per child of the blockquote", () => {
365
+ const blocks: Array<MarkdownBlock> = blocksOf(
366
+ root([
367
+ {
368
+ type: "blockquote",
369
+ children: [
370
+ { type: "paragraph", children: [text("one")] },
371
+ { type: "paragraph", children: [text("two")] },
372
+ ],
373
+ },
374
+ ]),
375
+ );
376
+
377
+ expect(blocks).toHaveLength(2);
378
+ expect(
379
+ blocks.every((block: MarkdownBlock) => {
380
+ return block.type === MarkdownBlockType.Quote;
381
+ }),
382
+ ).toBe(true);
383
+ });
384
+ });
385
+
386
+ describe("thematic break", () => {
387
+ test("lowers a thematic break to a bare Rule block", () => {
388
+ expect(blocksOf(root([{ type: "thematicBreak" }]))).toEqual([
389
+ { type: MarkdownBlockType.Rule },
390
+ ]);
391
+ });
392
+ });
393
+
394
+ describe("raw html", () => {
395
+ test("shows raw html as an unstyled paragraph", () => {
396
+ expect(blocksOf(root([{ type: "html", value: "<b>x</b>" }]))).toEqual([
397
+ {
398
+ type: MarkdownBlockType.Paragraph,
399
+ inlines: [{ text: "<b>x</b>" }],
400
+ indent: 0,
401
+ },
402
+ ]);
403
+ });
404
+
405
+ test("drops an html node that has no value", () => {
406
+ expect(blocksOf(root([{ type: "html" }]))).toEqual([]);
407
+ });
408
+ });
409
+
410
+ describe("unordered lists", () => {
411
+ test("renders each item with a dash marker at the given indent", () => {
412
+ expect(
413
+ blocksOf(
414
+ root([
415
+ {
416
+ type: "list",
417
+ children: [
418
+ {
419
+ type: "listItem",
420
+ children: [{ type: "paragraph", children: [text("one")] }],
421
+ },
422
+ {
423
+ type: "listItem",
424
+ children: [{ type: "paragraph", children: [text("two")] }],
425
+ },
426
+ ],
427
+ },
428
+ ]),
429
+ ),
430
+ ).toEqual([
431
+ {
432
+ type: MarkdownBlockType.ListItem,
433
+ inlines: [plain("one")],
434
+ indent: 0,
435
+ marker: "-",
436
+ },
437
+ {
438
+ type: MarkdownBlockType.ListItem,
439
+ inlines: [plain("two")],
440
+ indent: 0,
441
+ marker: "-",
442
+ },
443
+ ]);
444
+ });
445
+
446
+ test("joins multiple paragraphs in one item with a soft break", () => {
447
+ const blocks: Array<MarkdownBlock> = blocksOf(
448
+ root([
449
+ {
450
+ type: "list",
451
+ children: [
452
+ {
453
+ type: "listItem",
454
+ children: [
455
+ { type: "paragraph", children: [text("p1")] },
456
+ { type: "paragraph", children: [text("p2")] },
457
+ ],
458
+ },
459
+ ],
460
+ },
461
+ ]),
462
+ );
463
+
464
+ expect(blocks).toEqual([
465
+ {
466
+ type: MarkdownBlockType.ListItem,
467
+ inlines: [plain("p1"), { text: "\n" }, plain("p2")],
468
+ indent: 0,
469
+ marker: "-",
470
+ },
471
+ ]);
472
+ });
473
+
474
+ test("renders an empty item as an empty inline list", () => {
475
+ expect(
476
+ blocksOf(root([{ type: "list", children: [{ type: "listItem" }] }])),
477
+ ).toEqual([
478
+ {
479
+ type: MarkdownBlockType.ListItem,
480
+ inlines: [],
481
+ indent: 0,
482
+ marker: "-",
483
+ },
484
+ ]);
485
+ });
486
+ });
487
+
488
+ describe("ordered lists", () => {
489
+ test("numbers items sequentially starting at 1 by default", () => {
490
+ const blocks: Array<MarkdownBlock> = blocksOf(
491
+ root([
492
+ {
493
+ type: "list",
494
+ ordered: true,
495
+ children: [
496
+ {
497
+ type: "listItem",
498
+ children: [{ type: "paragraph", children: [text("a")] }],
499
+ },
500
+ {
501
+ type: "listItem",
502
+ children: [{ type: "paragraph", children: [text("b")] }],
503
+ },
504
+ ],
505
+ },
506
+ ]),
507
+ );
508
+
509
+ expect(
510
+ blocks.map((block: MarkdownBlock) => {
511
+ return block.marker;
512
+ }),
513
+ ).toEqual(["1.", "2."]);
514
+ });
515
+
516
+ test("honours a numeric start value and increments from it", () => {
517
+ const blocks: Array<MarkdownBlock> = blocksOf(
518
+ root([
519
+ {
520
+ type: "list",
521
+ ordered: true,
522
+ start: 5,
523
+ children: [
524
+ {
525
+ type: "listItem",
526
+ children: [{ type: "paragraph", children: [text("five")] }],
527
+ },
528
+ {
529
+ type: "listItem",
530
+ children: [{ type: "paragraph", children: [text("six")] }],
531
+ },
532
+ ],
533
+ },
534
+ ]),
535
+ );
536
+
537
+ expect(
538
+ blocks.map((block: MarkdownBlock) => {
539
+ return block.marker;
540
+ }),
541
+ ).toEqual(["5.", "6."]);
542
+ });
543
+
544
+ test("treats a null start as starting at 1", () => {
545
+ const blocks: Array<MarkdownBlock> = blocksOf(
546
+ root([
547
+ {
548
+ type: "list",
549
+ ordered: true,
550
+ start: null,
551
+ children: [
552
+ {
553
+ type: "listItem",
554
+ children: [{ type: "paragraph", children: [text("only")] }],
555
+ },
556
+ ],
557
+ },
558
+ ]),
559
+ );
560
+
561
+ expect(blocks[0]?.marker).toBe("1.");
562
+ });
563
+ });
564
+
565
+ describe("nested lists and non-paragraph list children", () => {
566
+ test("increments indent for a nested list and appends it after the item", () => {
567
+ const blocks: Array<MarkdownBlock> = blocksOf(
568
+ root([
569
+ {
570
+ type: "list",
571
+ children: [
572
+ {
573
+ type: "listItem",
574
+ children: [
575
+ { type: "paragraph", children: [text("a")] },
576
+ {
577
+ type: "list",
578
+ children: [
579
+ {
580
+ type: "listItem",
581
+ children: [
582
+ { type: "paragraph", children: [text("a1")] },
583
+ ],
584
+ },
585
+ ],
586
+ },
587
+ ],
588
+ },
589
+ ],
590
+ },
591
+ ]),
592
+ );
593
+
594
+ expect(blocks).toEqual([
595
+ {
596
+ type: MarkdownBlockType.ListItem,
597
+ inlines: [plain("a")],
598
+ indent: 0,
599
+ marker: "-",
600
+ },
601
+ {
602
+ type: MarkdownBlockType.ListItem,
603
+ inlines: [plain("a1")],
604
+ indent: 1,
605
+ marker: "-",
606
+ },
607
+ ]);
608
+ });
609
+
610
+ test("lowers a non-paragraph, non-list child through toBlocks after the item", () => {
611
+ const blocks: Array<MarkdownBlock> = blocksOf(
612
+ root([
613
+ {
614
+ type: "list",
615
+ children: [
616
+ {
617
+ type: "listItem",
618
+ children: [
619
+ { type: "paragraph", children: [text("intro")] },
620
+ { type: "code", lang: "sh", value: "ls" },
621
+ ],
622
+ },
623
+ ],
624
+ },
625
+ ]),
626
+ );
627
+
628
+ expect(blocks).toEqual([
629
+ {
630
+ type: MarkdownBlockType.ListItem,
631
+ inlines: [plain("intro")],
632
+ indent: 0,
633
+ marker: "-",
634
+ },
635
+ {
636
+ type: MarkdownBlockType.Code,
637
+ code: "ls",
638
+ language: "sh",
639
+ },
640
+ ]);
641
+ });
642
+ });
643
+
644
+ describe("tables (tableToBlock)", () => {
645
+ test("splits the header row from data rows and maps alignments", () => {
646
+ const blocks: Array<MarkdownBlock> = blocksOf(
647
+ root([
648
+ {
649
+ type: "table",
650
+ align: ["left", "center", "right", null],
651
+ children: [
652
+ {
653
+ type: "tableRow",
654
+ children: [
655
+ { type: "tableCell", children: [text("H1")] },
656
+ {
657
+ type: "tableCell",
658
+ children: [{ type: "strong", children: [text("H2")] }],
659
+ },
660
+ ],
661
+ },
662
+ {
663
+ type: "tableRow",
664
+ children: [
665
+ { type: "tableCell", children: [text("r1c1")] },
666
+ { type: "tableCell", children: [text("r1c2")] },
667
+ ],
668
+ },
669
+ {
670
+ type: "tableRow",
671
+ children: [
672
+ { type: "tableCell", children: [text("r2c1")] },
673
+ { type: "tableCell", children: [text("r2c2")] },
674
+ ],
675
+ },
676
+ ],
677
+ },
678
+ ]),
679
+ );
680
+
681
+ expect(blocks).toEqual([
682
+ {
683
+ type: MarkdownBlockType.Table,
684
+ headers: ["H1", "H2"],
685
+ rows: [
686
+ ["r1c1", "r1c2"],
687
+ ["r2c1", "r2c2"],
688
+ ],
689
+ alignments: ["left", "center", "right", "left"],
690
+ },
691
+ ]);
692
+ });
693
+
694
+ test("flattens styled cell content down to plain text", () => {
695
+ const blocks: Array<MarkdownBlock> = blocksOf(
696
+ root([
697
+ {
698
+ type: "table",
699
+ children: [
700
+ {
701
+ type: "tableRow",
702
+ children: [
703
+ {
704
+ type: "tableCell",
705
+ children: [
706
+ { type: "emphasis", children: [text("em")] },
707
+ { type: "inlineCode", value: "code" },
708
+ ],
709
+ },
710
+ ],
711
+ },
712
+ ],
713
+ },
714
+ ]),
715
+ );
716
+
717
+ expect(blocks[0]?.headers).toEqual(["emcode"]);
718
+ expect(blocks[0]?.rows).toEqual([]);
719
+ });
720
+
721
+ test("produces empty header, row and alignment arrays for an empty table", () => {
722
+ expect(blocksOf(root([{ type: "table" }]))).toEqual([
723
+ {
724
+ type: MarkdownBlockType.Table,
725
+ headers: [],
726
+ rows: [],
727
+ alignments: [],
728
+ },
729
+ ]);
730
+ });
731
+ });
732
+
733
+ describe("unknown top-level nodes", () => {
734
+ test("recurses into the children of an unknown container", () => {
735
+ expect(
736
+ blocksOf(
737
+ root([
738
+ {
739
+ type: "customBlock",
740
+ children: [{ type: "paragraph", children: [text("inside")] }],
741
+ },
742
+ ]),
743
+ ),
744
+ ).toEqual([
745
+ {
746
+ type: MarkdownBlockType.Paragraph,
747
+ inlines: [plain("inside")],
748
+ indent: 0,
749
+ },
750
+ ]);
751
+ });
752
+
753
+ test("emits nothing for an unknown leaf node", () => {
754
+ expect(blocksOf(root([{ type: "yaml" }]))).toEqual([]);
755
+ });
756
+ });
757
+
758
+ describe("document order", () => {
759
+ test("preserves block order across a mixed document", () => {
760
+ const blocks: Array<MarkdownBlock> = blocksOf(
761
+ root([
762
+ { type: "heading", depth: 1, children: [text("Title")] },
763
+ { type: "paragraph", children: [text("intro")] },
764
+ {
765
+ type: "list",
766
+ children: [
767
+ {
768
+ type: "listItem",
769
+ children: [{ type: "paragraph", children: [text("point")] }],
770
+ },
771
+ ],
772
+ },
773
+ { type: "code", lang: "js", value: "x" },
774
+ { type: "thematicBreak" },
775
+ ]),
776
+ );
777
+
778
+ expect(
779
+ blocks.map((block: MarkdownBlock) => {
780
+ return block.type;
781
+ }),
782
+ ).toEqual([
783
+ MarkdownBlockType.Heading,
784
+ MarkdownBlockType.Paragraph,
785
+ MarkdownBlockType.ListItem,
786
+ MarkdownBlockType.Code,
787
+ MarkdownBlockType.Rule,
788
+ ]);
789
+ });
790
+ });
791
+ });