slidev-theme-practicum 0.1.0 → 0.1.3
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/LICENSE +21 -0
- package/README.md +72 -10
- package/components/Slide.vue +81 -26
- package/components/Slot.vue +20 -85
- package/components/Text.vue +68 -2
- package/components/TextFitGroup.vue +71 -0
- package/components/Timeline.vue +40 -37
- package/composables/deck-slot-markup.cjs +249 -0
- package/composables/decor-catalog.mjs +169 -47
- package/composables/layout-authoring.ts +6 -2
- package/composables/layout-markdown.mjs +33 -0
- package/composables/layout-recipes.ts +51 -5
- package/composables/layout-shorthands.ts +251 -81
- package/composables/markdown-to-vnodes.cjs +1 -0
- package/composables/markdown-to-vnodes.mjs +237 -0
- package/composables/slot-placement.ts +1 -1
- package/composables/slot-spacing.ts +66 -0
- package/composables/text-fit-runtime.ts +458 -76
- package/composables/text-fit-scope.ts +13 -0
- package/composables/text-slot-markdown.mjs +193 -0
- package/composables/text-typography.mjs +2 -0
- package/composables/theme-assets.mjs +60 -0
- package/composables/timeline-grid.ts +31 -0
- package/composables/typescript-require.cjs +29 -0
- package/composables/validate-deck-layouts.cjs +277 -0
- package/decor-library.md +2 -0
- package/example.md +132 -10
- package/package.json +25 -11
- package/public/photos/photo-1.webp +0 -0
- package/public/photos/photo-10.webp +0 -0
- package/public/photos/photo-11.webp +0 -0
- package/public/photos/photo-12.webp +0 -0
- package/public/photos/photo-13.webp +0 -0
- package/public/photos/photo-14.webp +0 -0
- package/public/photos/photo-15.webp +0 -0
- package/public/photos/photo-16.webp +0 -0
- package/public/photos/photo-17.webp +0 -0
- package/public/photos/photo-18.webp +0 -0
- package/public/photos/photo-19.webp +0 -0
- package/public/photos/photo-2.webp +0 -0
- package/public/photos/photo-20.webp +0 -0
- package/public/photos/photo-21.webp +0 -0
- package/public/photos/photo-22.webp +0 -0
- package/public/photos/photo-23.webp +0 -0
- package/public/photos/photo-24.webp +0 -0
- package/public/photos/photo-25.webp +0 -0
- package/public/photos/photo-26.webp +0 -0
- package/public/photos/photo-27.webp +0 -0
- package/public/photos/photo-28.webp +0 -0
- package/public/photos/photo-29.webp +0 -0
- package/public/photos/photo-3.webp +0 -0
- package/public/photos/photo-30.webp +0 -0
- package/public/photos/photo-4.webp +0 -0
- package/public/photos/photo-5.webp +0 -0
- package/public/photos/photo-6.webp +0 -0
- package/public/photos/photo-7.webp +0 -0
- package/public/photos/photo-8.webp +0 -0
- package/public/photos/photo-9.webp +0 -0
- package/scripts/browser-smoke-runtime.mjs +428 -0
- package/scripts/browser-smoke.mjs +436 -0
- package/scripts/build-artifact-assets.mjs +55 -0
- package/scripts/check-build-artifact.mjs +243 -0
- package/scripts/check-package.mjs +132 -0
- package/scripts/check-test-architecture.mjs +176 -0
- package/scripts/npm-spawn.mjs +31 -0
- package/scripts/test-architecture-policy.mjs +245 -0
- package/scripts/theme-asset-inventory.mjs +83 -0
- package/scripts/validate-deck.cjs +18 -0
- package/setup/decor-save-middleware.ts +244 -0
- package/setup/vite-plugins.ts +14 -84
- package/skills/slidev-practicum/SKILL.md +25 -0
- package/skills/slidev-practicum/agents/openai.yaml +4 -0
- package/styles/index.css +10 -1
- package/composables/layout-registry.ts +0 -47
- package/env.d.ts +0 -6
- 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/slidev-theme-practicum.png +0 -0
- package/slidev-theme-practicum.svg +0 -102
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
isVNode,
|
|
7
|
+
shallowRef,
|
|
8
|
+
type PropType,
|
|
9
|
+
type VNode,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
import { createTextFitRuntime } from '../composables/text-fit-runtime'
|
|
12
|
+
import { useTextFitScope } from '../composables/text-fit-scope'
|
|
13
|
+
|
|
14
|
+
const textFit = createTextFitRuntime()
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
fitGroup: string
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const TextFitGroupContent = defineComponent({
|
|
21
|
+
name: 'TextFitGroupContent',
|
|
22
|
+
props: {
|
|
23
|
+
fitGroup: {
|
|
24
|
+
type: String as PropType<string>,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
setup(props, { slots }) {
|
|
29
|
+
const groupElement = shallowRef<HTMLElement | null>(null)
|
|
30
|
+
const textFitScope = useTextFitScope()
|
|
31
|
+
const sharedKey = computed(() => {
|
|
32
|
+
if (!props.fitGroup)
|
|
33
|
+
return ''
|
|
34
|
+
|
|
35
|
+
return `${textFitScope}:${props.fitGroup}`
|
|
36
|
+
})
|
|
37
|
+
let currentChildren: VNode[] = []
|
|
38
|
+
const { apply } = textFit.useGroup({
|
|
39
|
+
target: groupElement,
|
|
40
|
+
nodes: () => currentChildren,
|
|
41
|
+
sharedKey,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
const children = (slots.default?.() ?? []).filter(isVNode)
|
|
46
|
+
currentChildren = children
|
|
47
|
+
|
|
48
|
+
return h('div', {
|
|
49
|
+
ref: groupElement,
|
|
50
|
+
class: 'TextFitGroup',
|
|
51
|
+
}, apply(children))
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<TextFitGroupContent :fit-group="props.fitGroup">
|
|
59
|
+
<slot />
|
|
60
|
+
</TextFitGroupContent>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<style scoped>
|
|
64
|
+
.TextFitGroup {
|
|
65
|
+
min-width: 0;
|
|
66
|
+
width: 100%;
|
|
67
|
+
height: 100%;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: inherit;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
package/components/Timeline.vue
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, shallowRef, type ComponentPublicInstance } from 'vue'
|
|
2
|
+
import { computed, getCurrentInstance, shallowRef, type ComponentPublicInstance } from 'vue'
|
|
3
|
+
import { resolveTimelineItemGridStyle, resolveTimelineLabelSpan } from '../composables/timeline-grid'
|
|
3
4
|
import { createTextFitRuntime, type ThemeTextSize } from '../composables/text-fit-runtime'
|
|
5
|
+
import Text from './Text.vue'
|
|
6
|
+
import TextFitGroup from './TextFitGroup.vue'
|
|
4
7
|
|
|
5
8
|
const TIMELINE_ACTIVE_YEAR_FALLBACK_SIZE: ThemeTextSize = 9
|
|
9
|
+
const timelineFitGroup = `timeline-${getCurrentInstance()?.uid ?? 'default'}`
|
|
6
10
|
|
|
7
11
|
const props = defineProps({
|
|
8
12
|
items: {
|
|
@@ -15,6 +19,7 @@ const timelineElement = shallowRef<HTMLElement | null>(null)
|
|
|
15
19
|
const activeYearElement = shallowRef<HTMLElement | null>(null)
|
|
16
20
|
const hasActiveItem = computed(() => props.items.some(item => item.active))
|
|
17
21
|
const activeIndices = computed(() => props.items.flatMap((item, itemIndex) => item.active ? [itemIndex] : []))
|
|
22
|
+
const labelSpan = computed(() => resolveTimelineLabelSpan(props.items))
|
|
18
23
|
const TIMELINE_ACTIVE_YEAR_MAX_SIZE = computed<ThemeTextSize>(() => 12)
|
|
19
24
|
const textFit = createTextFitRuntime()
|
|
20
25
|
|
|
@@ -63,38 +68,19 @@ const { size: fittedActiveYearSize } = textFit.useElement({
|
|
|
63
68
|
],
|
|
64
69
|
})
|
|
65
70
|
|
|
66
|
-
const timelineStyle = computed(() =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
const timelineStyle = computed(() => ({
|
|
72
|
+
'--theme-timeline-label-width': `var(--theme-grid-span-${labelSpan.value}-width)`,
|
|
73
|
+
...(hasActiveItem.value
|
|
74
|
+
? {
|
|
75
|
+
'--theme-timeline-active-year-size': `var(--theme-text-size-${fittedActiveYearSize.value})`,
|
|
76
|
+
'--theme-timeline-active-year-line': `var(--theme-text-line-${fittedActiveYearSize.value})`,
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
}))
|
|
72
80
|
|
|
73
81
|
function getItemGridStyle(index: number) {
|
|
74
|
-
const count = props.items.length || 1
|
|
75
|
-
const activeItemIndices = activeIndices.value
|
|
76
|
-
|
|
77
82
|
assertSingleActiveItem()
|
|
78
|
-
|
|
79
|
-
if (count === 3 && activeItemIndices.length === 1) {
|
|
80
|
-
const activeIndex = activeItemIndices[0]
|
|
81
|
-
const spans = props.items.map((_, itemIndex) => itemIndex === activeIndex ? 6 : 3)
|
|
82
|
-
const start = spans.slice(0, index).reduce((sum, span) => sum + span, 1)
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
gridColumn: `${start} / ${start + spans[index]}`,
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (count > 12)
|
|
90
|
-
throw new Error('[Timeline] items поддерживает не больше 12 событий.')
|
|
91
|
-
|
|
92
|
-
const start = Math.floor((index * 12) / count) + 1
|
|
93
|
-
const end = Math.floor(((index + 1) * 12) / count) + 1
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
gridColumn: `${start} / ${end}`,
|
|
97
|
-
}
|
|
83
|
+
return resolveTimelineItemGridStyle(props.items, index)
|
|
98
84
|
}
|
|
99
85
|
</script>
|
|
100
86
|
|
|
@@ -110,9 +96,21 @@ function getItemGridStyle(index: number) {
|
|
|
110
96
|
class="Timeline-Item"
|
|
111
97
|
:style="getItemGridStyle(index)"
|
|
112
98
|
:class="{ 'Timeline-Item_active': item.active }">
|
|
113
|
-
<div :ref="element => setYearElement(element, item.active)" class="Timeline-Year">
|
|
99
|
+
<div :ref="element => setYearElement(element, item.active)" class="Timeline-Year">
|
|
100
|
+
<span v-if="item.active">{{ item.year }}</span>
|
|
101
|
+
<TextFitGroup v-else
|
|
102
|
+
class="Timeline-YearFitGroup"
|
|
103
|
+
:fit-group="`${timelineFitGroup}-years`">
|
|
104
|
+
<Text size="4-7">{{ item.year }}</Text>
|
|
105
|
+
</TextFitGroup>
|
|
106
|
+
</div>
|
|
114
107
|
<div class="Timeline-Dot" />
|
|
115
|
-
<div class="Timeline-Label">
|
|
108
|
+
<div class="Timeline-Label">
|
|
109
|
+
<Text v-if="item.active" size="2-3">{{ item.label }}</Text>
|
|
110
|
+
<TextFitGroup v-else :fit-group="`${timelineFitGroup}-labels`">
|
|
111
|
+
<Text size="2-3">{{ item.label }}</Text>
|
|
112
|
+
</TextFitGroup>
|
|
113
|
+
</div>
|
|
116
114
|
</div>
|
|
117
115
|
</div>
|
|
118
116
|
</template>
|
|
@@ -123,6 +121,7 @@ function getItemGridStyle(index: number) {
|
|
|
123
121
|
--timeline-dot-size: calc(var(--theme-grid-module) * 4);
|
|
124
122
|
--theme-timeline-active-year-size: var(--theme-text-size-9);
|
|
125
123
|
--theme-timeline-active-year-line: var(--theme-text-line-9);
|
|
124
|
+
--theme-timeline-label-width: var(--theme-grid-span-3-width);
|
|
126
125
|
position: relative;
|
|
127
126
|
display: grid;
|
|
128
127
|
grid-template-columns: repeat(var(--theme-grid-columns), minmax(0, 1fr));
|
|
@@ -143,6 +142,7 @@ function getItemGridStyle(index: number) {
|
|
|
143
142
|
|
|
144
143
|
.Timeline-Item {
|
|
145
144
|
position: relative;
|
|
145
|
+
min-width: 0;
|
|
146
146
|
display: grid;
|
|
147
147
|
grid-template-rows:
|
|
148
148
|
var(--timeline-block-height)
|
|
@@ -152,14 +152,18 @@ function getItemGridStyle(index: number) {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
.Timeline-Year {
|
|
155
|
+
min-width: 0;
|
|
155
156
|
min-height: var(--timeline-block-height);
|
|
157
|
+
padding-right: var(--theme-grid-gap);
|
|
156
158
|
display: flex;
|
|
157
159
|
align-items: flex-end;
|
|
158
|
-
font-size: var(--theme-text-size-7);
|
|
159
|
-
line-height: var(--theme-text-line-7);
|
|
160
160
|
color: var(--theme-text);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
.Timeline-YearFitGroup {
|
|
164
|
+
padding-bottom: var(--theme-grid-module);
|
|
165
|
+
}
|
|
166
|
+
|
|
163
167
|
.Timeline-Dot {
|
|
164
168
|
width: var(--timeline-dot-size);
|
|
165
169
|
height: var(--timeline-dot-size);
|
|
@@ -171,11 +175,10 @@ function getItemGridStyle(index: number) {
|
|
|
171
175
|
|
|
172
176
|
.Timeline-Label {
|
|
173
177
|
min-height: var(--timeline-block-height);
|
|
174
|
-
|
|
178
|
+
width: min(100%, var(--theme-timeline-label-width));
|
|
179
|
+
padding-right: var(--theme-grid-gap);
|
|
175
180
|
padding-top: var(--theme-slot-margin-3);
|
|
176
181
|
color: var(--theme-text-muted);
|
|
177
|
-
font-size: var(--theme-text-size-2);
|
|
178
|
-
line-height: var(--theme-text-line-2);
|
|
179
182
|
}
|
|
180
183
|
|
|
181
184
|
.Timeline_has_active .Timeline-Year {
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const MarkdownIt = require('markdown-it')
|
|
2
|
+
const { parse } = require('vue/compiler-sfc')
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('@vue/compiler-core').TemplateChildNode} TemplateChildNode */
|
|
5
|
+
/** @typedef {{ role: string, hasCoordinates: boolean, label: string }} ExplicitSlotIntent */
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {(
|
|
8
|
+
* | { kind: 'comment' }
|
|
9
|
+
* | { kind: 'text', content: string }
|
|
10
|
+
* | { kind: 'element', tag: string, props: Record<string, unknown>, children: DeckLiveNode[] }
|
|
11
|
+
* )} DeckLiveNode
|
|
12
|
+
*/
|
|
13
|
+
/** @typedef {{ children: DeckLiveNode[], intents: ExplicitSlotIntent[] }} DeckLiveStructure */
|
|
14
|
+
|
|
15
|
+
const SLOT_CONTRACT_PROPS = new Set(['role', 'area', 'col', 'row'])
|
|
16
|
+
const SLOT_CONTROL_FLOW_DIRECTIVES = new Set(['if', 'else-if', 'else', 'for'])
|
|
17
|
+
const markdown = new MarkdownIt({ html: true })
|
|
18
|
+
|
|
19
|
+
/** @param {unknown} error */
|
|
20
|
+
function formatCompilerError(error) {
|
|
21
|
+
return error instanceof Error ? error.message : String(error)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {import('@vue/compiler-core').DirectiveNode} prop
|
|
26
|
+
* @returns {unknown}
|
|
27
|
+
*/
|
|
28
|
+
function readLiteralBinding(prop) {
|
|
29
|
+
const ast = prop.exp?.ast
|
|
30
|
+
if (!ast)
|
|
31
|
+
return undefined
|
|
32
|
+
|
|
33
|
+
if (ast.type === 'StringLiteral'
|
|
34
|
+
|| ast.type === 'NumericLiteral'
|
|
35
|
+
|| ast.type === 'BooleanLiteral') {
|
|
36
|
+
return ast.value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (ast.type === 'NullLiteral')
|
|
40
|
+
return null
|
|
41
|
+
|
|
42
|
+
if (ast.type === 'TemplateLiteral' && ast.expressions.length === 0)
|
|
43
|
+
return ast.quasis[0]?.value?.cooked ?? ''
|
|
44
|
+
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {string} directive */
|
|
49
|
+
function failSlotControlFlow(directive) {
|
|
50
|
+
const error = /** @type {Error & { hint: string }} */ (
|
|
51
|
+
new Error(
|
|
52
|
+
`[DeckValidator] Slot не поддерживает v-${directive} `
|
|
53
|
+
+ 'на самом компоненте или его предке.',
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
error.hint = 'Разверните конечное количество Slot явно, чтобы validator мог проверить role и capacity.'
|
|
57
|
+
throw error
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @param {import('@vue/compiler-core').ElementNode} node */
|
|
61
|
+
function controlFlowDirective(node) {
|
|
62
|
+
return node.props.find(prop =>
|
|
63
|
+
prop.type === 7
|
|
64
|
+
&& SLOT_CONTROL_FLOW_DIRECTIVES.has(prop.name),
|
|
65
|
+
)?.name ?? ''
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {TemplateChildNode} node */
|
|
69
|
+
function containsSlot(node) {
|
|
70
|
+
if (node.type !== 1)
|
|
71
|
+
return false
|
|
72
|
+
|
|
73
|
+
return node.tag === 'Slot' || node.children.some(containsSlot)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {import('@vue/compiler-core').ElementNode} node
|
|
78
|
+
* @returns {Record<string, unknown>}
|
|
79
|
+
*/
|
|
80
|
+
function elementProps(node) {
|
|
81
|
+
/** @type {Record<string, unknown>} */
|
|
82
|
+
const props = {}
|
|
83
|
+
|
|
84
|
+
for (const prop of node.props) {
|
|
85
|
+
if (prop.type === 6) {
|
|
86
|
+
props[prop.name] = prop.value?.content ?? ''
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (prop.name !== 'bind')
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
const key = prop.arg?.type === 4 && prop.arg.isStatic
|
|
94
|
+
? prop.arg.content
|
|
95
|
+
: ''
|
|
96
|
+
|
|
97
|
+
if (node.tag !== 'Slot'
|
|
98
|
+
|| (!SLOT_CONTRACT_PROPS.has(key) && key !== 'label' && key)) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!key) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
'[DeckValidator] Slot не поддерживает динамический v-bind без статического имени prop.',
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const value = readLiteralBinding(prop)
|
|
109
|
+
if (key === 'label' && value === undefined)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
if (value === undefined) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`[DeckValidator] Slot не поддерживает динамический v-bind:${key}; укажите литеральное значение.`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
props[key] = value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return props
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {TemplateChildNode} node
|
|
126
|
+
* @returns {DeckLiveNode | null}
|
|
127
|
+
*/
|
|
128
|
+
function liveNode(node) {
|
|
129
|
+
if (node.type === 1) {
|
|
130
|
+
const directive = controlFlowDirective(node)
|
|
131
|
+
if (directive && containsSlot(node))
|
|
132
|
+
failSlotControlFlow(directive)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
kind: 'element',
|
|
136
|
+
tag: node.tag,
|
|
137
|
+
props: elementProps(node),
|
|
138
|
+
children: node.children
|
|
139
|
+
.map(liveNode)
|
|
140
|
+
.filter(isLiveNode),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (node.type === 2)
|
|
145
|
+
return { kind: 'text', content: node.content }
|
|
146
|
+
|
|
147
|
+
if (node.type === 3)
|
|
148
|
+
return { kind: 'comment' }
|
|
149
|
+
|
|
150
|
+
if (node.type === 5)
|
|
151
|
+
return { kind: 'text', content: `{{ ${node.content.loc.source} }}` }
|
|
152
|
+
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {DeckLiveNode | null} node
|
|
158
|
+
* @returns {node is DeckLiveNode}
|
|
159
|
+
*/
|
|
160
|
+
function isLiveNode(node) {
|
|
161
|
+
return node !== null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {DeckLiveNode} node
|
|
166
|
+
* @returns {ExplicitSlotIntent[]}
|
|
167
|
+
*/
|
|
168
|
+
function collectSlotIntents(node) {
|
|
169
|
+
const nested = node.kind === 'element'
|
|
170
|
+
? node.children.flatMap(collectSlotIntents)
|
|
171
|
+
: []
|
|
172
|
+
|
|
173
|
+
if (node.kind !== 'element' || node.tag !== 'Slot')
|
|
174
|
+
return nested
|
|
175
|
+
|
|
176
|
+
const role = String(node.props.role ?? '').trim()
|
|
177
|
+
const label = String(node.props.label ?? '').trim()
|
|
178
|
+
const hasCoordinates = ['area', 'col', 'row']
|
|
179
|
+
.some(key => Boolean(String(node.props[key] ?? '').trim()))
|
|
180
|
+
|
|
181
|
+
return [{ role, hasCoordinates, label }, ...nested]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* MarkdownIt оборачивает одиночные inline HTML components в `<p>`. Slidev
|
|
186
|
+
* поднимает component-only параграф обратно в default slot, поэтому validator
|
|
187
|
+
* нормализует ту же границу.
|
|
188
|
+
*
|
|
189
|
+
* @param {DeckLiveNode} node
|
|
190
|
+
* @returns {DeckLiveNode[]}
|
|
191
|
+
*/
|
|
192
|
+
function normalizeTopLevelNode(node) {
|
|
193
|
+
if (node.kind !== 'element' || node.tag !== 'p')
|
|
194
|
+
return [node]
|
|
195
|
+
|
|
196
|
+
const meaningful = node.children.filter(child =>
|
|
197
|
+
child.kind !== 'comment'
|
|
198
|
+
&& !(child.kind === 'text' && !child.content.trim()),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if (meaningful.length > 0
|
|
202
|
+
&& meaningful.every(child => child.kind === 'element' && child.tag === 'Slot')) {
|
|
203
|
+
return meaningful
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return [node]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Компилирует ровно ту top-level структуру, которую Slidev передаст layout-компоненту:
|
|
211
|
+
* Markdown уже превращён в HTML, а live Vue-компоненты сохранены как элементы.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} content
|
|
214
|
+
* @returns {DeckLiveStructure}
|
|
215
|
+
*/
|
|
216
|
+
function extractDeckLiveStructure(content) {
|
|
217
|
+
const rendered = markdown.render(String(content))
|
|
218
|
+
const source = `<template>${rendered}</template>`
|
|
219
|
+
const { descriptor, errors } = parse(source, { filename: 'deck-slide.vue' })
|
|
220
|
+
|
|
221
|
+
if (errors.length) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`[DeckValidator] Не удалось разобрать live markup слайда: ${errors.map(formatCompilerError).join('; ')}`,
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const children = (descriptor.template?.ast?.children ?? [])
|
|
228
|
+
.map(liveNode)
|
|
229
|
+
.filter(isLiveNode)
|
|
230
|
+
.flatMap(normalizeTopLevelNode)
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
children,
|
|
234
|
+
intents: children.flatMap(collectSlotIntents),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @param {string} content
|
|
240
|
+
* @returns {ExplicitSlotIntent[]}
|
|
241
|
+
*/
|
|
242
|
+
function extractExplicitSlotIntents(content) {
|
|
243
|
+
return extractDeckLiveStructure(content).intents
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
extractDeckLiveStructure,
|
|
248
|
+
extractExplicitSlotIntents,
|
|
249
|
+
}
|