@flamingo-stack/openframe-frontend-core 0.0.284 → 0.0.285-snapshot.20260618135145

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 (229) hide show
  1. package/dist/chunk-3MCHAFHB.js +89 -0
  2. package/dist/chunk-3MCHAFHB.js.map +1 -0
  3. package/dist/{chunk-IMNGGLKL.js → chunk-3ZXUQQL4.js} +2 -2
  4. package/dist/{chunk-BRLGG3EZ.cjs → chunk-4WACBTZU.cjs} +30 -30
  5. package/dist/{chunk-BRLGG3EZ.cjs.map → chunk-4WACBTZU.cjs.map} +1 -1
  6. package/dist/{chunk-P4ARMFW3.js → chunk-5BTZOVDQ.js} +4 -4
  7. package/dist/chunk-5KD3S25X.cjs +312 -0
  8. package/dist/chunk-5KD3S25X.cjs.map +1 -0
  9. package/dist/chunk-6N26CURS.cjs +2417 -0
  10. package/dist/chunk-6N26CURS.cjs.map +1 -0
  11. package/dist/{chunk-XX7P4IL2.js → chunk-7NM7DEUK.js} +4 -143
  12. package/dist/chunk-7NM7DEUK.js.map +1 -0
  13. package/dist/chunk-BBZ7AX5H.cjs +290 -0
  14. package/dist/chunk-BBZ7AX5H.cjs.map +1 -0
  15. package/dist/chunk-C6SCWXDP.js +290 -0
  16. package/dist/chunk-C6SCWXDP.js.map +1 -0
  17. package/dist/{chunk-GVSTRN36.cjs → chunk-D3LEFMOA.cjs} +3 -3
  18. package/dist/{chunk-GVSTRN36.cjs.map → chunk-D3LEFMOA.cjs.map} +1 -1
  19. package/dist/{chunk-3SM6DA24.cjs → chunk-D5YY5U6J.cjs} +19 -15
  20. package/dist/chunk-D5YY5U6J.cjs.map +1 -0
  21. package/dist/chunk-DDAT4RKX.js +2417 -0
  22. package/dist/chunk-DDAT4RKX.js.map +1 -0
  23. package/dist/{chunk-PVXCVPJD.cjs → chunk-EI4WALN2.cjs} +67 -1626
  24. package/dist/chunk-EI4WALN2.cjs.map +1 -0
  25. package/dist/{chunk-ZIODAAFY.cjs → chunk-EL5GCMPU.cjs} +20 -159
  26. package/dist/chunk-EL5GCMPU.cjs.map +1 -0
  27. package/dist/{chunk-RGXDDOQR.js → chunk-FSBDVT6R.js} +655 -2419
  28. package/dist/chunk-FSBDVT6R.js.map +1 -0
  29. package/dist/{chunk-24L4FLRC.cjs → chunk-GDF2R2ER.cjs} +1079 -2843
  30. package/dist/chunk-GDF2R2ER.cjs.map +1 -0
  31. package/dist/chunk-HLQW7MWJ.cjs +1594 -0
  32. package/dist/chunk-HLQW7MWJ.cjs.map +1 -0
  33. package/dist/chunk-I6ZPGKZ2.cjs +151 -0
  34. package/dist/chunk-I6ZPGKZ2.cjs.map +1 -0
  35. package/dist/{chunk-J66U3LPZ.cjs → chunk-KBKZYJRI.cjs} +33 -332
  36. package/dist/chunk-KBKZYJRI.cjs.map +1 -0
  37. package/dist/{chunk-USPATRSC.js → chunk-N6ZM5PYZ.js} +45 -344
  38. package/dist/chunk-N6ZM5PYZ.js.map +1 -0
  39. package/dist/{chunk-RGALV4UX.js → chunk-POKKCWKF.js} +3 -82
  40. package/dist/chunk-POKKCWKF.js.map +1 -0
  41. package/dist/{chunk-XAS7JVHX.cjs → chunk-QCKN37OP.cjs} +45 -41
  42. package/dist/chunk-QCKN37OP.cjs.map +1 -0
  43. package/dist/{chunk-VKDJXB7L.js → chunk-QF2X6PTD.js} +10 -6
  44. package/dist/{chunk-VKDJXB7L.js.map → chunk-QF2X6PTD.js.map} +1 -1
  45. package/dist/{chunk-Z3V2SGQZ.js → chunk-R2KT5GDD.js} +324 -1883
  46. package/dist/chunk-R2KT5GDD.js.map +1 -0
  47. package/dist/{chunk-HX7ZGKQ4.cjs → chunk-RQ6RTBKF.cjs} +16 -12
  48. package/dist/chunk-RQ6RTBKF.cjs.map +1 -0
  49. package/dist/chunk-TFSYSWPS.cjs +89 -0
  50. package/dist/chunk-TFSYSWPS.cjs.map +1 -0
  51. package/dist/chunk-TYZEMPPH.js +312 -0
  52. package/dist/chunk-TYZEMPPH.js.map +1 -0
  53. package/dist/{chunk-33RLTUN2.js → chunk-VCQ3CTYK.js} +12 -8
  54. package/dist/{chunk-33RLTUN2.js.map → chunk-VCQ3CTYK.js.map} +1 -1
  55. package/dist/chunk-VRSXJ5QJ.js +1599 -0
  56. package/dist/chunk-VRSXJ5QJ.js.map +1 -0
  57. package/dist/{chunk-OUI2MBU7.js → chunk-XQL4WDML.js} +11 -7
  58. package/dist/{chunk-OUI2MBU7.js.map → chunk-XQL4WDML.js.map} +1 -1
  59. package/dist/chunk-Y4JNA4W6.cjs +1599 -0
  60. package/dist/chunk-Y4JNA4W6.cjs.map +1 -0
  61. package/dist/{chunk-ZSUNQJRO.cjs → chunk-YETA25JW.cjs} +2 -81
  62. package/dist/chunk-YETA25JW.cjs.map +1 -0
  63. package/dist/chunk-YVB3VDIQ.js +1594 -0
  64. package/dist/chunk-YVB3VDIQ.js.map +1 -0
  65. package/dist/chunk-ZLN6SM2U.js +151 -0
  66. package/dist/chunk-ZLN6SM2U.js.map +1 -0
  67. package/dist/components/chat/chat-attachment-bar.d.ts.map +1 -1
  68. package/dist/components/chat/index.cjs +12 -6
  69. package/dist/components/chat/index.cjs.map +1 -1
  70. package/dist/components/chat/index.js +45 -39
  71. package/dist/components/contact/index.cjs +11 -7
  72. package/dist/components/contact/index.cjs.map +1 -1
  73. package/dist/components/contact/index.js +10 -6
  74. package/dist/components/docs/doc-navigation-context.d.ts +39 -0
  75. package/dist/components/docs/doc-navigation-context.d.ts.map +1 -0
  76. package/dist/components/docs/doc-viewer.d.ts +66 -0
  77. package/dist/components/docs/doc-viewer.d.ts.map +1 -0
  78. package/dist/components/docs/index.cjs +34 -0
  79. package/dist/components/docs/index.cjs.map +1 -0
  80. package/dist/components/docs/index.d.ts +8 -0
  81. package/dist/components/docs/index.d.ts.map +1 -0
  82. package/dist/components/docs/index.js +34 -0
  83. package/dist/components/docs/index.js.map +1 -0
  84. package/dist/components/docs/use-document-tree.d.ts +41 -0
  85. package/dist/components/docs/use-document-tree.d.ts.map +1 -0
  86. package/dist/components/docs/use-scroll-spy.d.ts +16 -0
  87. package/dist/components/docs/use-scroll-spy.d.ts.map +1 -0
  88. package/dist/components/embeds/embed-iframe.d.ts +29 -0
  89. package/dist/components/embeds/embed-iframe.d.ts.map +1 -0
  90. package/dist/components/embeds/figma-embed.d.ts +27 -0
  91. package/dist/components/embeds/figma-embed.d.ts.map +1 -0
  92. package/dist/components/embeds/google-sheets-viewer.d.ts +8 -0
  93. package/dist/components/embeds/google-sheets-viewer.d.ts.map +1 -0
  94. package/dist/components/embeds/index.cjs +37 -0
  95. package/dist/components/embeds/index.cjs.map +1 -0
  96. package/dist/components/embeds/index.d.ts +9 -0
  97. package/dist/components/embeds/index.d.ts.map +1 -0
  98. package/dist/components/embeds/index.js +37 -0
  99. package/dist/components/embeds/index.js.map +1 -0
  100. package/dist/components/embeds/pdf-viewer.d.ts +10 -0
  101. package/dist/components/embeds/pdf-viewer.d.ts.map +1 -0
  102. package/dist/components/faq/index.cjs +12 -8
  103. package/dist/components/faq/index.cjs.map +1 -1
  104. package/dist/components/faq/index.js +11 -7
  105. package/dist/components/features/board/board-column.d.ts +3 -1
  106. package/dist/components/features/board/board-column.d.ts.map +1 -1
  107. package/dist/components/features/board/board-ticket-approval.d.ts +16 -0
  108. package/dist/components/features/board/board-ticket-approval.d.ts.map +1 -0
  109. package/dist/components/features/board/board.d.ts +3 -1
  110. package/dist/components/features/board/board.d.ts.map +1 -1
  111. package/dist/components/features/board/index.d.ts +3 -1
  112. package/dist/components/features/board/index.d.ts.map +1 -1
  113. package/dist/components/features/board/ticket-card.d.ts +3 -1
  114. package/dist/components/features/board/ticket-card.d.ts.map +1 -1
  115. package/dist/components/features/board/types.d.ts +11 -0
  116. package/dist/components/features/board/types.d.ts.map +1 -1
  117. package/dist/components/features/index.cjs +12 -6
  118. package/dist/components/features/index.cjs.map +1 -1
  119. package/dist/components/features/index.js +11 -5
  120. package/dist/components/index.cjs +249 -1120
  121. package/dist/components/index.cjs.map +1 -1
  122. package/dist/components/index.d.ts +2 -0
  123. package/dist/components/index.d.ts.map +1 -1
  124. package/dist/components/index.js +647 -1518
  125. package/dist/components/index.js.map +1 -1
  126. package/dist/components/navigation/index.cjs +15 -6
  127. package/dist/components/navigation/index.cjs.map +1 -1
  128. package/dist/components/navigation/index.d.ts +2 -0
  129. package/dist/components/navigation/index.d.ts.map +1 -1
  130. package/dist/components/navigation/index.js +15 -6
  131. package/dist/components/navigation/multi-level-navigation.d.ts +28 -0
  132. package/dist/components/navigation/multi-level-navigation.d.ts.map +1 -0
  133. package/dist/components/onboarding-guides/index.cjs +34 -29
  134. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  135. package/dist/components/onboarding-guides/index.js +13 -8
  136. package/dist/components/onboarding-guides/index.js.map +1 -1
  137. package/dist/components/related-content/index.cjs +12 -8
  138. package/dist/components/related-content/index.cjs.map +1 -1
  139. package/dist/components/related-content/index.js +11 -7
  140. package/dist/components/tickets/index.cjs +75 -70
  141. package/dist/components/tickets/index.cjs.map +1 -1
  142. package/dist/components/tickets/index.js +16 -11
  143. package/dist/components/tickets/index.js.map +1 -1
  144. package/dist/components/ui/index.cjs +12 -6
  145. package/dist/components/ui/index.cjs.map +1 -1
  146. package/dist/components/ui/index.js +65 -59
  147. package/dist/embed-shims/index.cjs +3 -3
  148. package/dist/embed-shims/index.cjs.map +1 -1
  149. package/dist/embed-shims/index.js +8 -8
  150. package/dist/hooks/index.cjs +6 -3
  151. package/dist/hooks/index.cjs.map +1 -1
  152. package/dist/hooks/index.js +7 -4
  153. package/dist/index.cjs +64 -6
  154. package/dist/index.cjs.map +1 -1
  155. package/dist/index.js +259 -201
  156. package/dist/types/doc-source.cjs +4 -0
  157. package/dist/types/doc-source.cjs.map +1 -0
  158. package/dist/types/doc-source.d.ts +86 -0
  159. package/dist/types/doc-source.d.ts.map +1 -0
  160. package/dist/types/doc-source.js +3 -0
  161. package/dist/types/doc-source.js.map +1 -0
  162. package/dist/types/index.d.ts +1 -0
  163. package/dist/types/index.d.ts.map +1 -1
  164. package/dist/utils/doc-path-utils.d.ts +24 -0
  165. package/dist/utils/doc-path-utils.d.ts.map +1 -0
  166. package/dist/utils/doc-tree-nav.d.ts +35 -0
  167. package/dist/utils/doc-tree-nav.d.ts.map +1 -0
  168. package/dist/utils/embed-url-converters.d.ts +16 -0
  169. package/dist/utils/embed-url-converters.d.ts.map +1 -0
  170. package/dist/utils/format.d.ts +9 -0
  171. package/dist/utils/format.d.ts.map +1 -1
  172. package/dist/utils/index.cjs +297 -0
  173. package/dist/utils/index.cjs.map +1 -1
  174. package/dist/utils/index.d.ts +7 -1
  175. package/dist/utils/index.d.ts.map +1 -1
  176. package/dist/utils/index.js +276 -1
  177. package/dist/utils/index.js.map +1 -1
  178. package/dist/utils/markdown-section-extractor.d.ts +23 -0
  179. package/dist/utils/markdown-section-extractor.d.ts.map +1 -0
  180. package/dist/utils/markdown-to-plain.d.ts +32 -0
  181. package/dist/utils/markdown-to-plain.d.ts.map +1 -0
  182. package/dist/utils/tree-builder.d.ts +30 -0
  183. package/dist/utils/tree-builder.d.ts.map +1 -0
  184. package/package.json +19 -1
  185. package/src/components/chat/chat-attachment-bar.tsx +6 -11
  186. package/src/components/docs/doc-navigation-context.tsx +123 -0
  187. package/src/components/docs/doc-viewer.tsx +345 -0
  188. package/src/components/docs/index.ts +12 -0
  189. package/src/components/docs/use-document-tree.ts +427 -0
  190. package/src/components/docs/use-scroll-spy.ts +93 -0
  191. package/src/components/embeds/embed-iframe.tsx +103 -0
  192. package/src/components/embeds/figma-embed.tsx +147 -0
  193. package/src/components/embeds/google-sheets-viewer.tsx +54 -0
  194. package/src/components/embeds/index.ts +11 -0
  195. package/src/components/embeds/pdf-viewer.tsx +64 -0
  196. package/src/components/features/board/board-column.tsx +11 -1
  197. package/src/components/features/board/board-ticket-approval.tsx +76 -0
  198. package/src/components/features/board/board.tsx +6 -0
  199. package/src/components/features/board/index.ts +3 -0
  200. package/src/components/features/board/ticket-card.tsx +12 -0
  201. package/src/components/features/board/types.ts +12 -0
  202. package/src/components/index.ts +2 -0
  203. package/src/components/navigation/index.ts +5 -0
  204. package/src/components/navigation/multi-level-navigation.tsx +358 -0
  205. package/src/types/doc-source.ts +88 -0
  206. package/src/types/index.ts +4 -0
  207. package/src/utils/doc-path-utils.ts +47 -0
  208. package/src/utils/doc-tree-nav.ts +111 -0
  209. package/src/utils/embed-url-converters.ts +77 -0
  210. package/src/utils/format.ts +16 -0
  211. package/src/utils/index.ts +11 -0
  212. package/src/utils/markdown-section-extractor.ts +106 -0
  213. package/src/utils/markdown-to-plain.ts +69 -0
  214. package/src/utils/tree-builder.ts +114 -0
  215. package/dist/chunk-24L4FLRC.cjs.map +0 -1
  216. package/dist/chunk-3SM6DA24.cjs.map +0 -1
  217. package/dist/chunk-HX7ZGKQ4.cjs.map +0 -1
  218. package/dist/chunk-J66U3LPZ.cjs.map +0 -1
  219. package/dist/chunk-PVXCVPJD.cjs.map +0 -1
  220. package/dist/chunk-RGALV4UX.js.map +0 -1
  221. package/dist/chunk-RGXDDOQR.js.map +0 -1
  222. package/dist/chunk-USPATRSC.js.map +0 -1
  223. package/dist/chunk-XAS7JVHX.cjs.map +0 -1
  224. package/dist/chunk-XX7P4IL2.js.map +0 -1
  225. package/dist/chunk-Z3V2SGQZ.js.map +0 -1
  226. package/dist/chunk-ZIODAAFY.cjs.map +0 -1
  227. package/dist/chunk-ZSUNQJRO.cjs.map +0 -1
  228. /package/dist/{chunk-IMNGGLKL.js.map → chunk-3ZXUQQL4.js.map} +0 -0
  229. /package/dist/{chunk-P4ARMFW3.js.map → chunk-5BTZOVDQ.js.map} +0 -0
