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.
- package/LICENSE +21 -0
- package/README.md +72 -10
- package/components/Slide.vue +81 -26
- package/components/Slot.vue +33 -48
- package/components/Text.vue +68 -2
- package/components/Timeline.vue +2 -23
- 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 +27 -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 +424 -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,237 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it'
|
|
2
|
+
import { h } from 'vue'
|
|
3
|
+
import { dedentMarkdownSource } from './layout-markdown.mjs'
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('markdown-it/lib/token.mjs').default} MarkdownToken */
|
|
6
|
+
/** @typedef {import('vue').VNode} VNode */
|
|
7
|
+
/** @typedef {Extract<import('vue').VNodeChild, VNode | string>} InlineVNodeChild */
|
|
8
|
+
|
|
9
|
+
const md = new MarkdownIt({ html: false, linkify: false })
|
|
10
|
+
|
|
11
|
+
/** @type {Partial<Record<string, readonly [string, string]>>} */
|
|
12
|
+
const INLINE_TAGS = {
|
|
13
|
+
strong_open: ['strong_close', 'strong'],
|
|
14
|
+
em_open: ['em_close', 'em'],
|
|
15
|
+
s_open: ['s_close', 's'],
|
|
16
|
+
link_open: ['link_close', 'a'],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {MarkdownToken[]} [tokens]
|
|
21
|
+
* @param {number} [start]
|
|
22
|
+
* @param {string} [closeType]
|
|
23
|
+
* @returns {[InlineVNodeChild[], number]}
|
|
24
|
+
*/
|
|
25
|
+
function renderInlineTokens(tokens = [], start = 0, closeType = '') {
|
|
26
|
+
/** @type {InlineVNodeChild[]} */
|
|
27
|
+
const children = []
|
|
28
|
+
let index = start
|
|
29
|
+
|
|
30
|
+
while (index < tokens.length) {
|
|
31
|
+
const token = tokens[index]
|
|
32
|
+
if (closeType && token.type === closeType)
|
|
33
|
+
return [children, index + 1]
|
|
34
|
+
|
|
35
|
+
if (token.type === 'text' || token.type === 'code_inline') {
|
|
36
|
+
children.push(token.type === 'code_inline' ? h('code', token.content) : token.content)
|
|
37
|
+
index += 1
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (token.type === 'softbreak' || token.type === 'hardbreak') {
|
|
42
|
+
children.push(token.type === 'hardbreak' ? h('br') : ' ')
|
|
43
|
+
index += 1
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const entry = INLINE_TAGS[token.type]
|
|
48
|
+
if (entry) {
|
|
49
|
+
const [nested, next] = renderInlineTokens(tokens, index + 1, entry[0])
|
|
50
|
+
const props = token.type === 'link_open'
|
|
51
|
+
? Object.fromEntries((token.attrs ?? []).filter(([key]) => key === 'href' || key === 'title'))
|
|
52
|
+
: null
|
|
53
|
+
children.push(h(entry[1], props, nested))
|
|
54
|
+
index = next
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
index += 1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [children, index]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {MarkdownToken[]} tokens
|
|
66
|
+
* @param {number} index
|
|
67
|
+
* @returns {InlineVNodeChild[]}
|
|
68
|
+
*/
|
|
69
|
+
function inlineChildren(tokens, index) {
|
|
70
|
+
const token = tokens[index]
|
|
71
|
+
if (!token || token.type !== 'inline')
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
return renderInlineTokens(/** @type {MarkdownToken[]} */ (token.children))[0]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {InlineVNodeChild[]} children
|
|
79
|
+
* @returns {InlineVNodeChild | InlineVNodeChild[]}
|
|
80
|
+
*/
|
|
81
|
+
function inlineBlockContent(children) {
|
|
82
|
+
return children.length === 1 ? children[0] : children
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {MarkdownToken[]} tokens
|
|
87
|
+
* @param {number} start
|
|
88
|
+
* @returns {[VNode, number]}
|
|
89
|
+
*/
|
|
90
|
+
function parseListItem(tokens, start) {
|
|
91
|
+
/** @type {InlineVNodeChild[]} */
|
|
92
|
+
const childNodes = []
|
|
93
|
+
let index = start + 1
|
|
94
|
+
|
|
95
|
+
while (index < tokens.length) {
|
|
96
|
+
const token = tokens[index]
|
|
97
|
+
|
|
98
|
+
if (token.type === 'list_item_close')
|
|
99
|
+
return [h('li', childNodes), index + 1]
|
|
100
|
+
|
|
101
|
+
if (token.type === 'paragraph_open') {
|
|
102
|
+
const children = inlineChildren(tokens, index + 1)
|
|
103
|
+
if (children.length)
|
|
104
|
+
childNodes.push(...children)
|
|
105
|
+
index += 3
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
|
|
110
|
+
const nestedTag = token.type === 'bullet_list_open' ? 'ul' : 'ol'
|
|
111
|
+
const [nested, next] = parseList(tokens, index, nestedTag)
|
|
112
|
+
childNodes.push(nested)
|
|
113
|
+
index = next
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
index += 1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [h('li', childNodes), index]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {'ul' | 'ol'} tag
|
|
125
|
+
* @returns {'bullet_list_close' | 'ordered_list_close'}
|
|
126
|
+
*/
|
|
127
|
+
function listCloseType(tag) {
|
|
128
|
+
return tag === 'ol' ? 'ordered_list_close' : 'bullet_list_close'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {MarkdownToken[]} tokens
|
|
133
|
+
* @param {number} start
|
|
134
|
+
* @param {'ul' | 'ol'} tag
|
|
135
|
+
* @returns {[VNode, number]}
|
|
136
|
+
*/
|
|
137
|
+
function parseList(tokens, start, tag) {
|
|
138
|
+
/** @type {VNode[]} */
|
|
139
|
+
const items = []
|
|
140
|
+
let index = start + 1
|
|
141
|
+
const closeType = listCloseType(tag)
|
|
142
|
+
|
|
143
|
+
while (index < tokens.length) {
|
|
144
|
+
const token = tokens[index]
|
|
145
|
+
|
|
146
|
+
if (token.type === closeType)
|
|
147
|
+
return [h(tag, items), index + 1]
|
|
148
|
+
|
|
149
|
+
if (token.type === 'list_item_open') {
|
|
150
|
+
const [item, next] = parseListItem(tokens, index)
|
|
151
|
+
items.push(item)
|
|
152
|
+
index = next
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
index += 1
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return [h(tag, items), index]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {MarkdownToken[]} tokens
|
|
164
|
+
* @param {number} start
|
|
165
|
+
* @returns {[VNode, number]}
|
|
166
|
+
*/
|
|
167
|
+
function parseBlockquote(tokens, start) {
|
|
168
|
+
/** @type {VNode[]} */
|
|
169
|
+
const children = []
|
|
170
|
+
let index = start + 1
|
|
171
|
+
|
|
172
|
+
while (index < tokens.length && tokens[index].type !== 'blockquote_close') {
|
|
173
|
+
const token = tokens[index]
|
|
174
|
+
|
|
175
|
+
if (token.type === 'paragraph_open') {
|
|
176
|
+
const inline = inlineChildren(tokens, index + 1)
|
|
177
|
+
if (inline.length)
|
|
178
|
+
children.push(h('p', inlineBlockContent(inline)))
|
|
179
|
+
index += 3
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
index += 1
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return [h('blockquote', children), index + 1]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {unknown} [source]
|
|
191
|
+
* @returns {VNode[]}
|
|
192
|
+
*/
|
|
193
|
+
export function markdownToVnodes(source = '') {
|
|
194
|
+
const tokens = md.parse(dedentMarkdownSource(String(source ?? '')), {})
|
|
195
|
+
/** @type {VNode[]} */
|
|
196
|
+
const children = []
|
|
197
|
+
let index = 0
|
|
198
|
+
|
|
199
|
+
while (index < tokens.length) {
|
|
200
|
+
const token = tokens[index]
|
|
201
|
+
|
|
202
|
+
if (token.type === 'heading_open') {
|
|
203
|
+
const inline = inlineChildren(tokens, index + 1)
|
|
204
|
+
if (inline.length)
|
|
205
|
+
children.push(h(token.tag, inlineBlockContent(inline)))
|
|
206
|
+
index += 3
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (token.type === 'paragraph_open') {
|
|
211
|
+
const inline = inlineChildren(tokens, index + 1)
|
|
212
|
+
if (inline.length)
|
|
213
|
+
children.push(h('p', inlineBlockContent(inline)))
|
|
214
|
+
index += 3
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (token.type === 'bullet_list_open' || token.type === 'ordered_list_open') {
|
|
219
|
+
const tag = token.type === 'bullet_list_open' ? 'ul' : 'ol'
|
|
220
|
+
const [list, next] = parseList(tokens, index, tag)
|
|
221
|
+
children.push(list)
|
|
222
|
+
index = next
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (token.type === 'blockquote_open') {
|
|
227
|
+
const [quote, next] = parseBlockquote(tokens, index)
|
|
228
|
+
children.push(quote)
|
|
229
|
+
index = next
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
index += 1
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return children
|
|
237
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type GridRect,
|
|
9
9
|
} from './grid-placement'
|
|
10
10
|
import type { LayoutSlotPlan, ResolvedLayoutSlot } from './layout-authoring'
|
|
11
|
-
import type { ThemeLayoutRole, ThemeLayoutSlotSpec } from './layout-
|
|
11
|
+
import type { ThemeLayoutRole, ThemeLayoutSlotSpec } from './layout-recipes'
|
|
12
12
|
import { createSlotRules } from './slot-rules'
|
|
13
13
|
|
|
14
14
|
export type GridFootprint = {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type SlotMarginLevel = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
|
|
2
|
+
export type SlotMarginInput = SlotMarginLevel | `${SlotMarginLevel}`
|
|
3
|
+
export type SlotGapLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
|
|
4
|
+
export type SlotGapInput = SlotGapLevel | `${SlotGapLevel}`
|
|
5
|
+
|
|
6
|
+
export interface SlotSpacingInput {
|
|
7
|
+
margin?: SlotMarginInput
|
|
8
|
+
marginTop?: SlotMarginInput
|
|
9
|
+
marginRight?: SlotMarginInput
|
|
10
|
+
marginBottom?: SlotMarginInput
|
|
11
|
+
marginLeft?: SlotMarginInput
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SlotSpacingStyle {
|
|
15
|
+
paddingTop: string | undefined
|
|
16
|
+
paddingRight: string | undefined
|
|
17
|
+
paddingBottom: string | undefined
|
|
18
|
+
paddingLeft: string | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SLOT_MARGIN_LEVELS = new Set<string>(['1', '2', '3', '4', '5', '6', '7', '8'])
|
|
22
|
+
const SLOT_GAP_LEVELS = new Set<string>(['0', '1', '2', '3', '4', '5', '6', '7'])
|
|
23
|
+
|
|
24
|
+
export function resolveSlotMarginCssValue(
|
|
25
|
+
value: SlotMarginInput | undefined,
|
|
26
|
+
label = 'slot',
|
|
27
|
+
): string | undefined {
|
|
28
|
+
if (value == null)
|
|
29
|
+
return undefined
|
|
30
|
+
|
|
31
|
+
const normalizedValue = String(value)
|
|
32
|
+
if (!SLOT_MARGIN_LEVELS.has(normalizedValue)) {
|
|
33
|
+
throw new Error(`[${label}] margin должен быть уровнем 1..8. Получено: "${normalizedValue}".`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `var(--theme-slot-margin-${normalizedValue})`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveSlotGapCssValue(
|
|
40
|
+
value: SlotGapInput | undefined,
|
|
41
|
+
label = 'slot',
|
|
42
|
+
): string {
|
|
43
|
+
if (value == null || String(value) === '0')
|
|
44
|
+
return '0'
|
|
45
|
+
|
|
46
|
+
const normalizedValue = String(value)
|
|
47
|
+
if (!SLOT_GAP_LEVELS.has(normalizedValue)) {
|
|
48
|
+
throw new Error(`[${label}] gap должен быть уровнем 0..7. Получено: "${normalizedValue}".`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return `var(--theme-slot-margin-${normalizedValue})`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveSlotSpacingStyle(
|
|
55
|
+
input: SlotSpacingInput,
|
|
56
|
+
label = 'slot',
|
|
57
|
+
): SlotSpacingStyle | null {
|
|
58
|
+
const style: SlotSpacingStyle = {
|
|
59
|
+
paddingTop: resolveSlotMarginCssValue(input.marginTop ?? input.margin, label),
|
|
60
|
+
paddingRight: resolveSlotMarginCssValue(input.marginRight ?? input.margin, label),
|
|
61
|
+
paddingBottom: resolveSlotMarginCssValue(input.marginBottom ?? input.margin, label),
|
|
62
|
+
paddingLeft: resolveSlotMarginCssValue(input.marginLeft ?? input.margin, label),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Object.values(style).every(value => value == null) ? null : style
|
|
66
|
+
}
|