slidev-theme-practicum 0.3.0 → 0.4.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.
@@ -118,10 +118,64 @@ function readHeadingText(children: VNode[], label: string) {
118
118
  failMarkdownContract(label, 'ожидает один markdown heading внутри default slot.')
119
119
  }
120
120
 
121
+ function readRichHeadingChildren(node: VNode | null | undefined): Array<string | VNode> | null {
122
+ if (!node || !isHeadingNode(node))
123
+ return null
124
+
125
+ if (typeof node.children === 'string')
126
+ return [node.children]
127
+ if (!Array.isArray(node.children))
128
+ return null
129
+
130
+ return node.children.filter(child => isVNode(child) || typeof child === 'string') as Array<string | VNode>
131
+ }
132
+
133
+ function readRichNodeChildren(node: VNode | null | undefined): Array<string | VNode> | null {
134
+ if (!node || typeof node.children === 'string')
135
+ return null
136
+
137
+ if (!Array.isArray(node.children))
138
+ return null
139
+
140
+ return node.children.filter(child => isVNode(child) || typeof child === 'string') as Array<string | VNode>
141
+ }
142
+
143
+ function readHeadingSlotContent(node: VNode | null | undefined, textFallback: string) {
144
+ const richChildren = readRichHeadingChildren(node) ?? readRichNodeChildren(node)
145
+ return richChildren ?? readRichTextWithLineBreaks(textFallback)
146
+ }
147
+
148
+ function readParagraphSlotContent(node: VNode | null | undefined, textFallback: string) {
149
+ const richChildren = readRichNodeChildren(node)
150
+ return richChildren ?? readRichTextWithLineBreaks(textFallback)
151
+ }
152
+
153
+ function readRichTextWithLineBreaks(text: string): string | Array<string | VNode> {
154
+ const matches = text.split(/(<br\s*\/?>)/i)
155
+ if (matches.length === 1)
156
+ return text
157
+
158
+ return matches.flatMap<string | VNode>((value): Array<string | VNode> => {
159
+ if (!value.trim())
160
+ return []
161
+ if (/^<br\s*\/?>$/i.test(value))
162
+ return [h('br')]
163
+ return [value]
164
+ })
165
+ }
166
+
167
+ function hasRichSlotContent(content: Array<string | VNode>) {
168
+ return content.some(item => typeof item === 'string' ? item.trim() : isVNode(item))
169
+ }
170
+
121
171
  function readParagraphText(children: VNode[]) {
122
172
  const paragraph = children.find(isParagraphNode)
123
173
  if (paragraph)
124
- return { node: paragraph, text: readInlineText(paragraph) }
174
+ return {
175
+ node: paragraph,
176
+ text: readInlineText(paragraph),
177
+ content: readParagraphSlotContent(paragraph, readInlineText(paragraph)),
178
+ }
125
179
 
126
180
  return null
127
181
  }
