slidev-theme-practicum 0.1.0 → 0.1.2

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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -10
  3. package/components/Slide.vue +81 -26
  4. package/components/Slot.vue +33 -48
  5. package/components/Text.vue +68 -2
  6. package/components/Timeline.vue +2 -23
  7. package/composables/deck-slot-markup.cjs +249 -0
  8. package/composables/decor-catalog.mjs +169 -47
  9. package/composables/layout-authoring.ts +6 -2
  10. package/composables/layout-markdown.mjs +33 -0
  11. package/composables/layout-recipes.ts +51 -5
  12. package/composables/layout-shorthands.ts +251 -81
  13. package/composables/markdown-to-vnodes.cjs +1 -0
  14. package/composables/markdown-to-vnodes.mjs +237 -0
  15. package/composables/slot-placement.ts +1 -1
  16. package/composables/slot-spacing.ts +66 -0
  17. package/composables/text-fit-runtime.ts +458 -76
  18. package/composables/text-fit-scope.ts +13 -0
  19. package/composables/text-slot-markdown.mjs +193 -0
  20. package/composables/text-typography.mjs +2 -0
  21. package/composables/theme-assets.mjs +60 -0
  22. package/composables/timeline-grid.ts +27 -0
  23. package/composables/typescript-require.cjs +29 -0
  24. package/composables/validate-deck-layouts.cjs +277 -0
  25. package/decor-library.md +2 -0
  26. package/example.md +132 -10
  27. package/package.json +25 -11
  28. package/public/photos/photo-1.webp +0 -0
  29. package/public/photos/photo-10.webp +0 -0
  30. package/public/photos/photo-11.webp +0 -0
  31. package/public/photos/photo-12.webp +0 -0
  32. package/public/photos/photo-13.webp +0 -0
  33. package/public/photos/photo-14.webp +0 -0
  34. package/public/photos/photo-15.webp +0 -0
  35. package/public/photos/photo-16.webp +0 -0
  36. package/public/photos/photo-17.webp +0 -0
  37. package/public/photos/photo-18.webp +0 -0
  38. package/public/photos/photo-19.webp +0 -0
  39. package/public/photos/photo-2.webp +0 -0
  40. package/public/photos/photo-20.webp +0 -0
  41. package/public/photos/photo-21.webp +0 -0
  42. package/public/photos/photo-22.webp +0 -0
  43. package/public/photos/photo-23.webp +0 -0
  44. package/public/photos/photo-24.webp +0 -0
  45. package/public/photos/photo-25.webp +0 -0
  46. package/public/photos/photo-26.webp +0 -0
  47. package/public/photos/photo-27.webp +0 -0
  48. package/public/photos/photo-28.webp +0 -0
  49. package/public/photos/photo-29.webp +0 -0
  50. package/public/photos/photo-3.webp +0 -0
  51. package/public/photos/photo-30.webp +0 -0
  52. package/public/photos/photo-4.webp +0 -0
  53. package/public/photos/photo-5.webp +0 -0
  54. package/public/photos/photo-6.webp +0 -0
  55. package/public/photos/photo-7.webp +0 -0
  56. package/public/photos/photo-8.webp +0 -0
  57. package/public/photos/photo-9.webp +0 -0
  58. package/scripts/browser-smoke-runtime.mjs +428 -0
  59. package/scripts/browser-smoke.mjs +424 -0
  60. package/scripts/build-artifact-assets.mjs +55 -0
  61. package/scripts/check-build-artifact.mjs +243 -0
  62. package/scripts/check-package.mjs +132 -0
  63. package/scripts/check-test-architecture.mjs +176 -0
  64. package/scripts/npm-spawn.mjs +31 -0
  65. package/scripts/test-architecture-policy.mjs +245 -0
  66. package/scripts/theme-asset-inventory.mjs +83 -0
  67. package/scripts/validate-deck.cjs +18 -0
  68. package/setup/decor-save-middleware.ts +244 -0
  69. package/setup/vite-plugins.ts +14 -84
  70. package/skills/slidev-practicum/SKILL.md +25 -0
  71. package/skills/slidev-practicum/agents/openai.yaml +4 -0
  72. package/styles/index.css +10 -1
  73. package/composables/layout-registry.ts +0 -47
  74. package/env.d.ts +0 -6
  75. package/public/photos/photo-1.png +0 -0
  76. package/public/photos/photo-10.png +0 -0
  77. package/public/photos/photo-2.png +0 -0
  78. package/public/photos/photo-3.png +0 -0
  79. package/public/photos/photo-4.png +0 -0
  80. package/public/photos/photo-5.png +0 -0
  81. package/public/photos/photo-6.png +0 -0
  82. package/public/photos/photo-7.png +0 -0
  83. package/public/photos/photo-8.png +0 -0
  84. package/public/photos/photo-9.png +0 -0
  85. package/slidev-theme-practicum.png +0 -0
  86. package/slidev-theme-practicum.svg +0 -102