@@ -81,6 +81,22 @@ export function formatBytesShort(bytes: number): string {
81
81
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
82
82
  }
83
83
 
84
+ /**
85
+ * File-size formatter (1 decimal place, `'B'` unit, caps at `GB`).
86
+ * Hub historical semantics — distinct from `formatBytesShort` (2dp) and
87
+ * `formatBytes` (2dp / `'Bytes'`). Lifted from the hub during the doc-viewer
88
+ * unification so all upload UIs, publication cards, and data-room file-size
89
+ * displays render the same way.
90
+ * @example formatFileSize(0) → "0 B"; formatFileSize(1500) → "1.5 KB"
91
+ */
92
+ export function formatFileSize(bytes: number): string {
93
+ if (bytes === 0) return '0 B'
94
+ const k = 1024
95
+ const sizes = ['B', 'KB', 'MB', 'GB']
96
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
97
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
98
+ }
99
+
84
100
  /**
85
101
  * Format large numbers to abbreviated form (K, M, B) with no decimal points
86
102
  * @param num - The number to format
@@ -102,6 +102,7 @@ export {
102
102
  formatWholeDollars,
103
103
  formatLegalDate,
104
104
  formatBytesShort,
105
+ formatFileSize,
105
106
  formatDateTimeAt,
106
107
  formatDurationFromMs,
107
108
  type MetricFormat,
@@ -283,3 +284,13 @@ export {
283
284
  // decision fn the lib forms feed. Also exported via the granular subpath
284
285
  // `./utils/humanity-signals` for server-only consumers.
285
286
  export * from './humanity-signals'
287
+
288
+ // Doc-source viewer utilities (path parsing, tree building, section extraction,
289
+ // embed-URL conversion) — single home for all doc-viewer pure helpers across
290
+ // hub + lib consumers (knowledge-base, data-room, and future sources).
291
+ export * from './doc-path-utils'
292
+ export * from './doc-tree-nav'
293
+ export * from './tree-builder'
294
+ export * from './markdown-section-extractor'
295
+ export * from './markdown-to-plain'
296
+ export * from './embed-url-converters'
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Shared markdown section extractor
3
+ * Extracts heading sections from markdown content for navigation.
4
+ */
5
+
6
+ import { stripInlineMarkdown } from './markdown-to-plain'
7
+
8
+ export interface MarkdownSection {
9
+ id: string
10
+ title: string
11
+ level: number
12
+ }
13
+
14
+ export interface ExtractSectionsOptions {
15
+ /** Maximum heading level to include (default: 2 = H1-H2) */
16
+ maxLevel?: number
17
+ /** Remove emoji characters from IDs (default: true) */
18
+ removeEmojis?: boolean
19
+ /** Handle duplicate IDs by appending a counter suffix (default: true) */
20
+ handleDuplicateIds?: boolean
21
+ /** Strip bold, italic, and inline code markers from titles (default: true) */
22
+ stripFormattingMarkers?: boolean
23
+ /** Skip code blocks and YAML frontmatter (default: true) */
24
+ skipCodeAndYamlBlocks?: boolean
25
+ }
26
+
27
+ const DEFAULT_OPTIONS: Required<ExtractSectionsOptions> = {
28
+ maxLevel: 2,
29
+ removeEmojis: true,
30
+ handleDuplicateIds: true,
31
+ stripFormattingMarkers: true,
32
+ skipCodeAndYamlBlocks: true,
33
+ }
34
+
35
+ export function extractSections(
36
+ markdown: string,
37
+ options: ExtractSectionsOptions = {},
38
+ ): MarkdownSection[] {
39
+ const opts = { ...DEFAULT_OPTIONS, ...options }
40
+ const sections: MarkdownSection[] = []
41
+ const lines = markdown.split('\n')
42
+ const idCounts: Record<string, number> = {}
43
+
44
+ let inCodeBlock = false
45
+ let inYamlBlock = false
46
+
47
+ for (const line of lines) {
48
+ if (opts.skipCodeAndYamlBlocks) {
49
+ if (line.startsWith('```')) {
50
+ inCodeBlock = !inCodeBlock
51
+ continue
52
+ }
53
+ if (line === '---') {
54
+ inYamlBlock = !inYamlBlock
55
+ continue
56
+ }
57
+ if (inCodeBlock || inYamlBlock) continue
58
+ }
59
+
60
+ const pattern = new RegExp(`^(#{1,${opts.maxLevel}})\\s+(.+)`)
61
+ const match = line.match(pattern)
62
+ if (!match) continue
63
+
64
+ const level = match[1].length
65
+ if (level > opts.maxLevel) continue
66
+
67
+ let title = match[2].trim()
68
+
69
+ if (opts.stripFormattingMarkers) {
70
+ title = stripInlineMarkdown(title).trim()
71
+ }
72
+
73
+ let baseId = title
74
+
75
+ if (opts.removeEmojis) {
76
+ baseId = baseId
77
+ .replace(
78
+ /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu,
79
+ '',
80
+ )
81
+ .trim()
82
+ }
83
+
84
+ baseId = baseId
85
+ .toLowerCase()
86
+ .replace(/[^\w\s-]/g, '')
87
+ .replace(/\s+/g, '-')
88
+ .replace(/^-+|-+$/g, '')
89
+
90
+ const cleanId = baseId || `section-${sections.length + 1}`
91
+
92
+ let id = cleanId
93
+ if (opts.handleDuplicateIds) {
94
+ if (idCounts[cleanId]) {
95
+ idCounts[cleanId]++
96
+ id = `${cleanId}-${idCounts[cleanId]}`
97
+ } else {
98
+ idCounts[cleanId] = 1
99
+ }
100
+ }
101
+
102
+ sections.push({ id, title, level })
103
+ }
104
+
105
+ return sections
106
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Convert markdown source to plain text. Used by both the SEO server component
3
+ * (article-body echo in JSON-LD) and chat entity-card preview (clamped previews
4
+ * inside chat cards).
5
+ *
6
+ * The two consumers differ only in pre/post processing — `mode: 'article'`
7
+ * strips code blocks + images + headings + list bullets and preserves paragraph
8
+ * breaks; `mode: 'preview'` collapses everything to a single line. Both share
9
+ * the same inline-marker strip (link / bold / italic / inline-code).
10
+ *
11
+ * All regex patterns are linear (negated character classes, non-greedy with
12
+ * bounded targets) — no nested quantifiers, no catastrophic backtracking on
13
+ * pathological input.
14
+ */
15
+
16
+ export interface MarkdownToPlainOptions {
17
+ /**
18
+ * - `'article'` (default): preserve paragraph structure (`\n\n` as separator),
19
+ * strip code blocks + images + headings + list bullets. Used for the
20
+ * article-body echo on SEO surfaces.
21
+ * - `'preview'`: collapse all whitespace to single spaces; result is a single
22
+ * line. Used for chat-card previews / OG-card snippets where line breaks
23
+ * would wrap awkwardly.
24
+ */
25
+ mode?: 'article' | 'preview'
26
+ /** Optional max-character cap. Truncates with no ellipsis (caller-decided). */
27
+ maxChars?: number
28
+ }
29
+
30
+ /** Strip inline markdown markers (bold, italic, inline-code, links). Shared
31
+ * by `extractSections`'s `stripFormattingMarkers` and `markdownToPlainText`. */
32
+ export function stripInlineMarkdown(s: string): string {
33
+ return s
34
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // [text](url) → text
35
+ .replace(/\*\*([^*]+)\*\*/g, '$1') // **bold** → bold
36
+ .replace(/\*([^*]+)\*/g, '$1') // *italic* → italic
37
+ .replace(/`([^`]+)`/g, '$1') // `inline code` → inline code
38
+ }
39
+
40
+ export function markdownToPlainText(
41
+ markdown: string,
42
+ options: MarkdownToPlainOptions = {},
43
+ ): string {
44
+ const { mode = 'article', maxChars } = options
45
+
46
+ let out = markdown
47
+ // Fenced code blocks — must run BEFORE inline strippers (otherwise the
48
+ // backtick-grave strip below grazes them).
49
+ .replace(/```[\s\S]*?```/g, '')
50
+ // Images — `![alt](src)` → empty (we drop them entirely in plain text).
51
+ .replace(/!\[[^\]]*\]\([^)]+\)/g, '')
52
+
53
+ out = stripInlineMarkdown(out)
54
+ // Heading markers (any depth).
55
+ .replace(/^#+\s*/gm, '')
56
+ // List bullets at start-of-line.
57
+ .replace(/^\s*[-*+]\s+/gm, '')
58
+
59
+ if (mode === 'preview') {
60
+ out = out.replace(/\s+/g, ' ').trim()
61
+ } else {
62
+ out = out.replace(/\n{3,}/g, '\n\n').trim()
63
+ }
64
+
65
+ if (maxChars != null && out.length > maxChars) {
66
+ out = out.slice(0, maxChars)
67
+ }
68
+ return out
69
+ }
@@ -0,0 +1,114 @@
1
+ // =============================================================================
2
+ // Shared Document Tree Builder
3
+ // =============================================================================
4
+ // Generic 3-pass algorithm for building hierarchical trees from flat records.
5
+ // Used by all doc-source DALs (openframe-docs, data-room-docs, future).
6
+
7
+ /**
8
+ * Base interface for tree nodes. Consumers extend this with additional fields.
9
+ */
10
+ export interface TreeNodeBase {
11
+ id: string
12
+ name: string
13
+ path: string
14
+ type: 'file' | 'folder'
15
+ sortOrder?: number
16
+ children?: TreeNodeBase[]
17
+ }
18
+
19
+ /**
20
+ * Format a document name for display: remove .md extension and capitalize.
21
+ */
22
+ export function formatDocName(name: string): string {
23
+ let displayName = name
24
+ if (displayName.endsWith('.md')) {
25
+ displayName = displayName.slice(0, -3)
26
+ }
27
+ if (displayName.toLowerCase() === 'readme') {
28
+ return 'README'
29
+ }
30
+ return displayName.charAt(0).toUpperCase() + displayName.slice(1).replace(/-/g, ' ')
31
+ }
32
+
33
+ /**
34
+ * Sort children: README first, then folders, then files — each group by sort_order then alphabetical.
35
+ * Recurses into folder children to ensure consistent ordering at all levels.
36
+ */
37
+ export function sortTreeChildren<TNode extends TreeNodeBase>(nodes: TNode[]): TNode[] {
38
+ nodes.sort((a, b) => {
39
+ const aIsReadme = a.name.toLowerCase() === 'readme' || a.name.toLowerCase() === 'readme.md'
40
+ const bIsReadme = b.name.toLowerCase() === 'readme' || b.name.toLowerCase() === 'readme.md'
41
+ if (aIsReadme && !bIsReadme) return -1
42
+ if (!aIsReadme && bIsReadme) return 1
43
+
44
+ if (a.type === 'folder' && b.type !== 'folder') return -1
45
+ if (a.type !== 'folder' && b.type === 'folder') return 1
46
+
47
+ const aOrder = a.sortOrder ?? Infinity
48
+ const bOrder = b.sortOrder ?? Infinity
49
+ if (aOrder !== bOrder) return aOrder - bOrder
50
+
51
+ return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
52
+ })
53
+
54
+ for (const node of nodes) {
55
+ if (node.type === 'folder' && node.children) {
56
+ node.children = sortTreeChildren(node.children as TNode[])
57
+ }
58
+ }
59
+
60
+ return nodes
61
+ }
62
+
63
+ /**
64
+ * Build a hierarchical document tree from a flat list of records.
65
+ *
66
+ * 3-pass algorithm:
67
+ * 1. Create node map from flat records using the provided `mapFn`
68
+ * 2. Build parent-child hierarchy using `getParentPath`
69
+ * 3. Sort children (folders first, README, then others)
70
+ */
71
+ export function buildDocumentTree<TNode extends TreeNodeBase, TDoc>(
72
+ docs: TDoc[],
73
+ mapFn: (doc: TDoc) => TNode,
74
+ getParentPath: (doc: TDoc) => string | null,
75
+ getPath: (doc: TDoc) => string
76
+ ): TNode[] {
77
+ const nodeMap = new Map<string, TNode>()
78
+ const rootNodes: TNode[] = []
79
+
80
+ for (const doc of docs) {
81
+ const node = mapFn(doc)
82
+ nodeMap.set(getPath(doc), node)
83
+ }
84
+
85
+ for (const doc of docs) {
86
+ const node = nodeMap.get(getPath(doc))!
87
+ let resolvedParentPath = getParentPath(doc)
88
+
89
+ if (!resolvedParentPath) {
90
+ const nodePath = getPath(doc)
91
+ const lastSlash = nodePath.lastIndexOf('/')
92
+ if (lastSlash > 0) {
93
+ resolvedParentPath = nodePath.substring(0, lastSlash)
94
+ }
95
+ }
96
+
97
+ if (resolvedParentPath) {
98
+ const parent = nodeMap.get(resolvedParentPath)
99
+ if (parent) {
100
+ // Lazily initialize `children` so callers whose `mapFn` doesn't
101
+ // pre-seed it still get a proper tree (previously a missing
102
+ // `parent.children` silently flattened the child to the root).
103
+ if (!parent.children) parent.children = []
104
+ ;(parent.children as TNode[]).push(node)
105
+ } else {
106
+ rootNodes.push(node)
107
+ }
108
+ } else {
109
+ rootNodes.push(node)
110
+ }
111
+ }
112
+
113
+ return sortTreeChildren(rootNodes)
114
+ }