@@ -130,7 +184,7 @@ function hText(
130
184
  context: LayoutShorthandContext,
131
185
  as: string,
132
186
  size: ThemeTextSizeInput,
133
- content: string,
187
+ content: string | VNode | Array<string | VNode>,
134
188
  options: {
135
189
  muted?: boolean
136
190
  maxSize?: boolean
@@ -172,10 +226,12 @@ function hText(
172
226
  if (options.typography)
173
227
  textProps['data-typography'] = options.typography
174
228
 
229
+ const slotChildren = Array.isArray(content) ? content : [content]
230
+
175
231
  return h(context.components.Text, {
176
232
  ...textProps,
177
233
  }, {
178
- default: withCtx(() => [content]),
234
+ default: withCtx(() => slotChildren),
179
235
  })
180
236
  }
181
237
 
@@ -251,15 +307,26 @@ function readListItemText(li: VNode) {
251
307
  return normalizeInlineText(parts.join(' '))
252
308
  }
253
309
 
254
- function readListItems(list: VNode, label: string) {
255
- if (!Array.isArray(list.children))
256
- failMarkdownContract(label, 'ожидает list items с текстом.')
310
+ function readListItemSlotContent(li: VNode): Array<string | VNode> {
311
+ if (typeof li.children === 'string') {
312
+ const text = li.children
313
+ return text.trim() ? [text] : []
314
+ }
257
315
 
258
- return list.children
259
- .filter(isVNode)
260
- .filter(node => node.type === 'li')
261
- .map(readListItemText)
262
- .filter(Boolean)
316
+ if (!Array.isArray(li.children))
317
+ return []
318
+
319
+ return li.children.flatMap<string | VNode>((child): Array<string | VNode> => {
320
+ if (!isVNode(child)) {
321
+ const text = String(child)
322
+ return text.trim() ? [text] : []
323
+ }
324
+
325
+ if (child.type === 'ul' || child.type === 'ol')
326
+ return []
327
+
328
+ return [cloneVNode(child)]
329
+ })
263
330
  }
264
331
 
265
332
  function readListItemNodes(list: VNode, label: string) {
@@ -296,7 +363,7 @@ function readListItemContent(li: VNode): VNode[] {
296
363
  if (!Array.isArray(li.children))
297
364
  return []
298
365
 
299
- return li.children.flatMap((child) => {
366
+ return li.children.flatMap<VNode>((child): VNode[] => {
300
367
  if (!isVNode(child)) {
301
368
  const text = String(child).trim()
302
369
  return text ? [h('p', text)] : []
@@ -306,7 +373,10 @@ function readListItemContent(li: VNode): VNode[] {
306
373
  })
307
374
  }
308
375
 
309
- type NestedListItem = { value: string, label: string }
376
+ type NestedListItem = {
377
+ value: Array<string | VNode>
378
+ label: Array<string | VNode>
379
+ }
310
380
 
311
381
  function readNestedListItems(list: VNode, label: string): NestedListItem[] {
312
382
  if (!Array.isArray(list.children))
@@ -317,46 +387,38 @@ function readNestedListItems(list: VNode, label: string): NestedListItem[] {
317
387
  .filter(node => node.type === 'li')
318
388
  .map((li) => {
319
389
  if (!Array.isArray(li.children)) {
320
- const text = typeof li.children === 'string' ? normalizeInlineText(li.children) : ''
321
- return { value: text, label: '' }
390
+ const text = typeof li.children === 'string' ? li.children : ''
391
+ const value = text.trim() ? [text] : []
392
+ return { value, label: [] }
322
393
  }
323
394
 
324
- const valueParts: string[] = []
325
395
  let nestedList: VNode | undefined
326
-
327
- for (const child of li.children) {
396
+ const value = li.children.flatMap<string | VNode>((child): Array<string | VNode> => {
328
397
  if (!isVNode(child)) {
329
- const text = String(child ?? '').trim()
330
- if (text)
331
- valueParts.push(text)
332
- continue
398
+ const text = String(child)
399
+ return text.trim() ? [text] : []
333
400
  }
334
401
 
335
402
  if (child.type === 'ul' || child.type === 'ol') {
336
403
  nestedList = child
337
- continue
404
+ return []
338
405
  }
339
406
 
340
- const text = readInlineText(child).trim()
341
- if (text)
342
- valueParts.push(text)
343
- }
344
-
345
- const value = normalizeInlineText(valueParts.join(' '))
407
+ return [cloneVNode(child)]
408
+ })
346
409
 
347
- let label = ''
348
- if (nestedList && Array.isArray(nestedList.children)) {
349
- const firstNestedLi = nestedList.children
410
+ const nestedLabel = nestedList && Array.isArray(nestedList.children)
411
+ ? nestedList.children
350
412
  .filter(isVNode)
351
413
  .find(node => node.type === 'li')
414
+ : null
352
415
 
353
- if (firstNestedLi)
354
- label = readListItemText(firstNestedLi)
416
+ return {
417
+ value,
418
+ label: nestedLabel ? readListItemSlotContent(nestedLabel) : [],
355
419
  }
356
-
357
- return { value, label }
358
420
  })
359
- .filter(item => item.value)
421
+ .filter(item => hasRichSlotContent(item.value))
360
422
  }
361
423
 
362
424
  const FACTS_VARIANT_COUNTS: Record<string, number> = {
@@ -540,6 +602,7 @@ function buildQuoteChildren(children: VNode[], context: LayoutShorthandContext)
540
602
 
541
603
  function buildCenteredChildren(children: VNode[], context: LayoutShorthandContext) {
542
604
  const title = readHeadingText(children, 'message:centered')
605
+ const titleContent = readHeadingSlotContent(title.node, title.text)
543
606
 
544
607
  ensureOnlySupportedChildren('message:centered', children, node =>
545
608
  node === title.node
@@ -548,7 +611,7 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
548
611
 
549
612
  return [
550
613
  hSlot(context, 'primary', 'slide-message-centered-primary', { centered: true }, [
551
- hText(context, 'h1', '7-12', title.text, { align: 'middle' }),
614
+ hText(context, 'h1', '7-12', titleContent, { align: 'middle' }),
552
615
  ]),
553
616
  ]
554
617
  }
@@ -556,6 +619,7 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
556
619
  function buildClosingChildren(children: VNode[], context: LayoutShorthandContext) {
557
620
  const title = readHeadingText(children, 'message:closing')
558
621
  const paragraph = readParagraphText(children)
622
+ const titleContent = readHeadingSlotContent(title.node, title.text)
559
623
 
560
624
  ensureOnlySupportedChildren('message:closing', children, node =>
561
625
  node === title.node
@@ -565,9 +629,9 @@ function buildClosingChildren(children: VNode[], context: LayoutShorthandContext
565
629
 
566
630
  return [
567
631
  hSlot(context, 'primary', 'slide-message-closing-primary', { centered: true }, [
568
- hText(context, 'h1', '7', title.text, { align: 'middle' }),
632
+ hText(context, 'h1', '7', titleContent, { align: 'middle' }),
569
633
  ...(paragraph
570
- ? [hText(context, 'p', '3', paragraph.text, { muted: true })]
634
+ ? [hText(context, 'p', '3', paragraph.content, { muted: true })]
571
635
  : []),
572
636
  ]),
573
637
  ]
@@ -575,8 +639,9 @@ function buildClosingChildren(children: VNode[], context: LayoutShorthandContext
575
639
 
576
640
  function buildDefinitionChildren(children: VNode[], context: LayoutShorthandContext) {
577
641
  const title = readHeadingText(children, 'explainer:definition')
642
+ const titleContent = readHeadingSlotContent(title.node, title.text)
578
643
  const paragraph = readParagraphText(children)
579
- const body = context.readFrontmatterText('body') || paragraph?.text || ''
644
+ const body = context.readFrontmatterText('body') || paragraph?.content || ''
580
645
  const label = context.readFrontmatterText('label')
581
646
 
582
647
  if (!body)
@@ -591,7 +656,7 @@ function buildDefinitionChildren(children: VNode[], context: LayoutShorthandCont
591
656
  return [
592
657
  hSlot(context, 'primary', 'slide-explainer-definition-primary', {}, [
593
658
  ...(label ? [hText(context, 'div', '3', label, { muted: true, className: 'Slide-DefinitionLabel' })] : []),
594
- hText(context, 'h1', '6', title.text, { priority: 1 }),
659
+ hText(context, 'h1', '6', titleContent, { priority: 1 }),
595
660
  hText(context, 'div', '6', body, { muted: true, priority: 1 }),
596
661
  ]),
597
662
  ]
@@ -608,6 +673,7 @@ function buildTitleRichSideChildren(
608
673
  options: { muted?: boolean } = {},
609
674
  ) {
610
675
  const title = readHeadingText(children, contract)
676
+ const titleContent = readHeadingSlotContent(title.node, title.text)
611
677
  const bodyNodes = children.filter(node =>
612
678
  node !== title.node
613
679
  && !isBlankTextNode(node),
@@ -627,7 +693,7 @@ function buildTitleRichSideChildren(
627
693
 
628
694
  return [
629
695
  hSlot(context, 'primary', `slide-explainer-${slotPrefix}-primary`, {}, [
630
- hText(context, 'h1', '6', title.text),
696
+ hText(context, 'h1', '6', titleContent),
631
697
  ]),
632
698
  hSlot(context, 'secondary', `slide-explainer-${slotPrefix}-secondary`, {}, [
633
699
  h('div', { class: bodyClass }, bodyNodes.map(node => cloneVNode(node))),
@@ -646,6 +712,7 @@ function buildTitleSupportsChildren(
646
712
  return buildTitleRichSideChildren(children, context, 'explainer:title-supports', { muted: true })
647
713
 
648
714
  const title = readHeadingText(children, 'explainer:title-supports')
715
+ const titleContent = readHeadingSlotContent(title.node, title.text)
649
716
  const list = children.find(isListNode)
650
717
  const muted = options.muted ?? true
651
718
  const supportSize = options.supportSize ?? '4'
@@ -678,7 +745,7 @@ function buildTitleSupportsChildren(
678
745
 
679
746
  return [
680
747
  hSlot(context, 'primary', 'slide-explainer-title-supports-bottom-primary', {}, [
681
- hText(context, 'h1', '6', title.text),
748
+ hText(context, 'h1', '6', titleContent),
682
749
  ]),
683
750
  ...items.map((item, index) =>
684
751
  hSlot(context, 'support', `slide-explainer-title-supports-bottom-support-${index + 1}`, {}, [
@@ -702,12 +769,21 @@ function buildTitleBodyChildren(children: VNode[], context: LayoutShorthandConte
702
769
 
703
770
  function buildPointsChildren(children: VNode[], context: LayoutShorthandContext) {
704
771
  const title = readHeadingText(children, 'collection:points')
772
+ const titleContent = readHeadingSlotContent(title.node, title.text)
705
773
  const list = children.find(isListNode)
706
774
 
707
775
  if (!list)
708
776
  failMarkdownContract('collection:points', 'ожидает markdown list с тремя пунктами.')
709
777
 
710
- const items = readListItems(list, 'collection:points')
778
+ const listItems = readListItemNodes(list, 'collection:points')
779
+ .map(node => ({
780
+ text: readListItemText(node),
781
+ content: readListItemSlotContent(node),
782
+ }))
783
+ const items = listItems
784
+ .filter(item => item.text)
785
+ .map(item => item.content)
786
+
711
787
  if (items.length !== 3)
712
788
  failMarkdownContract('collection:points', 'ожидает ровно три пункта для arrangement: trio.')
713
789
 
@@ -721,7 +797,7 @@ function buildPointsChildren(children: VNode[], context: LayoutShorthandContext)
721
797
  hSlot(context, 'primary', 'slide-collection-points-primary', {
722
798
  ...(context.frontmatterDecor ? { decor: context.frontmatterDecor } : {}),
723
799
  }, [
724
- hText(context, 'h1', '7', title.text, { className: 'Slide-PointsTitle' }),
800
+ hText(context, 'h1', '7', titleContent, { className: 'Slide-PointsTitle' }),
725
801
  ]),
726
802
  ...items.map((item, index) =>
727
803
  hSlot(context, 'support', `slide-collection-points-support-${index + 1}`, {}, [
@@ -778,6 +854,7 @@ function buildComparisonSideChildren(
778
854
 
779
855
  function buildComparisonChildren(children: VNode[], context: LayoutShorthandContext) {
780
856
  const title = readHeadingText(children, 'collection:comparison')
857
+ const titleContent = readHeadingSlotContent(title.node, title.text)
781
858
  const comparison = context.readFrontmatterObject('comparison')
782
859
 
783
860
  if (!comparison)
@@ -796,7 +873,7 @@ function buildComparisonChildren(children: VNode[], context: LayoutShorthandCont
796
873
 
797
874
  return [
798
875
  hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
799
- hText(context, 'h1', '6', title.text),
876
+ hText(context, 'h1', '6', titleContent),
800
877
  ]),
801
878
  buildComparisonSideChildren(context, from, 0),
802
879
  buildComparisonSideChildren(context, to, 1),
@@ -826,12 +903,13 @@ type StepItem = {
826
903
  function normalizeStepItems(context: LayoutShorthandContext): StepItem[] {
827
904
  const items = context.readFrontmatterArray('items')
828
905
  const isStaggered = context.variant === 'steps-staggered'
906
+ const minItems = isStaggered ? 2 : 3
829
907
 
830
- if (items.length < 3 || items.length > 6)
831
- failMarkdownContract('collection:steps', 'ожидает от 3 до 6 элементов во frontmatter items.')
832
-
833
- if (isStaggered && items.length !== 5)
834
- failMarkdownContract('collection:steps', 'arrangement: staggered ожидает ровно 5 элементов.')
908
+ if (items.length < minItems || items.length > 6) {
909
+ const range = isStaggered ? 'от 2 до 6' : 'от 3 до 6'
910
+ const prefix = isStaggered ? 'arrangement: staggered ' : ''
911
+ failMarkdownContract('collection:steps', `${prefix}ожидает ${range} элементов во frontmatter items.`)
912
+ }
835
913
 
836
914
  const normalized = items.map((item, index) => {
837
915
  if (typeof item !== 'object' || item === null || Array.isArray(item))
@@ -859,6 +937,7 @@ function normalizeStepItems(context: LayoutShorthandContext): StepItem[] {
859
937
 
860
938
  function buildStepsChildren(children: VNode[], context: LayoutShorthandContext) {
861
939
  const title = readHeadingText(children, 'collection:steps')
940
+ const titleContent = readHeadingSlotContent(title.node, title.text)
862
941
  const items = normalizeStepItems(context)
863
942
  const arrangement = context.variant === 'steps-staggered' ? 'staggered' : 'linear'
864
943
 
@@ -869,7 +948,7 @@ function buildStepsChildren(children: VNode[], context: LayoutShorthandContext)
869
948
 
870
949
  return [
871
950
  hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
872
- hText(context, 'h1', '6', title.text),
951
+ hText(context, 'h1', '6', titleContent),
873
952
  ]),
874
953
  hSlot(context, 'support', `slide-collection-${context.variant}-support`, {}, [
875
954
  h(context.components.StepsGrid, {
@@ -907,8 +986,9 @@ function normalizeTimelineItems(context: LayoutShorthandContext) {
907
986
 
908
987
  function buildTimelineChildren(children: VNode[], context: LayoutShorthandContext) {
909
988
  const title = readHeadingText(children, 'collection:timeline')
989
+ const titleContent = readHeadingSlotContent(title.node, title.text)
910
990
  const paragraph = readParagraphText(children)
911
- const body = context.readFrontmatterText('body') || paragraph?.text || ''
991
+ const body = context.readFrontmatterText('body') || paragraph?.content || ''
912
992
 
913
993
  ensureOnlySupportedChildren('collection:timeline', children, node =>
914
994
  node === title.node
@@ -918,7 +998,7 @@ function buildTimelineChildren(children: VNode[], context: LayoutShorthandContex
918
998
 
919
999
  return [
920
1000
  hSlot(context, 'primary', 'slide-collection-timeline-primary', {}, [
921
- hText(context, 'h1', '6', title.text),
1001
+ hText(context, 'h1', '6', titleContent),
922
1002
  ]),
923
1003
  ...(body
924
1004
  ? [
@@ -991,7 +1071,7 @@ function normalizeMetricItemsFromList(list: VNode) {
991
1071
  failMarkdownContract('collection:metrics', 'ожидает минимум три пункта в markdown-списке.')
992
1072
 
993
1073
  return entries.map((entry, index) => {
994
- if (!entry.label)
1074
+ if (!hasRichSlotContent(entry.label))
995
1075
  failMarkdownContract('collection:metrics', `ожидает вложенный пункт с подписью у metric[${index}].`)
996
1076
 
997
1077
  return {
@@ -1023,6 +1103,7 @@ function normalizeMediaItems(context: LayoutShorthandContext) {
1023
1103
  function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext) {
1024
1104
  if (context.variant === 'metrics-dashboard') {
1025
1105
  const title = readHeadingText(children, 'collection:metrics')
1106
+ const titleContent = readHeadingSlotContent(title.node, title.text)
1026
1107
  const metrics = normalizeDashboardMetrics(context)
1027
1108
  const fitGroup = 'collection-metrics-dashboard'
1028
1109
 
@@ -1033,7 +1114,7 @@ function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext
1033
1114
 
1034
1115
  return [
1035
1116
  hSlot(context, 'primary', 'slide-collection-metrics-dashboard-primary', {}, [
1036
- hText(context, 'h1', '6', title.text),
1117
+ hText(context, 'h1', '6', titleContent),
1037
1118
  ]),
1038
1119
  ...metrics.map((metric, index) =>
1039
1120
  hSlot(context, 'support', `slide-collection-metrics-dashboard-support-${index + 1}`, {
@@ -1156,7 +1237,7 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
1156
1237
  }
1157
1238
 
1158
1239
  facts.forEach((fact, index) => {
1159
- if (!fact.label) {
1240
+ if (!hasRichSlotContent(fact.label)) {
1160
1241
  failMarkdownContract(
1161
1242
  'collection:facts',
1162
1243
  `ожидает вложенный пункт с подписью у facts[${index}].`,
@@ -1175,8 +1256,6 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
1175
1256
  const factFitGroup = shouldCoordinateFactSizes ? `collection-${context.variant}-facts` : ''
1176
1257
  const supportFactFitGroup = isFeatured ? `collection-${context.variant}-support-facts` : factFitGroup
1177
1258
  const headingText = readInlineText(heading)
1178
- const descriptionText = description ? readInlineText(description) : ''
1179
- const footnoteText = footnote ? readInlineText(footnote) : ''
1180
1259
  const rowFactSize: ThemeTextSizeInput = isQuartet ? '5-8' : isDuo ? '5-10' : '5-9'
1181
1260
  const stackedFactSize: ThemeTextSizeInput = '5-9'
1182
1261
  const stackedLabelSize: ThemeTextSizeInput = '3-5'
@@ -1185,24 +1264,30 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
1185
1264
  const supportFactSize: ThemeTextSizeInput = isFeatured ? '5-7' : isStacked ? stackedFactSize : rowFactSize
1186
1265
  const featuredLabelSize: ThemeTextSizeInput = isFeatured ? '4-6' : isStacked ? stackedLabelSize : rowLabelSize
1187
1266
  const supportLabelSize: ThemeTextSizeInput = isFeatured ? '2-4' : isStacked ? stackedLabelSize : rowLabelSize
1267
+ const headingContent = readHeadingSlotContent(heading, headingText)
1268
+ const descriptionContent = description ? readParagraphSlotContent(description, readInlineText(description)) : ''
1269
+ const footnoteContent = footnote ? readParagraphSlotContent(footnote, readInlineText(footnote)) : ''
1270
+ const hasFootnote = Array.isArray(footnoteContent)
1271
+ ? footnoteContent.length > 0
1272
+ : Boolean(footnoteContent)
1188
1273
 
1189
1274
  const primaryChildren: VNode[] = [
1190
- hText(context, 'h2', isFeatured ? '5-7' : '6-8', headingText, { priority: 1 }),
1275
+ hText(context, 'h2', isFeatured ? '5-7' : '6-8', headingContent, { priority: 1 }),
1191
1276
  ]
1192
1277
 
1193
- if (descriptionText)
1194
- primaryChildren.push(hText(context, 'p', isStacked ? '4-5' : '3-4', descriptionText, { priority: 2, muted: true }))
1278
+ if (descriptionContent)
1279
+ primaryChildren.push(hText(context, 'p', isStacked ? '4-5' : '3-4', descriptionContent, { priority: 2, muted: true }))
1195
1280
 
1196
- if (footnoteText && factFitGroup)
1197
- primaryChildren.push(hText(context, 'div', '2-3', footnoteText, { priority: 3, muted: true }))
1281
+ if (hasFootnote && factFitGroup)
1282
+ primaryChildren.push(hText(context, 'div', '2-3', footnoteContent, { priority: 3, muted: true }))
1198
1283
 
1199
1284
  const featuredChildren: VNode[] = [
1200
1285
  hText(context, 'div', featuredFactSize, featured.value, { priority: 1 }),
1201
1286
  hText(context, 'div', featuredLabelSize, featured.label, { priority: 2, muted: true }),
1202
1287
  ]
1203
1288
 
1204
- if (footnoteText && !factFitGroup)
1205
- featuredChildren.push(hText(context, 'div', '2-3', footnoteText, { priority: 3, muted: true }))
1289
+ if (hasFootnote && !factFitGroup)
1290
+ featuredChildren.push(hText(context, 'div', '2-3', footnoteContent, { priority: 3, muted: true }))
1206
1291
 
1207
1292
  if (isNumberedQuartet) {
1208
1293
  return [
@@ -190,8 +190,8 @@ function readBounds(input: ElementFitInput) {
190
190
  return bounds
191
191
  }
192
192
 
193
- function readFitObserverElements(element: HTMLElement) {
194
- const elements: HTMLElement[] = [element]
193
+ function readFitObserverElements(element: HTMLElement, observeOwnSize: boolean) {
194
+ const elements: HTMLElement[] = observeOwnSize ? [element] : []
195
195
  let parent = element.parentElement
196
196
 
197
197
  while (parent && elements.length < 8) {
@@ -496,6 +496,7 @@ function createTextFitCleanupOwner(isDisposed: () => boolean) {
496
496
  type TextFitLifecycleOwnerInput = {
497
497
  adapter: TextFitAdapter
498
498
  fit: () => void | Promise<void>
499
+ observeOwnSize?: boolean
499
500
  onDispose: () => void
500
501
  target: Ref<HTMLElement | null>
501
502
  watchTarget?: boolean
@@ -504,6 +505,7 @@ type TextFitLifecycleOwnerInput = {
504
505
  function createTextFitLifecycleOwner({
505
506
  adapter,
506
507
  fit,
508
+ observeOwnSize = true,
507
509
  onDispose,
508
510
  target,
509
511
  watchTarget = false,
@@ -532,7 +534,7 @@ function createTextFitLifecycleOwner({
532
534
  if (!element)
533
535
  return null
534
536
 
535
- return adapter.observe(readFitObserverElements(element), scheduleFit)
537
+ return adapter.observe(readFitObserverElements(element, observeOwnSize), scheduleFit)
536
538
  })
537
539
  }
538
540
 
@@ -734,6 +736,7 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
734
736
  const { scheduleFit } = createTextFitLifecycleOwner({
735
737
  adapter,
736
738
  fit: fitNow,
739
+ observeOwnSize: false,
737
740
  onDispose: () => {
738
741
  runId += 1
739
742
  },
@@ -876,6 +879,7 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
876
879
  const { scheduleFit } = createTextFitLifecycleOwner({
877
880
  adapter,
878
881
  fit: fitNow,
882
+ observeOwnSize: false,
879
883
  onDispose: () => {
880
884
  runId += 1
881
885
  unregisterSharedItem()
@@ -13,6 +13,7 @@ export type ResolvedThemeMode = {
13
13
  text: string
14
14
  muted: string
15
15
  contrast: boolean
16
+ foreground: 'light' | 'dark'
16
17
  }
17
18
 
18
19
  export type ResolvedSlideTheme = ResolvedThemeMode
@@ -112,6 +113,7 @@ export function resolveThemeMode(options: ResolveThemeModeOptions = {}): Resolve
112
113
  text: 'var(--theme-color-dark-0)',
113
114
  muted: 'var(--theme-color-dark-2)',
114
115
  contrast: false,
116
+ foreground: 'dark',
115
117
  }
116
118
  }
117
119
 
@@ -123,6 +125,7 @@ export function resolveThemeMode(options: ResolveThemeModeOptions = {}): Resolve
123
125
  text: 'var(--theme-color-light-0)',
124
126
  muted: 'var(--theme-text-muted-on-contrast)',
125
127
  contrast: true,
128
+ foreground: 'light',
126
129
  }
127
130
  }
128
131
 
@@ -130,9 +133,10 @@ export function resolveThemeMode(options: ResolveThemeModeOptions = {}): Resolve
130
133
  mode,
131
134
  tone,
132
135
  background: `var(--theme-color-${tone}-0)`,
133
- text: 'var(--theme-color-light-0)',
134
- muted: 'var(--theme-text-muted-on-contrast)',
136
+ text: 'var(--theme-text-on-color)',
137
+ muted: 'var(--theme-text-muted-on-color)',
135
138
  contrast: true,
139
+ foreground: 'light',
136
140
  }
137
141
  }
138
142
 
@@ -176,7 +180,7 @@ export function themeVars(
176
180
  '--theme-text-muted': slideTheme.muted,
177
181
  '--theme-inline-code-text': slideTheme.text,
178
182
  '--theme-text-on-dark': 'var(--theme-color-light-0)',
179
- '--theme-link': slideTheme.contrast ? 'var(--theme-color-light-0)' : 'var(--theme-current-color)',
183
+ '--theme-link': slideTheme.foreground === 'light' ? 'var(--theme-color-light-0)' : slideTheme.mode === 'color' ? 'var(--theme-text-on-color)' : 'var(--theme-current-color)',
180
184
  '--theme-current-color': `var(--theme-color-${slideTheme.tone}-0)`,
181
185
  }
182
186
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @typedef {{ text: string, size?: string, color?: string, slot: string, keyNumber: boolean, muted?: boolean }} TypographyText
3
+ * @typedef {{ code: string, message: string, hint?: string }} TypographyIssue
4
+ */
5
+
6
+ /** @param {TypographyText[]} texts */
7
+ function describeSizes(texts) {
8
+ return [...new Set(texts.map(text => `${text.size}: «${text.text}»`))].join(', ')
9
+ }
10
+
11
+ /**
12
+ * Общая политика для объявленных токенов и вычисленных значений браузера.
13
+ * Отсутствующее значение означает «неизвестно», а не значение по умолчанию.
14
+ * @param {TypographyText[]} texts
15
+ * @returns {TypographyIssue[]}
16
+ */
17
+ function inspectTypography(texts) {
18
+ /** @type {TypographyIssue[]} */
19
+ const issues = []
20
+ const regular = texts.filter(text => !text.keyNumber && text.size !== undefined)
21
+ const numbers = texts.filter(text => text.keyNumber && text.size !== undefined)
22
+ if (new Set(regular.map(text => text.size)).size > 2) {
23
+ issues.push({
24
+ code: 'text-sizes',
25
+ message: `Больше двух размеров обычного текста: ${describeSizes(regular)}.`,
26
+ hint: 'Объедините подписи с основным поясняющим текстом по размеру или измените композицию. Заголовок тоже входит в подсчёт.',
27
+ })
28
+ }
29
+ if (new Set(numbers.map(text => text.size)).size > 1) {
30
+ issues.push({
31
+ code: 'number-sizes',
32
+ message: `Ключевые числа имеют разные размеры: ${describeSizes(numbers)}.`,
33
+ hint: 'Все Text as="data" на слайде должны иметь один размер, в том числе после автоматического подбора.',
34
+ })
35
+ }
36
+
37
+ for (const slot of new Set(texts.map(text => text.slot))) {
38
+ const contents = texts.filter(text => text.slot === slot)
39
+ const colored = contents.filter(text => text.color !== undefined)
40
+ if (new Set(colored.map(text => text.color)).size > 1) {
41
+ issues.push({
42
+ code: 'slot-colors',
43
+ message: `${slot}: разные цвета текста: ${colored.map(text => `${text.color}: «${text.text}»`).join(', ')}.`,
44
+ hint: 'Выберите один основной цвет для всего текста внутри Slot.',
45
+ })
46
+ }
47
+ const muted = contents.filter(text => text.muted)
48
+ if (muted.length) {
49
+ issues.push({
50
+ code: 'muted-text',
51
+ message: `${slot}: приглушённый текст: ${muted.map(text => `«${text.text}»`).join(', ')}.`,
52
+ hint: 'Удалите muted и токены text-muted, используйте основной цвет поверхности.',
53
+ })
54
+ }
55
+ }
56
+ return issues
57
+ }
58
+
59
+ module.exports = { inspectTypography }
@@ -5,6 +5,7 @@ const { createCommentVNode, createTextVNode, createVNode } = require('vue')
5
5
  const { registerTypeScript } = require('./typescript-require.cjs')
6
6
  const { extractDeckLiveStructure } = require('./deck-slot-markup.cjs')
7
7
  const { markdownToVnodes } = require('./markdown-to-vnodes.cjs')
8
+ const { validateDeclaredTypography } = require('./validate-deck-typography.cjs')
8
9
 
9
10
  /** @typedef {import('@slidev/types').SourceSlideInfo} SourceSlideInfo */
10
11
  /** @typedef {import('./layout-recipes').ThemeLayout} ThemeLayout */
@@ -138,9 +139,10 @@ function slideRef(slide, index) {
138
139
 
139
140
  /**
140
141
  * @param {string} deckPath
142
+ * @param {{ typographyGuard?: boolean }} [options]
141
143
  * @returns {Promise<DeckValidationIssue[]>}
142
144
  */
143
- async function validateDeckLayouts(deckPath) {
145
+ async function validateDeckLayouts(deckPath, options = {}) {
144
146
  const absolutePath = isAbsolute(deckPath) ? deckPath : resolve(deckPath)
145
147
  const source = readFileSync(absolutePath, 'utf8')
146
148
  const deck = parseSync(source, absolutePath)
@@ -158,6 +160,21 @@ async function validateDeckLayouts(deckPath) {
158
160
  const layout = String(slide.frontmatter?.layout ?? '').trim()
159
161
  const ref = slideRef(slide, index)
160
162
 
163
+ if (options.typographyGuard) {
164
+ if (slide.frontmatter?.src) {
165
+ issues.push({ ...ref, message: 'Статическая проверка типографики пока не раскрывает включения src. Соберите слайды в один Markdown-файл перед проверкой.' })
166
+ continue
167
+ }
168
+ try {
169
+ const structure = extractDeckLiveStructure(slide.content ?? '')
170
+ issues.push(...validateDeclaredTypography(structure.children).map(issue => ({ ...ref, ...issue })))
171
+ }
172
+ catch (error) {
173
+ issues.push({ ...ref, message: error instanceof Error ? error.message : String(error) })
174
+ continue
175
+ }
176
+ }
177
+
161
178
  if (index > 0 && Object.prototype.hasOwnProperty.call(slide.frontmatter ?? {}, 'title')) {
162
179
  issues.push({
163
180
  ...ref,