@furystack/shades-common-components 12.1.0 → 12.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/esm/components/avatar.d.ts.map +1 -1
- package/esm/components/avatar.js +3 -1
- package/esm/components/avatar.js.map +1 -1
- package/esm/components/avatar.spec.js +4 -4
- package/esm/components/avatar.spec.js.map +1 -1
- package/esm/components/icons/icon-definitions.d.ts +82 -0
- package/esm/components/icons/icon-definitions.d.ts.map +1 -1
- package/esm/components/icons/icon-definitions.js +717 -0
- package/esm/components/icons/icon-definitions.js.map +1 -1
- package/esm/components/icons/icon-definitions.spec.js +22 -2
- package/esm/components/icons/icon-definitions.spec.js.map +1 -1
- package/esm/components/icons/icon-types.d.ts +10 -0
- package/esm/components/icons/icon-types.d.ts.map +1 -1
- package/esm/components/icons/index.d.ts +1 -1
- package/esm/components/icons/index.d.ts.map +1 -1
- package/esm/components/index.d.ts +1 -0
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +1 -0
- package/esm/components/index.js.map +1 -1
- package/esm/components/markdown/index.d.ts +5 -0
- package/esm/components/markdown/index.d.ts.map +1 -0
- package/esm/components/markdown/index.js +5 -0
- package/esm/components/markdown/index.js.map +1 -0
- package/esm/components/markdown/markdown-display.d.ts +19 -0
- package/esm/components/markdown/markdown-display.d.ts.map +1 -0
- package/esm/components/markdown/markdown-display.js +149 -0
- package/esm/components/markdown/markdown-display.js.map +1 -0
- package/esm/components/markdown/markdown-display.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-display.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-display.spec.js +191 -0
- package/esm/components/markdown/markdown-display.spec.js.map +1 -0
- package/esm/components/markdown/markdown-editor.d.ts +25 -0
- package/esm/components/markdown/markdown-editor.d.ts.map +1 -0
- package/esm/components/markdown/markdown-editor.js +113 -0
- package/esm/components/markdown/markdown-editor.js.map +1 -0
- package/esm/components/markdown/markdown-editor.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-editor.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-editor.spec.js +111 -0
- package/esm/components/markdown/markdown-editor.spec.js.map +1 -0
- package/esm/components/markdown/markdown-input.d.ts +29 -0
- package/esm/components/markdown/markdown-input.d.ts.map +1 -0
- package/esm/components/markdown/markdown-input.js +100 -0
- package/esm/components/markdown/markdown-input.js.map +1 -0
- package/esm/components/markdown/markdown-input.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-input.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-input.spec.js +215 -0
- package/esm/components/markdown/markdown-input.spec.js.map +1 -0
- package/esm/components/markdown/markdown-parser.d.ts +82 -0
- package/esm/components/markdown/markdown-parser.d.ts.map +1 -0
- package/esm/components/markdown/markdown-parser.js +274 -0
- package/esm/components/markdown/markdown-parser.js.map +1 -0
- package/esm/components/markdown/markdown-parser.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-parser.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-parser.spec.js +229 -0
- package/esm/components/markdown/markdown-parser.spec.js.map +1 -0
- package/esm/components/page-container/index.d.ts +1 -1
- package/esm/components/page-container/index.js +1 -1
- package/esm/components/page-container/page-header.d.ts +5 -5
- package/esm/components/page-container/page-header.d.ts.map +1 -1
- package/esm/components/page-container/page-header.js +3 -3
- package/esm/components/styles.d.ts +1 -0
- package/esm/components/styles.d.ts.map +1 -1
- package/esm/components/styles.js.map +1 -1
- package/esm/components/suggest/index.d.ts +1 -1
- package/esm/components/suggest/index.d.ts.map +1 -1
- package/esm/components/typography.d.ts.map +1 -1
- package/esm/components/typography.js +26 -14
- package/esm/components/typography.js.map +1 -1
- package/esm/services/css-variable-theme.d.ts +3 -0
- package/esm/services/css-variable-theme.d.ts.map +1 -1
- package/esm/services/css-variable-theme.js +3 -0
- package/esm/services/css-variable-theme.js.map +1 -1
- package/esm/services/css-variable-theme.spec.js +3 -0
- package/esm/services/css-variable-theme.spec.js.map +1 -1
- package/esm/services/default-dark-palette.d.ts +8 -0
- package/esm/services/default-dark-palette.d.ts.map +1 -0
- package/esm/services/default-dark-palette.js +56 -0
- package/esm/services/default-dark-palette.js.map +1 -0
- package/esm/services/default-dark-theme.d.ts +3 -0
- package/esm/services/default-dark-theme.d.ts.map +1 -1
- package/esm/services/default-dark-theme.js +7 -4
- package/esm/services/default-dark-theme.js.map +1 -1
- package/esm/services/default-light-theme.d.ts +3 -0
- package/esm/services/default-light-theme.d.ts.map +1 -1
- package/esm/services/default-light-theme.js +3 -0
- package/esm/services/default-light-theme.js.map +1 -1
- package/esm/services/index.d.ts +1 -0
- package/esm/services/index.d.ts.map +1 -1
- package/esm/services/index.js +1 -0
- package/esm/services/index.js.map +1 -1
- package/esm/services/theme-provider-service.d.ts +10 -1
- package/esm/services/theme-provider-service.d.ts.map +1 -1
- package/esm/services/theme-provider-service.js.map +1 -1
- package/package.json +3 -3
- package/src/components/avatar.spec.tsx +4 -4
- package/src/components/avatar.tsx +3 -1
- package/src/components/icons/icon-definitions.spec.ts +28 -2
- package/src/components/icons/icon-definitions.ts +759 -0
- package/src/components/icons/icon-types.ts +12 -0
- package/src/components/icons/index.ts +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/markdown/index.ts +4 -0
- package/src/components/markdown/markdown-display.spec.tsx +243 -0
- package/src/components/markdown/markdown-display.tsx +202 -0
- package/src/components/markdown/markdown-editor.spec.tsx +142 -0
- package/src/components/markdown/markdown-editor.tsx +167 -0
- package/src/components/markdown/markdown-input.spec.tsx +274 -0
- package/src/components/markdown/markdown-input.tsx +143 -0
- package/src/components/markdown/markdown-parser.spec.ts +258 -0
- package/src/components/markdown/markdown-parser.ts +333 -0
- package/src/components/page-container/index.tsx +1 -1
- package/src/components/page-container/page-header.tsx +5 -5
- package/src/components/styles.tsx +1 -0
- package/src/components/suggest/index.tsx +1 -1
- package/src/components/typography.tsx +28 -15
- package/src/services/css-variable-theme.spec.ts +3 -0
- package/src/services/css-variable-theme.ts +3 -0
- package/src/services/default-dark-palette.ts +57 -0
- package/src/services/default-dark-theme.ts +7 -4
- package/src/services/default-light-theme.ts +3 -0
- package/src/services/index.ts +1 -0
- package/src/services/theme-provider-service.ts +7 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { parseInline, parseMarkdown, toggleCheckbox } from './markdown-parser.js'
|
|
3
|
+
|
|
4
|
+
describe('parseInline', () => {
|
|
5
|
+
it('should parse plain text', () => {
|
|
6
|
+
const result = parseInline('hello world')
|
|
7
|
+
expect(result).toEqual([{ type: 'text', content: 'hello world' }])
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should parse inline code', () => {
|
|
11
|
+
const result = parseInline('use `const x = 1` here')
|
|
12
|
+
expect(result).toEqual([
|
|
13
|
+
{ type: 'text', content: 'use ' },
|
|
14
|
+
{ type: 'code', content: 'const x = 1' },
|
|
15
|
+
{ type: 'text', content: ' here' },
|
|
16
|
+
])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should parse bold with **', () => {
|
|
20
|
+
const result = parseInline('this is **bold** text')
|
|
21
|
+
expect(result).toEqual([
|
|
22
|
+
{ type: 'text', content: 'this is ' },
|
|
23
|
+
{ type: 'bold', children: [{ type: 'text', content: 'bold' }] },
|
|
24
|
+
{ type: 'text', content: ' text' },
|
|
25
|
+
])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should parse italic with *', () => {
|
|
29
|
+
const result = parseInline('this is *italic* text')
|
|
30
|
+
expect(result).toEqual([
|
|
31
|
+
{ type: 'text', content: 'this is ' },
|
|
32
|
+
{ type: 'italic', children: [{ type: 'text', content: 'italic' }] },
|
|
33
|
+
{ type: 'text', content: ' text' },
|
|
34
|
+
])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should parse bold+italic with ***', () => {
|
|
38
|
+
const result = parseInline('this is ***bold italic*** text')
|
|
39
|
+
expect(result).toEqual([
|
|
40
|
+
{ type: 'text', content: 'this is ' },
|
|
41
|
+
{ type: 'bold', children: [{ type: 'italic', children: [{ type: 'text', content: 'bold italic' }] }] },
|
|
42
|
+
{ type: 'text', content: ' text' },
|
|
43
|
+
])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should parse links', () => {
|
|
47
|
+
const result = parseInline('click [here](https://example.com) now')
|
|
48
|
+
expect(result).toEqual([
|
|
49
|
+
{ type: 'text', content: 'click ' },
|
|
50
|
+
{ type: 'link', href: 'https://example.com', children: [{ type: 'text', content: 'here' }] },
|
|
51
|
+
{ type: 'text', content: ' now' },
|
|
52
|
+
])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should parse images', () => {
|
|
56
|
+
const result = parseInline('see  here')
|
|
57
|
+
expect(result).toEqual([
|
|
58
|
+
{ type: 'text', content: 'see ' },
|
|
59
|
+
{ type: 'image', src: 'image.png', alt: 'alt text' },
|
|
60
|
+
{ type: 'text', content: ' here' },
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should parse nested bold in link', () => {
|
|
65
|
+
const result = parseInline('[**bold link**](url)')
|
|
66
|
+
expect(result).toEqual([
|
|
67
|
+
{
|
|
68
|
+
type: 'link',
|
|
69
|
+
href: 'url',
|
|
70
|
+
children: [{ type: 'bold', children: [{ type: 'text', content: 'bold link' }] }],
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should handle empty string', () => {
|
|
76
|
+
expect(parseInline('')).toEqual([])
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('parseMarkdown', () => {
|
|
81
|
+
it('should return empty array for empty input', () => {
|
|
82
|
+
expect(parseMarkdown('')).toEqual([])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should parse headings level 1–6', () => {
|
|
86
|
+
const md = '# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6'
|
|
87
|
+
const result = parseMarkdown(md)
|
|
88
|
+
expect(result).toHaveLength(6)
|
|
89
|
+
for (let i = 0; i < 6; i++) {
|
|
90
|
+
expect(result[i]).toMatchObject({ type: 'heading', level: i + 1 })
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should parse a paragraph', () => {
|
|
95
|
+
const result = parseMarkdown('Hello world\nthis is a paragraph.')
|
|
96
|
+
expect(result).toEqual([
|
|
97
|
+
{
|
|
98
|
+
type: 'paragraph',
|
|
99
|
+
children: [{ type: 'text', content: 'Hello world this is a paragraph.' }],
|
|
100
|
+
},
|
|
101
|
+
])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should split paragraphs on blank lines', () => {
|
|
105
|
+
const result = parseMarkdown('First paragraph.\n\nSecond paragraph.')
|
|
106
|
+
expect(result).toHaveLength(2)
|
|
107
|
+
expect(result[0]).toMatchObject({ type: 'paragraph' })
|
|
108
|
+
expect(result[1]).toMatchObject({ type: 'paragraph' })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should parse fenced code blocks', () => {
|
|
112
|
+
const md = '```typescript\nconst x = 1\nconsole.log(x)\n```'
|
|
113
|
+
const result = parseMarkdown(md)
|
|
114
|
+
expect(result).toEqual([
|
|
115
|
+
{
|
|
116
|
+
type: 'codeBlock',
|
|
117
|
+
language: 'typescript',
|
|
118
|
+
content: 'const x = 1\nconsole.log(x)',
|
|
119
|
+
},
|
|
120
|
+
])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should parse fenced code blocks without language', () => {
|
|
124
|
+
const md = '```\nhello\n```'
|
|
125
|
+
const result = parseMarkdown(md)
|
|
126
|
+
expect(result).toEqual([
|
|
127
|
+
{
|
|
128
|
+
type: 'codeBlock',
|
|
129
|
+
language: undefined,
|
|
130
|
+
content: 'hello',
|
|
131
|
+
},
|
|
132
|
+
])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should parse horizontal rules', () => {
|
|
136
|
+
for (const rule of ['---', '***', '___', '----', '****']) {
|
|
137
|
+
const result = parseMarkdown(rule)
|
|
138
|
+
expect(result).toEqual([{ type: 'horizontalRule' }])
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should parse unordered lists', () => {
|
|
143
|
+
const md = '- Item 1\n- Item 2\n- Item 3'
|
|
144
|
+
const result = parseMarkdown(md)
|
|
145
|
+
expect(result).toHaveLength(1)
|
|
146
|
+
expect(result[0]).toMatchObject({ type: 'list', ordered: false })
|
|
147
|
+
const list = result[0] as { type: 'list'; items: unknown[] }
|
|
148
|
+
expect(list.items).toHaveLength(3)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should parse ordered lists', () => {
|
|
152
|
+
const md = '1. First\n2. Second\n3. Third'
|
|
153
|
+
const result = parseMarkdown(md)
|
|
154
|
+
expect(result).toHaveLength(1)
|
|
155
|
+
expect(result[0]).toMatchObject({ type: 'list', ordered: true })
|
|
156
|
+
const list = result[0] as { type: 'list'; items: unknown[] }
|
|
157
|
+
expect(list.items).toHaveLength(3)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should parse checkboxes in unordered lists', () => {
|
|
161
|
+
const md = '- [x] Done task\n- [ ] Todo task\n- Regular item'
|
|
162
|
+
const result = parseMarkdown(md)
|
|
163
|
+
expect(result).toHaveLength(1)
|
|
164
|
+
const list = result[0] as { type: 'list'; items: Array<{ checkbox?: string }> }
|
|
165
|
+
expect(list.items[0].checkbox).toBe('checked')
|
|
166
|
+
expect(list.items[1].checkbox).toBe('unchecked')
|
|
167
|
+
expect(list.items[2].checkbox).toBeUndefined()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('should track sourceLineIndex on list items', () => {
|
|
171
|
+
const md = '- [x] Done\n- [ ] Todo'
|
|
172
|
+
const result = parseMarkdown(md)
|
|
173
|
+
const list = result[0] as { type: 'list'; items: Array<{ sourceLineIndex: number }> }
|
|
174
|
+
expect(list.items[0].sourceLineIndex).toBe(0)
|
|
175
|
+
expect(list.items[1].sourceLineIndex).toBe(1)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should parse blockquotes', () => {
|
|
179
|
+
const md = '> This is a quote\n> Second line'
|
|
180
|
+
const result = parseMarkdown(md)
|
|
181
|
+
expect(result).toHaveLength(1)
|
|
182
|
+
expect(result[0]).toMatchObject({ type: 'blockquote' })
|
|
183
|
+
const bq = result[0] as { type: 'blockquote'; children: unknown[] }
|
|
184
|
+
expect(bq.children).toHaveLength(1)
|
|
185
|
+
expect(bq.children[0]).toMatchObject({ type: 'paragraph' })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should parse a complex document', () => {
|
|
189
|
+
const md = [
|
|
190
|
+
'# Title',
|
|
191
|
+
'',
|
|
192
|
+
'A paragraph with **bold** and *italic*.',
|
|
193
|
+
'',
|
|
194
|
+
'## List Section',
|
|
195
|
+
'',
|
|
196
|
+
'- [x] Task done',
|
|
197
|
+
'- [ ] Task pending',
|
|
198
|
+
'',
|
|
199
|
+
'> A blockquote',
|
|
200
|
+
'',
|
|
201
|
+
'---',
|
|
202
|
+
'',
|
|
203
|
+
'```js',
|
|
204
|
+
'console.log("hi")',
|
|
205
|
+
'```',
|
|
206
|
+
].join('\n')
|
|
207
|
+
|
|
208
|
+
const result = parseMarkdown(md)
|
|
209
|
+
expect(result[0]).toMatchObject({ type: 'heading', level: 1 })
|
|
210
|
+
expect(result[1]).toMatchObject({ type: 'paragraph' })
|
|
211
|
+
expect(result[2]).toMatchObject({ type: 'heading', level: 2 })
|
|
212
|
+
expect(result[3]).toMatchObject({ type: 'list', ordered: false })
|
|
213
|
+
expect(result[4]).toMatchObject({ type: 'blockquote' })
|
|
214
|
+
expect(result[5]).toMatchObject({ type: 'horizontalRule' })
|
|
215
|
+
expect(result[6]).toMatchObject({ type: 'codeBlock', language: 'js' })
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('toggleCheckbox', () => {
|
|
220
|
+
it('should toggle an unchecked checkbox to checked', () => {
|
|
221
|
+
const source = '- [ ] Todo item'
|
|
222
|
+
const result = toggleCheckbox(source, 0)
|
|
223
|
+
expect(result).toBe('- [x] Todo item')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should toggle a checked checkbox to unchecked', () => {
|
|
227
|
+
const source = '- [x] Done item'
|
|
228
|
+
const result = toggleCheckbox(source, 0)
|
|
229
|
+
expect(result).toBe('- [ ] Done item')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should toggle the correct line in a multi-line document', () => {
|
|
233
|
+
const source = '- [x] Done\n- [ ] Todo\n- [ ] Another'
|
|
234
|
+
const result = toggleCheckbox(source, 1)
|
|
235
|
+
expect(result).toBe('- [x] Done\n- [x] Todo\n- [ ] Another')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should return original string for out-of-bounds index', () => {
|
|
239
|
+
const source = '- [ ] Todo'
|
|
240
|
+
expect(toggleCheckbox(source, 5)).toBe(source)
|
|
241
|
+
expect(toggleCheckbox(source, -1)).toBe(source)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should return original string for non-checkbox line', () => {
|
|
245
|
+
const source = 'Regular text'
|
|
246
|
+
expect(toggleCheckbox(source, 0)).toBe(source)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should not toggle [ ] that appears outside a list item', () => {
|
|
250
|
+
const source = 'This line has [ ] in it but is not a list item'
|
|
251
|
+
expect(toggleCheckbox(source, 0)).toBe(source)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('should toggle checkboxes with * list marker', () => {
|
|
255
|
+
const source = '* [ ] Star item'
|
|
256
|
+
expect(toggleCheckbox(source, 0)).toBe('* [x] Star item')
|
|
257
|
+
})
|
|
258
|
+
})
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency Markdown parser that converts a Markdown string into an AST.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type MarkdownNode = HeadingNode | ParagraphNode | CodeBlockNode | BlockquoteNode | ListNode | HorizontalRuleNode
|
|
6
|
+
|
|
7
|
+
export type HeadingNode = {
|
|
8
|
+
type: 'heading'
|
|
9
|
+
level: 1 | 2 | 3 | 4 | 5 | 6
|
|
10
|
+
children: InlineNode[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ParagraphNode = {
|
|
14
|
+
type: 'paragraph'
|
|
15
|
+
children: InlineNode[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CodeBlockNode = {
|
|
19
|
+
type: 'codeBlock'
|
|
20
|
+
language?: string
|
|
21
|
+
content: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type BlockquoteNode = {
|
|
25
|
+
type: 'blockquote'
|
|
26
|
+
children: MarkdownNode[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ListNode = {
|
|
30
|
+
type: 'list'
|
|
31
|
+
ordered: boolean
|
|
32
|
+
items: ListItemNode[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type HorizontalRuleNode = {
|
|
36
|
+
type: 'horizontalRule'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ListItemNode = {
|
|
40
|
+
children: InlineNode[]
|
|
41
|
+
checkbox?: 'checked' | 'unchecked'
|
|
42
|
+
sourceLineIndex: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type InlineNode = TextNode | BoldNode | ItalicNode | InlineCodeNode | LinkNode | ImageNode
|
|
46
|
+
|
|
47
|
+
export type TextNode = { type: 'text'; content: string }
|
|
48
|
+
export type BoldNode = { type: 'bold'; children: InlineNode[] }
|
|
49
|
+
export type ItalicNode = { type: 'italic'; children: InlineNode[] }
|
|
50
|
+
export type InlineCodeNode = { type: 'code'; content: string }
|
|
51
|
+
export type LinkNode = { type: 'link'; href: string; children: InlineNode[] }
|
|
52
|
+
export type ImageNode = { type: 'image'; src: string; alt: string }
|
|
53
|
+
|
|
54
|
+
const HORIZONTAL_RULE_RE = /^(\*{3,}|-{3,}|_{3,})\s*$/
|
|
55
|
+
const HEADING_RE = /^(#{1,6})\s+(.*)/
|
|
56
|
+
const FENCED_CODE_OPEN_RE = /^```(\w*)\s*$/
|
|
57
|
+
const FENCED_CODE_CLOSE_RE = /^```\s*$/
|
|
58
|
+
const UNORDERED_LIST_RE = /^(\s*)[-*]\s+(.*)/
|
|
59
|
+
const ORDERED_LIST_RE = /^(\s*)\d+\.\s+(.*)/
|
|
60
|
+
const BLOCKQUOTE_RE = /^>\s?(.*)/
|
|
61
|
+
const CHECKBOX_UNCHECKED_RE = /^\[[ ]\]\s+(.*)/
|
|
62
|
+
const CHECKBOX_CHECKED_RE = /^\[[xX]\]\s+(.*)/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse inline Markdown formatting into an array of InlineNodes.
|
|
66
|
+
*
|
|
67
|
+
* Known limitations:
|
|
68
|
+
* - Backslash escapes (e.g. `\*`) are not supported; special characters are always interpreted.
|
|
69
|
+
* - Underscore `_` markers are not restricted to word boundaries, so identifiers like
|
|
70
|
+
* `some_variable_name` may produce false italic/bold matches.
|
|
71
|
+
*/
|
|
72
|
+
export const parseInline = (text: string): InlineNode[] => {
|
|
73
|
+
const nodes: InlineNode[] = []
|
|
74
|
+
let pos = 0
|
|
75
|
+
|
|
76
|
+
while (pos < text.length) {
|
|
77
|
+
// Inline code
|
|
78
|
+
if (text[pos] === '`') {
|
|
79
|
+
const closeIdx = text.indexOf('`', pos + 1)
|
|
80
|
+
if (closeIdx !== -1) {
|
|
81
|
+
nodes.push({ type: 'code', content: text.slice(pos + 1, closeIdx) })
|
|
82
|
+
pos = closeIdx + 1
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Image 
|
|
88
|
+
if (text[pos] === '!' && text[pos + 1] === '[') {
|
|
89
|
+
const altClose = text.indexOf(']', pos + 2)
|
|
90
|
+
if (altClose !== -1 && text[altClose + 1] === '(') {
|
|
91
|
+
const srcClose = text.indexOf(')', altClose + 2)
|
|
92
|
+
if (srcClose !== -1) {
|
|
93
|
+
const alt = text.slice(pos + 2, altClose)
|
|
94
|
+
const src = text.slice(altClose + 2, srcClose)
|
|
95
|
+
nodes.push({ type: 'image', src, alt })
|
|
96
|
+
pos = srcClose + 1
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Link [text](href)
|
|
103
|
+
if (text[pos] === '[') {
|
|
104
|
+
const textClose = text.indexOf(']', pos + 1)
|
|
105
|
+
if (textClose !== -1 && text[textClose + 1] === '(') {
|
|
106
|
+
const hrefClose = text.indexOf(')', textClose + 2)
|
|
107
|
+
if (hrefClose !== -1) {
|
|
108
|
+
const linkText = text.slice(pos + 1, textClose)
|
|
109
|
+
const href = text.slice(textClose + 2, hrefClose)
|
|
110
|
+
nodes.push({ type: 'link', href, children: parseInline(linkText) })
|
|
111
|
+
pos = hrefClose + 1
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Bold+Italic (***text***) or Bold (**text**) or Italic (*text*)
|
|
118
|
+
if (text[pos] === '*' || text[pos] === '_') {
|
|
119
|
+
const marker = text[pos]
|
|
120
|
+
|
|
121
|
+
// Count consecutive markers
|
|
122
|
+
let markerCount = 0
|
|
123
|
+
while (pos + markerCount < text.length && text[pos + markerCount] === marker) {
|
|
124
|
+
markerCount++
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (markerCount >= 3) {
|
|
128
|
+
const closeIdx = text.indexOf(marker.repeat(3), pos + 3)
|
|
129
|
+
if (closeIdx !== -1) {
|
|
130
|
+
const inner = text.slice(pos + 3, closeIdx)
|
|
131
|
+
nodes.push({ type: 'bold', children: [{ type: 'italic', children: parseInline(inner) }] })
|
|
132
|
+
pos = closeIdx + 3
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (markerCount >= 2) {
|
|
138
|
+
const closeIdx = text.indexOf(marker.repeat(2), pos + 2)
|
|
139
|
+
if (closeIdx !== -1) {
|
|
140
|
+
const inner = text.slice(pos + 2, closeIdx)
|
|
141
|
+
nodes.push({ type: 'bold', children: parseInline(inner) })
|
|
142
|
+
pos = closeIdx + 2
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (markerCount >= 1) {
|
|
148
|
+
const closeIdx = text.indexOf(marker, pos + 1)
|
|
149
|
+
if (closeIdx !== -1) {
|
|
150
|
+
const inner = text.slice(pos + 1, closeIdx)
|
|
151
|
+
nodes.push({ type: 'italic', children: parseInline(inner) })
|
|
152
|
+
pos = closeIdx + 1
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Plain text — consume until the next special character
|
|
159
|
+
let end = pos + 1
|
|
160
|
+
while (end < text.length && !['`', '!', '[', '*', '_'].includes(text[end])) {
|
|
161
|
+
end++
|
|
162
|
+
}
|
|
163
|
+
const content = text.slice(pos, end)
|
|
164
|
+
const lastNode = nodes[nodes.length - 1]
|
|
165
|
+
if (lastNode?.type === 'text') {
|
|
166
|
+
lastNode.content += content
|
|
167
|
+
} else {
|
|
168
|
+
nodes.push({ type: 'text', content })
|
|
169
|
+
}
|
|
170
|
+
pos = end
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return nodes
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse a Markdown string into an array of block-level MarkdownNodes.
|
|
178
|
+
*/
|
|
179
|
+
export const parseMarkdown = (source: string): MarkdownNode[] => {
|
|
180
|
+
const lines = source.split('\n')
|
|
181
|
+
const nodes: MarkdownNode[] = []
|
|
182
|
+
let i = 0
|
|
183
|
+
|
|
184
|
+
while (i < lines.length) {
|
|
185
|
+
const line = lines[i]
|
|
186
|
+
|
|
187
|
+
// Blank lines — skip
|
|
188
|
+
if (line.trim() === '') {
|
|
189
|
+
i++
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fenced code block
|
|
194
|
+
const codeMatch = FENCED_CODE_OPEN_RE.exec(line)
|
|
195
|
+
if (codeMatch) {
|
|
196
|
+
const language = codeMatch[1] || undefined
|
|
197
|
+
const codeLines: string[] = []
|
|
198
|
+
i++
|
|
199
|
+
while (i < lines.length && !FENCED_CODE_CLOSE_RE.test(lines[i])) {
|
|
200
|
+
codeLines.push(lines[i])
|
|
201
|
+
i++
|
|
202
|
+
}
|
|
203
|
+
nodes.push({ type: 'codeBlock', language, content: codeLines.join('\n') })
|
|
204
|
+
i++ // skip closing ```
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Horizontal rule
|
|
209
|
+
if (HORIZONTAL_RULE_RE.test(line)) {
|
|
210
|
+
nodes.push({ type: 'horizontalRule' })
|
|
211
|
+
i++
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Heading
|
|
216
|
+
const headingMatch = HEADING_RE.exec(line)
|
|
217
|
+
if (headingMatch) {
|
|
218
|
+
const level = headingMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6
|
|
219
|
+
nodes.push({ type: 'heading', level, children: parseInline(headingMatch[2]) })
|
|
220
|
+
i++
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Blockquote
|
|
225
|
+
const bqMatch = BLOCKQUOTE_RE.exec(line)
|
|
226
|
+
if (bqMatch) {
|
|
227
|
+
const bqLines: string[] = []
|
|
228
|
+
while (i < lines.length) {
|
|
229
|
+
const bqLineMatch = BLOCKQUOTE_RE.exec(lines[i])
|
|
230
|
+
if (bqLineMatch) {
|
|
231
|
+
bqLines.push(bqLineMatch[1])
|
|
232
|
+
i++
|
|
233
|
+
} else {
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
nodes.push({ type: 'blockquote', children: parseMarkdown(bqLines.join('\n')) })
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Unordered list
|
|
242
|
+
const ulMatch = UNORDERED_LIST_RE.exec(line)
|
|
243
|
+
if (ulMatch) {
|
|
244
|
+
const items: ListItemNode[] = []
|
|
245
|
+
while (i < lines.length) {
|
|
246
|
+
const itemMatch = UNORDERED_LIST_RE.exec(lines[i])
|
|
247
|
+
if (!itemMatch) break
|
|
248
|
+
const itemText = itemMatch[2]
|
|
249
|
+
const checkedMatch = CHECKBOX_CHECKED_RE.exec(itemText)
|
|
250
|
+
const uncheckedMatch = CHECKBOX_UNCHECKED_RE.exec(itemText)
|
|
251
|
+
if (checkedMatch) {
|
|
252
|
+
items.push({
|
|
253
|
+
children: parseInline(checkedMatch[1]),
|
|
254
|
+
checkbox: 'checked',
|
|
255
|
+
sourceLineIndex: i,
|
|
256
|
+
})
|
|
257
|
+
} else if (uncheckedMatch) {
|
|
258
|
+
items.push({
|
|
259
|
+
children: parseInline(uncheckedMatch[1]),
|
|
260
|
+
checkbox: 'unchecked',
|
|
261
|
+
sourceLineIndex: i,
|
|
262
|
+
})
|
|
263
|
+
} else {
|
|
264
|
+
items.push({
|
|
265
|
+
children: parseInline(itemText),
|
|
266
|
+
sourceLineIndex: i,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
i++
|
|
270
|
+
}
|
|
271
|
+
nodes.push({ type: 'list', ordered: false, items })
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Ordered list
|
|
276
|
+
const olMatch = ORDERED_LIST_RE.exec(line)
|
|
277
|
+
if (olMatch) {
|
|
278
|
+
const items: ListItemNode[] = []
|
|
279
|
+
while (i < lines.length) {
|
|
280
|
+
const itemMatch = ORDERED_LIST_RE.exec(lines[i])
|
|
281
|
+
if (!itemMatch) break
|
|
282
|
+
items.push({
|
|
283
|
+
children: parseInline(itemMatch[2]),
|
|
284
|
+
sourceLineIndex: i,
|
|
285
|
+
})
|
|
286
|
+
i++
|
|
287
|
+
}
|
|
288
|
+
nodes.push({ type: 'list', ordered: true, items })
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Paragraph — collect consecutive non-blank, non-block-start lines
|
|
293
|
+
const paraLines: string[] = []
|
|
294
|
+
while (i < lines.length) {
|
|
295
|
+
const pLine = lines[i]
|
|
296
|
+
if (pLine.trim() === '') break
|
|
297
|
+
if (HEADING_RE.test(pLine)) break
|
|
298
|
+
if (FENCED_CODE_OPEN_RE.test(pLine)) break
|
|
299
|
+
if (HORIZONTAL_RULE_RE.test(pLine)) break
|
|
300
|
+
if (BLOCKQUOTE_RE.test(pLine)) break
|
|
301
|
+
if (UNORDERED_LIST_RE.test(pLine)) break
|
|
302
|
+
if (ORDERED_LIST_RE.test(pLine)) break
|
|
303
|
+
paraLines.push(pLine)
|
|
304
|
+
i++
|
|
305
|
+
}
|
|
306
|
+
if (paraLines.length > 0) {
|
|
307
|
+
nodes.push({ type: 'paragraph', children: parseInline(paraLines.join(' ')) })
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return nodes
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const TOGGLE_UNCHECKED_RE = /^(\s*[-*]\s+)\[ \]/
|
|
315
|
+
const TOGGLE_CHECKED_RE = /^(\s*[-*]\s+)\[[xX]\]/
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Toggle a checkbox at the given source line index in the raw Markdown string.
|
|
319
|
+
* Only matches checkboxes in unordered list items (`- [ ]` or `* [x]`).
|
|
320
|
+
* Returns the updated string.
|
|
321
|
+
*/
|
|
322
|
+
export const toggleCheckbox = (source: string, sourceLineIndex: number): string => {
|
|
323
|
+
const lines = source.split('\n')
|
|
324
|
+
if (sourceLineIndex < 0 || sourceLineIndex >= lines.length) return source
|
|
325
|
+
|
|
326
|
+
const line = lines[sourceLineIndex]
|
|
327
|
+
if (TOGGLE_UNCHECKED_RE.test(line)) {
|
|
328
|
+
lines[sourceLineIndex] = line.replace(TOGGLE_UNCHECKED_RE, '$1[x]')
|
|
329
|
+
} else if (TOGGLE_CHECKED_RE.test(line)) {
|
|
330
|
+
lines[sourceLineIndex] = line.replace(TOGGLE_CHECKED_RE, '$1[ ]')
|
|
331
|
+
}
|
|
332
|
+
return lines.join('\n')
|
|
333
|
+
}
|
|
@@ -30,7 +30,7 @@ export type PageContainerProps = {
|
|
|
30
30
|
* ```tsx
|
|
31
31
|
* <PageContainer maxWidth="800px" centered padding="48px" gap="24px">
|
|
32
32
|
* <PageHeader
|
|
33
|
-
* icon=
|
|
33
|
+
* icon={<Icon icon={icons.users} />}
|
|
34
34
|
* title="Users"
|
|
35
35
|
* description="Manage user accounts and their roles."
|
|
36
36
|
* />
|
|
@@ -6,8 +6,8 @@ import { Paper } from '../paper.js'
|
|
|
6
6
|
* Props for the PageHeader component.
|
|
7
7
|
*/
|
|
8
8
|
export type PageHeaderProps = {
|
|
9
|
-
/** Optional icon (
|
|
10
|
-
icon?: string
|
|
9
|
+
/** Optional icon displayed before the title (string or JSX element such as an Icon component) */
|
|
10
|
+
icon?: JSX.Element | string
|
|
11
11
|
/** The page title */
|
|
12
12
|
title: string
|
|
13
13
|
/** Optional description text displayed below the title */
|
|
@@ -27,9 +27,9 @@ export type PageHeaderProps = {
|
|
|
27
27
|
*
|
|
28
28
|
* @example
|
|
29
29
|
* ```tsx
|
|
30
|
-
* //
|
|
30
|
+
* // Header with an Icon component
|
|
31
31
|
* <PageHeader
|
|
32
|
-
* icon=
|
|
32
|
+
* icon={<Icon icon={icons.users} />}
|
|
33
33
|
* title="Users"
|
|
34
34
|
* description="Manage user accounts and their roles."
|
|
35
35
|
* />
|
|
@@ -39,7 +39,7 @@ export type PageHeaderProps = {
|
|
|
39
39
|
* ```tsx
|
|
40
40
|
* // Header with action buttons
|
|
41
41
|
* <PageHeader
|
|
42
|
-
* icon=
|
|
42
|
+
* icon={<Icon icon={icons.folder} />}
|
|
43
43
|
* title="Projects"
|
|
44
44
|
* description="View and manage your projects."
|
|
45
45
|
* actions={
|
|
@@ -16,7 +16,7 @@ export * from './suggestion-list.js'
|
|
|
16
16
|
export * from './suggestion-result.js'
|
|
17
17
|
|
|
18
18
|
export interface SuggestProps<T> {
|
|
19
|
-
defaultPrefix: string
|
|
19
|
+
defaultPrefix: JSX.Element | string
|
|
20
20
|
getEntries: (term: string) => Promise<T[]>
|
|
21
21
|
getSuggestionEntry: (entry: T) => SuggestionResult
|
|
22
22
|
onSelectSuggestion: (entry: T) => void
|