slidev-theme-practicum 0.1.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/README.md +270 -0
- package/components/DebugGrid.vue +50 -0
- package/components/DecorLibrary.vue +1117 -0
- package/components/Header.vue +124 -0
- package/components/Image.vue +82 -0
- package/components/ImageRenderer.vue +126 -0
- package/components/Logo.vue +142 -0
- package/components/Person.vue +89 -0
- package/components/Slide.vue +324 -0
- package/components/Slot.vue +419 -0
- package/components/Text.vue +216 -0
- package/components/Timeline.vue +202 -0
- package/composables/decor-catalog.mjs +339 -0
- package/composables/decor-config.mjs +488 -0
- package/composables/decor-file-store.ts +192 -0
- package/composables/decor-http-store.ts +25 -0
- package/composables/decor-store.ts +39 -0
- package/composables/decor-tuning-overrides.mjs +2 -0
- package/composables/decor-workbench.ts +799 -0
- package/composables/grid-placement.ts +396 -0
- package/composables/image-config.mjs +484 -0
- package/composables/layout-authoring.ts +372 -0
- package/composables/layout-markdown.mjs +56 -0
- package/composables/layout-recipes.ts +347 -0
- package/composables/layout-registry.ts +47 -0
- package/composables/layout-shorthands.ts +891 -0
- package/composables/layout-vnode.ts +43 -0
- package/composables/logo-mark.mjs +5 -0
- package/composables/slide-layout.ts +43 -0
- package/composables/slot-placement.ts +195 -0
- package/composables/slot-rules.ts +147 -0
- package/composables/text-fit-runtime.ts +577 -0
- package/composables/text-priority-fit.mjs +166 -0
- package/composables/text-typography.mjs +138 -0
- package/composables/theme-foundation.ts +192 -0
- package/composables/theme-media.mjs +202 -0
- package/composables/use-theme-config.ts +71 -0
- package/decor-library.md +10 -0
- package/env.d.ts +6 -0
- package/example.md +786 -0
- package/layouts/collection.vue +5 -0
- package/layouts/cover.vue +5 -0
- package/layouts/default.vue +5 -0
- package/layouts/explainer.vue +5 -0
- package/layouts/message.vue +5 -0
- package/layouts/none.vue +5 -0
- package/layouts/section.vue +5 -0
- package/layouts/two-cols.vue +11 -0
- package/package.json +84 -0
- package/public/decor/decor-1.svg +83 -0
- package/public/decor/decor-10.svg +40 -0
- package/public/decor/decor-11.svg +162 -0
- package/public/decor/decor-12.svg +83 -0
- package/public/decor/decor-13.svg +5 -0
- package/public/decor/decor-14.svg +7 -0
- package/public/decor/decor-15.svg +10 -0
- package/public/decor/decor-16.svg +9 -0
- package/public/decor/decor-17.png +0 -0
- package/public/decor/decor-18.png +0 -0
- package/public/decor/decor-19.svg +5 -0
- package/public/decor/decor-2.png +0 -0
- package/public/decor/decor-3.svg +9 -0
- package/public/decor/decor-4.svg +31 -0
- package/public/decor/decor-5.png +0 -0
- package/public/decor/decor-6.png +0 -0
- package/public/decor/decor-7.png +0 -0
- package/public/decor/decor-8.svg +21 -0
- package/public/decor/decor-9.svg +38 -0
- package/public/favicon.svg +11 -0
- package/public/photos/photo-1.png +0 -0
- package/public/photos/photo-10.png +0 -0
- package/public/photos/photo-2.png +0 -0
- package/public/photos/photo-3.png +0 -0
- package/public/photos/photo-4.png +0 -0
- package/public/photos/photo-5.png +0 -0
- package/public/photos/photo-6.png +0 -0
- package/public/photos/photo-7.png +0 -0
- package/public/photos/photo-8.png +0 -0
- package/public/photos/photo-9.png +0 -0
- package/setup/main.ts +3 -0
- package/setup/shiki.ts +10 -0
- package/setup/vite-plugins.ts +106 -0
- package/slidev-theme-practicum.png +0 -0
- package/slidev-theme-practicum.svg +102 -0
- package/styles/example.css +0 -0
- package/styles/index.css +192 -0
- package/styles/vars.css +119 -0
- package/types/slidev-client.d.ts +15 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { Fragment, Text as NativeText, isVNode, type VNode } from 'vue'
|
|
2
|
+
import { resolveAgendaDecor } from './decor-config.mjs'
|
|
3
|
+
import {
|
|
4
|
+
buildLayoutShorthandChildren,
|
|
5
|
+
hasLayoutShorthand,
|
|
6
|
+
shouldUseExplicitRoleChildren,
|
|
7
|
+
type LayoutShorthandComponents,
|
|
8
|
+
type LayoutShorthandContext,
|
|
9
|
+
} from './layout-shorthands'
|
|
10
|
+
import {
|
|
11
|
+
resolveLayoutSpec,
|
|
12
|
+
resolveLayoutVariant,
|
|
13
|
+
type ThemeLayout,
|
|
14
|
+
type ThemeLayoutHeader,
|
|
15
|
+
type ThemeLayoutRole,
|
|
16
|
+
type ThemeLayoutSlotSpec,
|
|
17
|
+
} from './layout-registry'
|
|
18
|
+
import { resolveLayoutRecipe, resolveLayoutSlotProps, THEME_LAYOUT_ROLES, type ThemeLayoutRecipe, type ThemeLayoutSpec } from './layout-recipes'
|
|
19
|
+
import { isBlankTextVNode, isCommentVNode } from './layout-vnode'
|
|
20
|
+
import { createSlotRules, type SlotRules } from './slot-rules'
|
|
21
|
+
import {
|
|
22
|
+
isContrastSlideMode,
|
|
23
|
+
resolveSlideMode,
|
|
24
|
+
resolveThemeMode,
|
|
25
|
+
type ResolvedThemeMode,
|
|
26
|
+
type ThemeSlideMode,
|
|
27
|
+
type ThemeTone,
|
|
28
|
+
} from './theme-foundation'
|
|
29
|
+
|
|
30
|
+
export type SlideLayoutProps = {
|
|
31
|
+
layout?: ThemeLayout | ''
|
|
32
|
+
variant?: string
|
|
33
|
+
arrangement?: string
|
|
34
|
+
mode?: ThemeSlideMode
|
|
35
|
+
tone?: ThemeTone | ''
|
|
36
|
+
header?: ThemeLayoutHeader
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type LayoutAuthoringInput = {
|
|
40
|
+
props: SlideLayoutProps
|
|
41
|
+
frontmatter: Record<string, unknown>
|
|
42
|
+
children?: readonly VNode[]
|
|
43
|
+
components: LayoutShorthandComponents
|
|
44
|
+
defaultTone: ThemeTone
|
|
45
|
+
debugGrid?: boolean
|
|
46
|
+
slotPlanState?: LayoutSlotPlanState
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type LayoutSlotPlanState = Map<string, { role: ThemeLayoutRole, index: number }>
|
|
50
|
+
|
|
51
|
+
export type ResolvedLayoutSlot = ThemeLayoutSlotSpec & {
|
|
52
|
+
role: ThemeLayoutRole
|
|
53
|
+
index: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type LayoutSlotPlan = {
|
|
57
|
+
recipe: ThemeLayoutRecipe
|
|
58
|
+
resolveRoleSlot(input: {
|
|
59
|
+
id: string
|
|
60
|
+
role: ThemeLayoutRole
|
|
61
|
+
overrides: Partial<ThemeLayoutSlotSpec>
|
|
62
|
+
label: string
|
|
63
|
+
}): ResolvedLayoutSlot
|
|
64
|
+
unregisterRoleSlot(id: string): void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type CompiledLayoutAuthoring = {
|
|
68
|
+
mode: 'manual' | 'layout'
|
|
69
|
+
layout: ThemeLayout | ''
|
|
70
|
+
variant: string
|
|
71
|
+
header: Exclude<ThemeLayoutHeader, 'auto'>
|
|
72
|
+
slideMode: ThemeSlideMode
|
|
73
|
+
theme: ResolvedThemeMode
|
|
74
|
+
contrast: boolean
|
|
75
|
+
showDebugGrid: boolean
|
|
76
|
+
children: VNode[]
|
|
77
|
+
slotPlan: LayoutSlotPlan | null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const THEME_LAYOUTS = new Set<ThemeLayout>(['cover', 'message', 'explainer', 'collection'])
|
|
81
|
+
|
|
82
|
+
export function createLayoutSlotPlanState(): LayoutSlotPlanState {
|
|
83
|
+
return new Map()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readFrontmatterValue(frontmatter: Record<string, unknown>, key: string) {
|
|
87
|
+
return frontmatter[key]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readFrontmatterString(frontmatter: Record<string, unknown>, key: string) {
|
|
91
|
+
const value = readFrontmatterValue(frontmatter, key)
|
|
92
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readFrontmatterText(frontmatter: Record<string, unknown>, key: string) {
|
|
96
|
+
const value = readFrontmatterValue(frontmatter, key)
|
|
97
|
+
|
|
98
|
+
if (typeof value === 'string')
|
|
99
|
+
return value.trim()
|
|
100
|
+
|
|
101
|
+
if (typeof value === 'number')
|
|
102
|
+
return String(value)
|
|
103
|
+
|
|
104
|
+
return ''
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readFrontmatterBoolean(frontmatter: Record<string, unknown>, key: string) {
|
|
108
|
+
const value = readFrontmatterValue(frontmatter, key)
|
|
109
|
+
if (typeof value === 'boolean')
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readFrontmatterArray(frontmatter: Record<string, unknown>, key: string) {
|
|
116
|
+
const value = readFrontmatterValue(frontmatter, key)
|
|
117
|
+
return Array.isArray(value) ? value : []
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readFrontmatterObject(frontmatter: Record<string, unknown>, key: string) {
|
|
121
|
+
const value = readFrontmatterValue(frontmatter, key)
|
|
122
|
+
|
|
123
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value))
|
|
124
|
+
return value as Record<string, unknown>
|
|
125
|
+
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readHeader(value: unknown): ThemeLayoutHeader | undefined {
|
|
130
|
+
if (typeof value !== 'string')
|
|
131
|
+
return undefined
|
|
132
|
+
|
|
133
|
+
const normalized = value.trim()
|
|
134
|
+
if (normalized === 'auto' || normalized === 'default' || normalized === 'cover' || normalized === 'none')
|
|
135
|
+
return normalized
|
|
136
|
+
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isThemeLayout(value: string): value is ThemeLayout {
|
|
141
|
+
return THEME_LAYOUTS.has(value as ThemeLayout)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function failMarkdownContract(label: string, message: string): never {
|
|
145
|
+
throw new Error(`[Slide] ${label} ${message}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function flattenVNodes(children: readonly VNode[], preserveText = false): VNode[] {
|
|
149
|
+
return children.flatMap((child) => {
|
|
150
|
+
if (!isVNode(child))
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
if (isCommentVNode(child))
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
if (child.type === NativeText)
|
|
157
|
+
return preserveText ? [child] : []
|
|
158
|
+
|
|
159
|
+
if (child.type === Fragment && Array.isArray(child.children))
|
|
160
|
+
return flattenVNodes(child.children.filter(isVNode), preserveText)
|
|
161
|
+
|
|
162
|
+
return [child]
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildShorthandContext(input: {
|
|
167
|
+
frontmatter: Record<string, unknown>
|
|
168
|
+
variant: string
|
|
169
|
+
frontmatterDecor: Record<string, unknown> | null
|
|
170
|
+
agendaDecor: Record<string, unknown> | null
|
|
171
|
+
components: LayoutShorthandComponents
|
|
172
|
+
}): LayoutShorthandContext {
|
|
173
|
+
return {
|
|
174
|
+
variant: input.variant,
|
|
175
|
+
readFrontmatterValue: key => readFrontmatterValue(input.frontmatter, key),
|
|
176
|
+
readFrontmatterText: key => readFrontmatterText(input.frontmatter, key),
|
|
177
|
+
readFrontmatterArray: key => readFrontmatterArray(input.frontmatter, key),
|
|
178
|
+
readFrontmatterObject: key => readFrontmatterObject(input.frontmatter, key),
|
|
179
|
+
frontmatterDecor: input.frontmatterDecor,
|
|
180
|
+
agendaDecor: input.agendaDecor,
|
|
181
|
+
components: input.components,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildSourceChildren(input: {
|
|
186
|
+
rawChildren: readonly VNode[]
|
|
187
|
+
layout: ThemeLayout
|
|
188
|
+
variant: string
|
|
189
|
+
layoutSpec: ThemeLayoutSpec | null
|
|
190
|
+
context: LayoutShorthandContext
|
|
191
|
+
slotRules: SlotRules
|
|
192
|
+
}) {
|
|
193
|
+
const shorthandKey = `${input.layout}:${input.variant}`
|
|
194
|
+
const preserveText = hasLayoutShorthand(shorthandKey)
|
|
195
|
+
const children = flattenVNodes(input.rawChildren, preserveText)
|
|
196
|
+
const readRole = (node: VNode) => input.slotRules.read(node)?.role ?? ''
|
|
197
|
+
|
|
198
|
+
if (shouldUseExplicitRoleChildren(children, shorthandKey, readRole)) {
|
|
199
|
+
input.slotRules.assertChildren(children, {
|
|
200
|
+
label: `${input.layout}:${input.variant}`,
|
|
201
|
+
})
|
|
202
|
+
return children.filter(node => !isBlankTextVNode(node))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const shorthandChildren = buildLayoutShorthandChildren(shorthandKey, children, input.context)
|
|
206
|
+
if (shorthandChildren)
|
|
207
|
+
return shorthandChildren
|
|
208
|
+
|
|
209
|
+
if (input.layoutSpec && children.some(node => !isBlankTextVNode(node))) {
|
|
210
|
+
failMarkdownContract(
|
|
211
|
+
`${input.layout}:${input.variant}`,
|
|
212
|
+
`ожидает поддерживаемую markdown shorthand-структуру или Slot role="${THEME_LAYOUT_ROLES.join('|')}".`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return children
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function validateLayoutChild(input: {
|
|
220
|
+
node: VNode
|
|
221
|
+
layout: ThemeLayout
|
|
222
|
+
variant: string
|
|
223
|
+
slotRules: SlotRules
|
|
224
|
+
}) {
|
|
225
|
+
return input.slotRules.assertLayoutChild(input.node, {
|
|
226
|
+
label: `${input.layout}:${input.variant}`,
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveLayoutRoleIndex(state: LayoutSlotPlanState, id: string, role: ThemeLayoutRole) {
|
|
231
|
+
const current = state.get(id)
|
|
232
|
+
if (current?.role === role)
|
|
233
|
+
return current.index
|
|
234
|
+
|
|
235
|
+
const occupied = new Set(
|
|
236
|
+
[...state.entries()]
|
|
237
|
+
.filter(([entryId, entry]) => entryId !== id && entry.role === role)
|
|
238
|
+
.map(([, entry]) => entry.index),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
let index = 0
|
|
242
|
+
while (occupied.has(index))
|
|
243
|
+
index += 1
|
|
244
|
+
|
|
245
|
+
state.set(id, { role, index })
|
|
246
|
+
return index
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function createLayoutSlotPlan(input: {
|
|
250
|
+
recipe: ThemeLayoutRecipe
|
|
251
|
+
themeTone: ThemeTone
|
|
252
|
+
state: LayoutSlotPlanState
|
|
253
|
+
}): LayoutSlotPlan {
|
|
254
|
+
return {
|
|
255
|
+
recipe: input.recipe,
|
|
256
|
+
resolveRoleSlot({ id, role, overrides }) {
|
|
257
|
+
const index = resolveLayoutRoleIndex(input.state, id, role)
|
|
258
|
+
const slot = resolveLayoutSlotProps({
|
|
259
|
+
recipe: input.recipe,
|
|
260
|
+
role,
|
|
261
|
+
index,
|
|
262
|
+
overrides,
|
|
263
|
+
themeTone: input.themeTone,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
return { ...slot, role, index }
|
|
267
|
+
},
|
|
268
|
+
unregisterRoleSlot(id) {
|
|
269
|
+
input.state.delete(id)
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolveHeader(input: {
|
|
275
|
+
props: SlideLayoutProps
|
|
276
|
+
frontmatter: Record<string, unknown>
|
|
277
|
+
active: boolean
|
|
278
|
+
layout: ThemeLayout | ''
|
|
279
|
+
variant: string
|
|
280
|
+
}) {
|
|
281
|
+
const requestedHeader = input.props.header
|
|
282
|
+
?? readHeader(readFrontmatterString(input.frontmatter, 'header'))
|
|
283
|
+
?? 'auto'
|
|
284
|
+
|
|
285
|
+
if (requestedHeader !== 'auto')
|
|
286
|
+
return requestedHeader
|
|
287
|
+
|
|
288
|
+
if (!input.active)
|
|
289
|
+
return 'none'
|
|
290
|
+
|
|
291
|
+
if (input.layout === 'message' && input.variant === 'closing') {
|
|
292
|
+
const closingLogo = readFrontmatterString(input.frontmatter, 'logo') || 'full'
|
|
293
|
+
return closingLogo === 'none' ? 'none' : 'cover'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return input.layout === 'cover' ? 'cover' : 'default'
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function compileLayoutAuthoring(input: LayoutAuthoringInput): CompiledLayoutAuthoring {
|
|
300
|
+
const props = input.props
|
|
301
|
+
const frontmatter = input.frontmatter
|
|
302
|
+
const explicitLayout = props.layout || readFrontmatterString(frontmatter, 'layout')
|
|
303
|
+
const layout = isThemeLayout(explicitLayout) ? explicitLayout : ''
|
|
304
|
+
const active = Boolean(layout)
|
|
305
|
+
const slideMode = resolveSlideMode(props.mode ?? readFrontmatterString(frontmatter, 'mode'), 'Slide.mode', 'light')
|
|
306
|
+
const variant = resolveLayoutVariant(
|
|
307
|
+
layout,
|
|
308
|
+
props.variant || readFrontmatterString(frontmatter, 'variant'),
|
|
309
|
+
props.arrangement || readFrontmatterString(frontmatter, 'arrangement'),
|
|
310
|
+
)
|
|
311
|
+
const layoutSpec = active ? resolveLayoutSpec(layout as ThemeLayout, variant) : null
|
|
312
|
+
const recipe = active ? resolveLayoutRecipe(layout as ThemeLayout, variant) : null
|
|
313
|
+
const contrast = isContrastSlideMode(slideMode)
|
|
314
|
+
const frontmatterDecor = readFrontmatterObject(frontmatter, 'decor')
|
|
315
|
+
const agendaDecor = resolveAgendaDecor({
|
|
316
|
+
decor: frontmatterDecor,
|
|
317
|
+
tone: props.tone || readFrontmatterString(frontmatter, 'tone') || input.defaultTone,
|
|
318
|
+
contrast,
|
|
319
|
+
})
|
|
320
|
+
const theme = resolveThemeMode({
|
|
321
|
+
mode: slideMode,
|
|
322
|
+
tone: props.tone || readFrontmatterString(frontmatter, 'tone'),
|
|
323
|
+
defaultTone: input.defaultTone,
|
|
324
|
+
label: 'Slide',
|
|
325
|
+
})
|
|
326
|
+
const context = buildShorthandContext({
|
|
327
|
+
frontmatter,
|
|
328
|
+
variant,
|
|
329
|
+
frontmatterDecor,
|
|
330
|
+
agendaDecor,
|
|
331
|
+
components: input.components,
|
|
332
|
+
})
|
|
333
|
+
const rawChildren = input.children?.filter(isVNode) ?? []
|
|
334
|
+
const shouldCompileChildren = input.children !== undefined
|
|
335
|
+
const slotRules = createSlotRules({ Slot: input.components.Slot })
|
|
336
|
+
const children = active && shouldCompileChildren
|
|
337
|
+
? buildSourceChildren({
|
|
338
|
+
rawChildren,
|
|
339
|
+
layout: layout as ThemeLayout,
|
|
340
|
+
variant,
|
|
341
|
+
layoutSpec,
|
|
342
|
+
context,
|
|
343
|
+
slotRules,
|
|
344
|
+
}).map(node =>
|
|
345
|
+
validateLayoutChild({
|
|
346
|
+
node,
|
|
347
|
+
layout: layout as ThemeLayout,
|
|
348
|
+
variant,
|
|
349
|
+
slotRules,
|
|
350
|
+
}),
|
|
351
|
+
)
|
|
352
|
+
: rawChildren
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
mode: active ? 'layout' : 'manual',
|
|
356
|
+
layout,
|
|
357
|
+
variant,
|
|
358
|
+
header: resolveHeader({ props, frontmatter, active, layout, variant }),
|
|
359
|
+
slideMode,
|
|
360
|
+
theme,
|
|
361
|
+
contrast,
|
|
362
|
+
showDebugGrid: readFrontmatterBoolean(frontmatter, 'debug') ?? Boolean(input.debugGrid),
|
|
363
|
+
children,
|
|
364
|
+
slotPlan: recipe
|
|
365
|
+
? createLayoutSlotPlan({
|
|
366
|
+
recipe,
|
|
367
|
+
themeTone: theme.tone,
|
|
368
|
+
state: input.slotPlanState ?? createLayoutSlotPlanState(),
|
|
369
|
+
})
|
|
370
|
+
: null,
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {unknown} input
|
|
3
|
+
* @returns {{ level: number, text: string } | null}
|
|
4
|
+
*/
|
|
5
|
+
export function extractMarkdownHeading(input) {
|
|
6
|
+
if (typeof input !== 'string')
|
|
7
|
+
return null
|
|
8
|
+
|
|
9
|
+
const lines = input
|
|
10
|
+
.split(/\r?\n/)
|
|
11
|
+
.map(line => line.trim())
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
|
|
14
|
+
if (lines.length !== 1)
|
|
15
|
+
return null
|
|
16
|
+
|
|
17
|
+
const match = lines[0].match(/^(#{1,6})\s+(.+?)\s*$/)
|
|
18
|
+
if (!match)
|
|
19
|
+
return null
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
level: match[1].length,
|
|
23
|
+
text: match[2].trim(),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {unknown} input
|
|
29
|
+
* @returns {string | null}
|
|
30
|
+
*/
|
|
31
|
+
export function extractMarkdownQuote(input) {
|
|
32
|
+
if (typeof input !== 'string')
|
|
33
|
+
return null
|
|
34
|
+
|
|
35
|
+
const trimmed = input.trim()
|
|
36
|
+
if (!trimmed.startsWith('>'))
|
|
37
|
+
return null
|
|
38
|
+
|
|
39
|
+
const lines = trimmed
|
|
40
|
+
.split(/\r?\n/)
|
|
41
|
+
.map(line => line.trim())
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
|
|
44
|
+
if (!lines.length)
|
|
45
|
+
return null
|
|
46
|
+
|
|
47
|
+
const parts = lines.map((line) => {
|
|
48
|
+
const match = line.match(/^>\s?(.*)$/)
|
|
49
|
+
return match ? match[1].trim() : null
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (parts.some(part => part == null))
|
|
53
|
+
return null
|
|
54
|
+
|
|
55
|
+
return parts.join(' ').trim() || null
|
|
56
|
+
}
|