@@ -0,0 +1,193 @@
1
+ import { Comment, Fragment, Text as NativeText, isVNode } from 'vue'
2
+ import { dedentMarkdownSource } from './layout-markdown.mjs'
3
+ import { markdownToVnodes } from './markdown-to-vnodes.mjs'
4
+
5
+ const PASS_THROUGH_BLOCK_TAGS = new Set([
6
+ 'ul',
7
+ 'ol',
8
+ 'blockquote',
9
+ 'h1',
10
+ 'h2',
11
+ 'h3',
12
+ 'h4',
13
+ 'h5',
14
+ 'h6',
15
+ 'table',
16
+ ])
17
+
18
+ const MARKDOWN_INLINE_PATTERN = /\*\*|__|`|!\[|\[[^\]]+\]\(/
19
+
20
+ /**
21
+ * @param {import('vue').VNode | null | undefined} node
22
+ * @param {string} [separator]
23
+ * @returns {string}
24
+ */
25
+ function readVnodePlainText(node, separator = '') {
26
+ if (!node)
27
+ return ''
28
+
29
+ if (node.type === NativeText)
30
+ return String(node.children ?? '')
31
+
32
+ if (typeof node.children === 'string')
33
+ return node.children
34
+
35
+ if (Array.isArray(node.children)) {
36
+ return node.children
37
+ .map(child => isVNode(child) ? readVnodePlainText(child, separator) : String(child ?? ''))
38
+ .join(separator)
39
+ }
40
+
41
+ return ''
42
+ }
43
+
44
+ /** @param {import('vue').VNode | null | undefined} node */
45
+ function readPreLikePlainText(node) {
46
+ return readVnodePlainText(node, '\n')
47
+ }
48
+
49
+ /** @param {import('vue').VNode | null | undefined} node */
50
+ function isBlankTextNode(node) {
51
+ return node?.type === NativeText
52
+ && typeof node.children === 'string'
53
+ && !node.children.trim()
54
+ }
55
+
56
+ /** @param {string} source */
57
+ function countOrderedListMarkers(source) {
58
+ return source.match(/\d+\.\s+/g)?.length ?? 0
59
+ }
60
+
61
+ /**
62
+ * @param {string} source
63
+ */
64
+ function expandInlineOrderedListLines(source) {
65
+ if (countOrderedListMarkers(source) < 2)
66
+ return source
67
+
68
+ if (source.split(/\r?\n/).filter(line => /^\s*\d+\.\s/.test(line)).length >= 2)
69
+ return source
70
+
71
+ return source.replace(/\s+(?=\d+\.\s+)/g, '\n')
72
+ }
73
+
74
+ /**
75
+ * @param {string} source
76
+ * @returns {string[]}
77
+ */
78
+ function splitContentLines(source) {
79
+ return source.trim().split(/\r?\n/).map(line => line.trim()).filter(Boolean)
80
+ }
81
+
82
+ /**
83
+ * @param {string} source
84
+ */
85
+ function looksLikeMarkdown(source) {
86
+ const trimmed = source.trim()
87
+
88
+ if (!trimmed)
89
+ return false
90
+
91
+ if (MARKDOWN_INLINE_PATTERN.test(trimmed))
92
+ return true
93
+
94
+ const lines = splitContentLines(source)
95
+ const firstLine = lines[0] ?? ''
96
+
97
+ if (/^#{1,6}\s/.test(firstLine) || /^>\s/.test(firstLine))
98
+ return true
99
+
100
+ const bulletLines = lines.filter(line => /^[-*+]\s/.test(line))
101
+ const orderedLines = lines.filter(line => /^\d+\.\s/.test(line))
102
+
103
+ if (orderedLines.length >= 2)
104
+ return true
105
+
106
+ if (countOrderedListMarkers(trimmed) >= 2)
107
+ return true
108
+
109
+ if (bulletLines.length >= 2)
110
+ return true
111
+
112
+ if (bulletLines.length === 1 && lines.length === 1)
113
+ return true
114
+
115
+ if (/\n\s*\n/.test(trimmed) && trimmed.split(/\n\s*\n/).map(block => block.trim()).filter(Boolean).length >= 2)
116
+ return true
117
+
118
+ return false
119
+ }
120
+
121
+ /**
122
+ * @param {import('vue').VNode[]} nodes
123
+ * @returns {string | null}
124
+ */
125
+ function collectMarkdownSource(nodes) {
126
+ const parts = []
127
+
128
+ for (const node of nodes) {
129
+ if (node.type === Comment || isBlankTextNode(node))
130
+ continue
131
+
132
+ if (node.type === NativeText) {
133
+ parts.push(String(node.children ?? ''))
134
+ continue
135
+ }
136
+
137
+ if (node.type === 'pre' || node.type === 'code') {
138
+ const text = readPreLikePlainText(node)
139
+
140
+ if (!text.trim())
141
+ return null
142
+
143
+ parts.push(text)
144
+ continue
145
+ }
146
+
147
+ if (node.type === Fragment && Array.isArray(node.children)) {
148
+ const inner = collectMarkdownSource(node.children.filter(isVNode))
149
+
150
+ if (inner == null)
151
+ return null
152
+
153
+ parts.push(inner)
154
+ continue
155
+ }
156
+
157
+ if (typeof node.type === 'string' && PASS_THROUGH_BLOCK_TAGS.has(node.type))
158
+ return null
159
+
160
+ if (node.type === 'p' || node.type === 'div') {
161
+ const text = readVnodePlainText(node).trim()
162
+
163
+ if (text)
164
+ parts.push(text)
165
+
166
+ continue
167
+ }
168
+
169
+ return null
170
+ }
171
+
172
+ const source = dedentMarkdownSource(expandInlineOrderedListLines(parts.join('\n').replace(/\r\n/g, '\n'))).trim()
173
+
174
+ return source || null
175
+ }
176
+
177
+ /**
178
+ * @param {import('vue').VNode[] | undefined} nodes
179
+ * @returns {import('vue').VNode[] | null}
180
+ */
181
+ export function resolveTextSlotMarkdown(nodes) {
182
+ if (!nodes?.length)
183
+ return null
184
+
185
+ const source = collectMarkdownSource(nodes)
186
+
187
+ if (source == null || !looksLikeMarkdown(source))
188
+ return null
189
+
190
+ const parsed = markdownToVnodes(source)
191
+
192
+ return parsed.length ? parsed : null
193
+ }
@@ -1,5 +1,6 @@
1
1
  const NON_BREAKING_SPACE = '\u00a0'
2
2
  const RUSSIAN_SPACED_HYPHEN = /(\p{sc=Cyrillic}) - (?=\p{sc=Cyrillic})/gu
3
+ const RUSSIAN_SPACED_EM_DASH = /(\p{sc=Cyrillic}) (?=—)/gu
3
4
  const TEXT_NODE_FILTER = 4
4
5
  const FILTER_ACCEPT = 1
5
6
  const FILTER_REJECT = 2
@@ -94,6 +95,7 @@ export function typographText(value, options = {}) {
94
95
  return value
95
96
  .replace(serviceWordPattern, `$1${NON_BREAKING_SPACE}`)
96
97
  .replace(RUSSIAN_SPACED_HYPHEN, `$1${NON_BREAKING_SPACE}— `)
98
+ .replace(RUSSIAN_SPACED_EM_DASH, `$1${NON_BREAKING_SPACE}`)
97
99
  }
98
100
 
99
101
  /**
@@ -0,0 +1,60 @@
1
+ const THEME_ASSET_PREFIX = '/theme'
2
+ const THEME_ASSET_ORIGIN = 'https://theme-assets.invalid'
3
+
4
+ function unsafeThemeAssetPath() {
5
+ throw new Error('theme asset path содержит небезопасные сегменты')
6
+ }
7
+
8
+ export function themeAssetPath(path = '') {
9
+ const source = String(path)
10
+ const pathPart = source.split(/[?#]/, 1)[0]
11
+
12
+ if (pathPart.includes('\\'))
13
+ unsafeThemeAssetPath()
14
+
15
+ const segments = pathPart
16
+ .replace(/^\/+/, '')
17
+ .split('/')
18
+ .filter(Boolean)
19
+
20
+ for (const segment of segments) {
21
+ let decoded = ''
22
+
23
+ try {
24
+ decoded = decodeURIComponent(segment)
25
+ }
26
+ catch {
27
+ unsafeThemeAssetPath()
28
+ }
29
+
30
+ if (decoded === '.' || decoded === '..' || decoded.includes('/') || decoded.includes('\\'))
31
+ unsafeThemeAssetPath()
32
+ }
33
+
34
+ const normalized = `/${source.replace(/^\/+/, '')}`
35
+ const normalizedPathPart = normalized.split(/[?#]/, 1)[0]
36
+ const candidate = normalizedPathPart === THEME_ASSET_PREFIX
37
+ || normalizedPathPart.startsWith(`${THEME_ASSET_PREFIX}/`)
38
+ ? normalized
39
+ : `${THEME_ASSET_PREFIX}${normalized}`
40
+ const url = new URL(candidate, THEME_ASSET_ORIGIN)
41
+
42
+ if (url.pathname !== THEME_ASSET_PREFIX && !url.pathname.startsWith(`${THEME_ASSET_PREFIX}/`))
43
+ unsafeThemeAssetPath()
44
+
45
+ return candidate
46
+ }
47
+
48
+ /**
49
+ * @param {number} number
50
+ */
51
+ export function themePhotoPath(number) {
52
+ return themeAssetPath(`/photos/photo-${number}.webp`)
53
+ }
54
+
55
+ /**
56
+ * @param {string} file
57
+ */
58
+ export function themeDecorPath(file) {
59
+ return themeAssetPath(`/decor/${String(file).replace(/^\/+/, '')}`)
60
+ }
@@ -0,0 +1,27 @@
1
+ export interface TimelineGridItem {
2
+ active?: boolean
3
+ }
4
+
5
+ export function resolveTimelineItemGridStyle(
6
+ items: readonly TimelineGridItem[],
7
+ index: number,
8
+ ) {
9
+ const count = items.length || 1
10
+ const activeItemIndices = items.flatMap((item, itemIndex) => item.active ? [itemIndex] : [])
11
+
12
+ if (count === 3 && activeItemIndices.length === 1) {
13
+ const activeIndex = activeItemIndices[0]
14
+ const spans = items.map((_, itemIndex) => itemIndex === activeIndex ? 6 : 3)
15
+ const start = spans.slice(0, index).reduce((sum, span) => sum + span, 1)
16
+
17
+ return { gridColumn: `${start} / ${start + spans[index]}` }
18
+ }
19
+
20
+ if (count > 12)
21
+ throw new Error('[Timeline] items поддерживает не больше 12 событий.')
22
+
23
+ const start = Math.floor((index * 12) / count) + 1
24
+ const end = Math.floor(((index + 1) * 12) / count) + 1
25
+
26
+ return { gridColumn: `${start} / ${end}` }
27
+ }
@@ -0,0 +1,29 @@
1
+ const { readFileSync } = require('node:fs')
2
+ const ts = require('typescript')
3
+
4
+ /** @typedef {NodeJS.Module & { _compile(source: string, filename: string): void }} CompilableModule */
5
+
6
+ let registered = false
7
+
8
+ function registerTypeScript() {
9
+ if (registered)
10
+ return
11
+
12
+ require.extensions['.ts'] = (module, filename) => {
13
+ const compilableModule = /** @type {CompilableModule} */ (module)
14
+ const source = readFileSync(filename, 'utf8')
15
+ const { outputText } = ts.transpileModule(source, {
16
+ compilerOptions: {
17
+ module: ts.ModuleKind.CommonJS,
18
+ target: ts.ScriptTarget.ES2022,
19
+ },
20
+ fileName: filename,
21
+ })
22
+
23
+ compilableModule._compile(outputText, filename)
24
+ }
25
+
26
+ registered = true
27
+ }
28
+
29
+ module.exports = { registerTypeScript }
@@ -0,0 +1,277 @@
1
+ const { readFileSync } = require('node:fs')
2
+ const { isAbsolute, join, resolve } = require('node:path')
3
+ const { parseSync } = require('@slidev/parser')
4
+ const { createCommentVNode, createTextVNode, createVNode } = require('vue')
5
+ const { registerTypeScript } = require('./typescript-require.cjs')
6
+ const { extractDeckLiveStructure } = require('./deck-slot-markup.cjs')
7
+ const { markdownToVnodes } = require('./markdown-to-vnodes.cjs')
8
+
9
+ /** @typedef {import('@slidev/types').SourceSlideInfo} SourceSlideInfo */
10
+ /** @typedef {import('./layout-recipes').ThemeLayout} ThemeLayout */
11
+ /** @typedef {import('./layout-recipes').ThemeLayoutRole} ThemeLayoutRole */
12
+ /** @typedef {{ role: string, hasCoordinates: boolean, label: string }} ExplicitSlotIntent */
13
+ /**
14
+ * @typedef {(
15
+ * | { kind: 'comment' }
16
+ * | { kind: 'text', content: string }
17
+ * | { kind: 'element', tag: string, props: Record<string, unknown>, children: DeckLiveNode[] }
18
+ * )} DeckLiveNode
19
+ */
20
+ /** @typedef {{ index: number, page: number, layout: string, variant: string, title: string, label: string }} SlideReference */
21
+ /** @typedef {SlideReference & { message: string, hint?: string }} DeckValidationIssue */
22
+
23
+ const THEME_ROOT = join(__dirname, '..')
24
+ /** @type {Set<ThemeLayout>} */
25
+ const THEME_LAYOUTS = new Set(['cover', 'message', 'explainer', 'collection'])
26
+
27
+ /** @param {...string} segments */
28
+ function themePath(...segments) {
29
+ return join(THEME_ROOT, ...segments)
30
+ }
31
+
32
+ /**
33
+ * @param {string} value
34
+ * @returns {value is ThemeLayout}
35
+ */
36
+ function isThemeLayout(value) {
37
+ return THEME_LAYOUTS.has(/** @type {ThemeLayout} */ (value))
38
+ }
39
+
40
+ registerTypeScript()
41
+
42
+ const { compileLayoutAuthoring } = require(themePath('composables/layout-authoring.ts'))
43
+ const { hasLayoutShorthand } = require(themePath('composables/layout-shorthands.ts'))
44
+ const {
45
+ resolveLayoutSlotSpec,
46
+ resolveLayoutVariant,
47
+ } = require(themePath('composables/layout-recipes.ts'))
48
+ const { createSlotRules } = require(themePath('composables/slot-rules.ts'))
49
+
50
+ /** @returns {Record<string, import('vue').Component>} */
51
+ function stubComponents() {
52
+ /** @param {string} name */
53
+ const named = name => ({ name })
54
+ return {
55
+ Image: named('Image'),
56
+ Person: named('Person'),
57
+ Slot: named('Slot'),
58
+ Text: named('Text'),
59
+ Timeline: named('Timeline'),
60
+ }
61
+ }
62
+
63
+ function stripAuthorNotes(content = '') {
64
+ return String(content).replace(/<!--[\s\S]*?-->/g, '').trim()
65
+ }
66
+
67
+ /**
68
+ * @param {DeckLiveNode} node
69
+ * @param {ReturnType<typeof stubComponents>} components
70
+ * @returns {import('vue').VNode}
71
+ */
72
+ function liveNodeToVNode(node, components) {
73
+ if (node.kind === 'comment')
74
+ return createCommentVNode()
75
+
76
+ if (node.kind === 'text')
77
+ return createTextVNode(node.content)
78
+
79
+ const component = components[node.tag] ?? node.tag
80
+ return createVNode(
81
+ component,
82
+ node.props,
83
+ node.children.map(child => liveNodeToVNode(child, components)),
84
+ )
85
+ }
86
+
87
+ /**
88
+ * @param {ExplicitSlotIntent[]} intents
89
+ * @param {{ Slot: import('vue').Component, layout: ThemeLayout, variant: string }} input
90
+ */
91
+ function validateExplicitSlotIntents(intents, input) {
92
+ const slotRules = createSlotRules({ Slot: input.Slot })
93
+ const roleIndices = new Map()
94
+
95
+ for (const intent of intents) {
96
+ const node = createVNode(input.Slot, {
97
+ role: intent.role,
98
+ label: intent.label,
99
+ area: intent.hasCoordinates ? 'explicit' : '',
100
+ })
101
+ slotRules.assertLayoutChild(node, {
102
+ label: `${input.layout}:${input.variant}`,
103
+ })
104
+
105
+ const index = roleIndices.get(intent.role) ?? 0
106
+ resolveLayoutSlotSpec(
107
+ input.layout,
108
+ input.variant,
109
+ /** @type {ThemeLayoutRole} */ (intent.role),
110
+ index,
111
+ )
112
+ roleIndices.set(intent.role, index + 1)
113
+ }
114
+ }
115
+
116
+ /**
117
+ * @param {SourceSlideInfo} slide
118
+ * @param {number} index
119
+ * @returns {SlideReference}
120
+ */
121
+ function slideRef(slide, index) {
122
+ const layout = String(slide.frontmatter?.layout ?? '').trim()
123
+ const variant = String(slide.frontmatter?.variant ?? '').trim()
124
+ const title = String(slide.title || slide.content?.match(/^#\s+(.+)/m)?.[1] || '').trim()
125
+ return {
126
+ index,
127
+ page: index + 1,
128
+ layout,
129
+ variant,
130
+ title: title.slice(0, 80),
131
+ label: `${layout}:${variant || '-'}`,
132
+ }
133
+ }
134
+
135
+ /**
136
+ * @param {string} deckPath
137
+ * @returns {Promise<DeckValidationIssue[]>}
138
+ */
139
+ async function validateDeckLayouts(deckPath) {
140
+ const absolutePath = isAbsolute(deckPath) ? deckPath : resolve(deckPath)
141
+ const source = readFileSync(absolutePath, 'utf8')
142
+ const deck = parseSync(source, absolutePath)
143
+ const components = stubComponents()
144
+ /** @type {DeckValidationIssue[]} */
145
+ const issues = []
146
+
147
+ for (const [index, slide] of deck.slides.entries()) {
148
+ const layout = String(slide.frontmatter?.layout ?? '').trim()
149
+ if (!isThemeLayout(layout))
150
+ continue
151
+
152
+ const rawVariant = String(slide.frontmatter?.variant ?? '').trim()
153
+ const arrangement = String(slide.frontmatter?.arrangement ?? '').trim()
154
+ const ref = slideRef(slide, index)
155
+ /** @type {string} */
156
+ let variant
157
+
158
+ try {
159
+ variant = resolveLayoutVariant(layout, rawVariant, arrangement)
160
+ }
161
+ catch (error) {
162
+ const hint = error && typeof error === 'object' && 'hint' in error ? error.hint : undefined
163
+ issues.push({
164
+ ...ref,
165
+ message: error instanceof Error ? error.message : String(error),
166
+ hint: typeof hint === 'string' ? hint : undefined,
167
+ })
168
+ continue
169
+ }
170
+
171
+ const shorthandKey = `${layout}:${variant}`
172
+
173
+ let liveStructure
174
+ try {
175
+ liveStructure = extractDeckLiveStructure(slide.content ?? '')
176
+ }
177
+ catch (error) {
178
+ const hint = error && typeof error === 'object' && 'hint' in error ? error.hint : undefined
179
+ issues.push({
180
+ ...ref,
181
+ message: error instanceof Error ? error.message : String(error),
182
+ hint: typeof hint === 'string' ? hint : undefined,
183
+ })
184
+ continue
185
+ }
186
+
187
+ if (liveStructure.intents.length) {
188
+ try {
189
+ compileLayoutAuthoring({
190
+ props: {
191
+ layout,
192
+ variant: rawVariant,
193
+ arrangement,
194
+ mode: slide.frontmatter?.mode,
195
+ tone: slide.frontmatter?.tone ?? '',
196
+ },
197
+ frontmatter: slide.frontmatter ?? {},
198
+ children: liveStructure.children.map(node => liveNodeToVNode(node, components)),
199
+ components,
200
+ defaultTone: 'blue',
201
+ })
202
+ validateExplicitSlotIntents(liveStructure.intents, {
203
+ Slot: components.Slot,
204
+ layout,
205
+ variant,
206
+ })
207
+ }
208
+ catch (error) {
209
+ const hint = error && typeof error === 'object' && 'hint' in error ? error.hint : undefined
210
+ issues.push({
211
+ ...ref,
212
+ message: error instanceof Error ? error.message : String(error),
213
+ hint: typeof hint === 'string' ? hint : undefined,
214
+ })
215
+ }
216
+ continue
217
+ }
218
+
219
+ if (!hasLayoutShorthand(shorthandKey) && layout !== 'cover')
220
+ continue
221
+
222
+ const content = stripAuthorNotes(slide.content ?? '')
223
+
224
+ try {
225
+ compileLayoutAuthoring({
226
+ props: {
227
+ layout,
228
+ variant: rawVariant,
229
+ arrangement,
230
+ mode: slide.frontmatter?.mode,
231
+ tone: slide.frontmatter?.tone ?? '',
232
+ },
233
+ frontmatter: slide.frontmatter ?? {},
234
+ children: markdownToVnodes(content),
235
+ components,
236
+ defaultTone: 'blue',
237
+ })
238
+ }
239
+ catch (error) {
240
+ const hint = error && typeof error === 'object' && 'hint' in error ? error.hint : undefined
241
+ issues.push({
242
+ ...ref,
243
+ message: error instanceof Error ? error.message : String(error),
244
+ hint: typeof hint === 'string' ? hint : undefined,
245
+ })
246
+ }
247
+ }
248
+
249
+ return issues
250
+ }
251
+
252
+ /**
253
+ * @param {DeckValidationIssue[]} issues
254
+ * @param {string} [deckPath]
255
+ */
256
+ function formatDeckLayoutIssues(issues, deckPath = 'deck') {
257
+ if (!issues.length)
258
+ return ''
259
+
260
+ const body = issues.map((issue) => {
261
+ const lines = [
262
+ `Слайд ${issue.page} (${issue.label})${issue.title ? `: «${issue.title}»` : ''}`,
263
+ ` ${issue.message}`,
264
+ ]
265
+ if (issue.hint)
266
+ lines.push(` Подсказка: ${issue.hint}`)
267
+ lines.push(' См. example.md в slidev-theme-practicum (заметки «Контракт»).')
268
+ return lines.join('\n')
269
+ }).join(`\n\n${'—'.repeat(40)}\n\n`)
270
+
271
+ return `${body}\n\nКолода: ${deckPath}\n`
272
+ }
273
+
274
+ module.exports = {
275
+ formatDeckLayoutIssues,
276
+ validateDeckLayouts,
277
+ }
package/decor-library.md CHANGED
@@ -7,4 +7,6 @@ themeConfig:
7
7
  debugGrid: false
8
8
  ---
9
9
 
10
+ <!-- Встроенные файлы предпросмотра загружаются по префиксу /theme. -->
11
+
10
12
  <DecorLibrary />