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,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-registry'
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
+ }