@slidev/client 0.48.0-beta.21 → 0.48.0-beta.22

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.
@@ -12,13 +12,12 @@ Learn more: https://sli.dev/guide/syntax.html#line-highlighting
12
12
  -->
13
13
 
14
14
  <script setup lang="ts">
15
- import { parseRangeString } from '@slidev/parser/core'
16
15
  import { useClipboard } from '@vueuse/core'
17
16
  import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
18
17
  import type { PropType } from 'vue'
19
18
  import { configs } from '../env'
20
- import { makeId } from '../logic/utils'
21
- import { CLASS_VCLICK_HIDDEN, CLASS_VCLICK_TARGET } from '../constants'
19
+ import { makeId, updateCodeHighlightRange } from '../logic/utils'
20
+ import { CLASS_VCLICK_HIDDEN } from '../constants'
22
21
  import { useSlideContext } from '../context'
23
22
 
24
23
  const props = defineProps({
@@ -87,28 +86,27 @@ onMounted(() => {
87
86
  if (hide)
88
87
  rangeStr = props.ranges[index.value + 1] ?? finallyRange.value
89
88
 
90
- const isDuoTone = el.value.querySelector('.shiki-dark')
91
- const targets = isDuoTone ? Array.from(el.value.querySelectorAll('.shiki')) : [el.value]
92
- const startLine = props.startLine
93
- for (const target of targets) {
94
- const lines = Array.from(target.querySelectorAll('code > .line'))
95
- const highlights: number[] = parseRangeString(lines.length + startLine - 1, rangeStr)
96
- lines.forEach((line, idx) => {
97
- const highlighted = highlights.includes(idx + startLine)
98
- line.classList.toggle(CLASS_VCLICK_TARGET, true)
99
- line.classList.toggle('highlighted', highlighted)
100
- line.classList.toggle('dishonored', !highlighted)
101
- })
102
- if (props.maxHeight) {
103
- const highlightedEls = Array.from(target.querySelectorAll('.line.highlighted')) as HTMLElement[]
104
- const height = highlightedEls.reduce((acc, el) => el.offsetHeight + acc, 0)
105
- if (height > el.value.offsetHeight) {
106
- highlightedEls[0].scrollIntoView({ behavior: 'smooth', block: 'start' })
107
- }
108
- else if (highlightedEls.length > 0) {
109
- const middleEl = highlightedEls[Math.round((highlightedEls.length - 1) / 2)]
110
- middleEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
111
- }
89
+ const pre = el.value.querySelector('.shiki')!
90
+ const lines = Array.from(pre.querySelectorAll('code > .line'))
91
+ const linesCount = lines.length
92
+
93
+ updateCodeHighlightRange(
94
+ rangeStr,
95
+ linesCount,
96
+ props.startLine,
97
+ no => [lines[no]],
98
+ )
99
+
100
+ // Scroll to the highlighted line if `maxHeight` is set
101
+ if (props.maxHeight) {
102
+ const highlightedEls = Array.from(pre.querySelectorAll('.line.highlighted')) as HTMLElement[]
103
+ const height = highlightedEls.reduce((acc, el) => el.offsetHeight + acc, 0)
104
+ if (height > el.value.offsetHeight) {
105
+ highlightedEls[0].scrollIntoView({ behavior: 'smooth', block: 'start' })
106
+ }
107
+ else if (highlightedEls.length > 0) {
108
+ const middleEl = highlightedEls[Math.round((highlightedEls.length - 1) / 2)]
109
+ middleEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
112
110
  }
113
111
  }
114
112
  })
@@ -1,22 +1,28 @@
1
1
  <script setup lang="ts">
2
2
  import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
3
3
  import type { KeyedTokensInfo } from 'shiki-magic-move/types'
4
- import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
4
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
5
5
  import lz from 'lz-string'
6
6
  import { useSlideContext } from '../context'
7
- import { makeId } from '../logic/utils'
7
+ import { makeId, updateCodeHighlightRange } from '../logic/utils'
8
8
 
9
9
  import 'shiki-magic-move/style.css'
10
10
 
11
11
  const props = defineProps<{
12
- stepsLz: string
13
12
  at?: string | number
13
+ stepsLz: string
14
+ stepRanges: string[][]
14
15
  }>()
15
16
 
16
17
  const steps = JSON.parse(lz.decompressFromBase64(props.stepsLz)) as KeyedTokensInfo[]
17
18
  const { $clicksContext: clicks, $scale: scale } = useSlideContext()
18
19
  const id = makeId()
19
- const index = ref(0)
20
+
21
+ const stepIndex = ref(0)
22
+ const container = ref<HTMLElement>()
23
+
24
+ // Normalized the ranges, to at least have one range
25
+ const ranges = computed(() => props.stepRanges.map(i => i.length ? i : ['all']))
20
26
 
21
27
  onUnmounted(() => {
22
28
  clicks!.unregister(id)
@@ -26,24 +32,68 @@ onMounted(() => {
26
32
  if (!clicks || clicks.disabled)
27
33
  return
28
34
 
29
- const { start, end, delta } = clicks.resolve(props.at || '+1', steps.length - 1)
35
+ if (ranges.value.length !== steps.length)
36
+ throw new Error('[slidev] The length of stepRanges does not match the length of steps, this is an internal error.')
37
+
38
+ const clickCounts = ranges.value.map(s => s.length).reduce((a, b) => a + b, 0)
39
+ const { start, end, delta } = clicks.resolve(props.at ?? '+1', clickCounts - 1)
30
40
  clicks.register(id, { max: end, delta })
31
41
 
32
- watchEffect(() => {
33
- if (clicks.disabled)
34
- index.value = steps.length - 1
35
- else
36
- index.value = Math.min(Math.max(0, clicks.current - start + 1), steps.length - 1)
37
- })
42
+ watch(
43
+ () => clicks.current,
44
+ () => {
45
+ // Calculate the step and rangeStr based on the current click count
46
+ const clickCount = clicks.current - start
47
+ let step = steps.length - 1
48
+ let _currentClickSum = 0
49
+ let rangeStr = 'all'
50
+ for (let i = 0; i < ranges.value.length; i++) {
51
+ const current = ranges.value[i]
52
+ if (clickCount < _currentClickSum + current.length - 1) {
53
+ step = i
54
+ rangeStr = current[clickCount - _currentClickSum + 1]
55
+ break
56
+ }
57
+ _currentClickSum += current.length || 1
58
+ }
59
+ stepIndex.value = step
60
+
61
+ const pre = container.value?.querySelector('.shiki') as HTMLElement
62
+ if (!pre)
63
+ return
64
+
65
+ const children = (Array.from(pre.children) as HTMLElement[])
66
+ .slice(1) // Remove the first anchor
67
+ .filter(i => !i.className.includes('shiki-magic-move-leave')) // Filter the leaving elements
68
+
69
+ // Group to lines between `<br>`
70
+ const lines = children.reduce((acc, el) => {
71
+ if (el.tagName === 'BR')
72
+ acc.push([])
73
+ else
74
+ acc[acc.length - 1].push(el)
75
+ return acc
76
+ }, [[]] as HTMLElement[][])
77
+
78
+ // Update highlight range
79
+ updateCodeHighlightRange(
80
+ rangeStr,
81
+ lines.length,
82
+ 1,
83
+ no => lines[no],
84
+ )
85
+ },
86
+ { immediate: true },
87
+ )
38
88
  })
39
89
  </script>
40
90
 
41
91
  <template>
42
- <div class="slidev-code-wrapper slidev-code-magic-move">
92
+ <div ref="container" class="slidev-code-wrapper slidev-code-magic-move relative">
43
93
  <ShikiMagicMovePrecompiled
44
94
  class="slidev-code relative shiki overflow-visible"
45
95
  :steps="steps"
46
- :step="index"
96
+ :step="stepIndex"
47
97
  :options="{ globalScale: scale }"
48
98
  />
49
99
  </div>
package/context.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { ref, shallowRef, toRef } from 'vue'
1
+ import { ref, toRef } from 'vue'
2
2
  import { injectLocal, objectOmit, provideLocal } from '@vueuse/core'
3
- import { useFixedClicks } from './composables/useClicks'
4
3
  import {
5
4
  FRONTMATTER_FIELDS,
6
5
  HEADMATTER_FIELDS,
@@ -13,15 +12,13 @@ import {
13
12
  injectionSlidevContext,
14
13
  } from './constants'
15
14
 
16
- const clicksContextFallback = shallowRef(useFixedClicks())
17
-
18
15
  /**
19
16
  * Get the current slide context, should be called inside the setup function of a component inside slide
20
17
  */
21
18
  export function useSlideContext() {
22
19
  const $slidev = injectLocal(injectionSlidevContext)!
23
20
  const $nav = toRef($slidev, 'nav')
24
- const $clicksContext = injectLocal(injectionClicksContext, clicksContextFallback)!.value
21
+ const $clicksContext = injectLocal(injectionClicksContext)!.value
25
22
  const $clicks = toRef($clicksContext, 'current')
26
23
  const $page = injectLocal(injectionCurrentPage)!
27
24
  const $renderContext = injectLocal(injectionRenderContext)!
package/env.ts CHANGED
@@ -1,15 +1,15 @@
1
- import { computed } from 'vue'
1
+ import { computed, ref } from 'vue'
2
2
  import { objectMap } from '@antfu/utils'
3
3
  import configs from '#slidev/configs'
4
4
 
5
5
  export { configs }
6
6
 
7
- export const slideAspect = configs.aspectRatio ?? (16 / 9)
8
- export const slideWidth = configs.canvasWidth ?? 980
7
+ export const slideAspect = ref(configs.aspectRatio ?? (16 / 9))
8
+ export const slideWidth = ref(configs.canvasWidth ?? 980)
9
9
 
10
10
  // To honor the aspect ratio more as possible, we need to approximate the height to the next integer.
11
11
  // Doing this, we will prevent on print, to create an additional empty white page after each page.
12
- export const slideHeight = Math.ceil(slideWidth / slideAspect)
12
+ export const slideHeight = computed(() => Math.ceil(slideWidth.value / slideAspect.value))
13
13
 
14
14
  export const themeVars = computed(() => {
15
15
  return objectMap(configs.themeConfig || {}, (k, v) => [`--slidev-theme-${k}`, v])
@@ -12,14 +12,14 @@ const props = defineProps<{
12
12
  }>()
13
13
 
14
14
  const width = computed(() => props.width)
15
- const height = computed(() => props.width / slideAspect)
15
+ const height = computed(() => props.width / slideAspect.value)
16
16
 
17
17
  const screenAspect = computed(() => width.value / height.value)
18
18
 
19
19
  const scale = computed(() => {
20
- if (screenAspect.value < slideAspect)
21
- return width.value / slideWidth
22
- return (height.value * slideAspect) / slideWidth
20
+ if (screenAspect.value < slideAspect.value)
21
+ return width.value / slideWidth.value
22
+ return (height.value * slideAspect.value) / slideWidth.value
23
23
  })
24
24
 
25
25
  // In print mode, the routes will never change. So we don't need reactivity here.
@@ -17,8 +17,8 @@ const { nav } = defineProps<{
17
17
  const route = computed(() => nav.currentSlideRoute.value)
18
18
 
19
19
  const style = computed(() => ({
20
- height: `${slideHeight}px`,
21
- width: `${slideWidth}px`,
20
+ height: `${slideHeight.value}px`,
21
+ width: `${slideWidth.value}px`,
22
22
  }))
23
23
 
24
24
  const DrawingPreview = shallowRef<any>()
@@ -25,7 +25,7 @@ const root = ref<HTMLDivElement>()
25
25
  const element = useElementSize(root)
26
26
 
27
27
  const width = computed(() => props.width ? props.width : element.width.value)
28
- const height = computed(() => props.width ? props.width / slideAspect : element.height.value)
28
+ const height = computed(() => props.width ? props.width / slideAspect.value : element.height.value)
29
29
 
30
30
  if (props.width) {
31
31
  watchEffect(() => {
@@ -41,14 +41,14 @@ const screenAspect = computed(() => width.value / height.value)
41
41
  const scale = computed(() => {
42
42
  if (props.scale && !isPrintMode.value)
43
43
  return props.scale
44
- if (screenAspect.value < slideAspect)
45
- return width.value / slideWidth
46
- return height.value * slideAspect / slideWidth
44
+ if (screenAspect.value < slideAspect.value)
45
+ return width.value / slideWidth.value
46
+ return height.value * slideAspect.value / slideWidth.value
47
47
  })
48
48
 
49
49
  const style = computed(() => ({
50
- 'height': `${slideHeight}px`,
51
- 'width': `${slideWidth}px`,
50
+ 'height': `${slideHeight.value}px`,
51
+ 'width': `${slideWidth.value}px`,
52
52
  'transform': `translate(-50%, -50%) scale(${scale.value})`,
53
53
  '--slidev-slide-scale': scale.value,
54
54
  }))
package/logic/nav.ts CHANGED
@@ -67,5 +67,5 @@ watch(
67
67
  await goLast()
68
68
  }
69
69
  },
70
- { flush: 'post', immediate: true },
70
+ { flush: 'pre', immediate: true },
71
71
  )
package/logic/utils.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { parseRangeString } from '@slidev/parser/core'
1
2
  import { useTimestamp } from '@vueuse/core'
2
3
  import { computed, ref } from 'vue'
4
+ import { CLASS_VCLICK_TARGET } from '../constants'
3
5
 
4
6
  export function useTimer() {
5
7
  const tsStart = ref(Date.now())
@@ -48,3 +50,25 @@ export function normalizeAtProp(at: string | number = '+1'): [isRelative: boolea
48
50
  n,
49
51
  ]
50
52
  }
53
+
54
+ export function updateCodeHighlightRange(
55
+ rangeStr: string,
56
+ linesCount: number,
57
+ startLine: number,
58
+ getTokenOfLine: (line: number) => Element[],
59
+ ) {
60
+ const highlights: number[] = parseRangeString(linesCount + startLine - 1, rangeStr)
61
+ for (let line = 0; line < linesCount; line++) {
62
+ const tokens = getTokenOfLine(line)
63
+ const isHighlighted = highlights.includes(line + startLine)
64
+ for (const token of tokens) {
65
+ token.classList.toggle(CLASS_VCLICK_TARGET, true)
66
+ token.classList.toggle('slidev-code-highlighted', isHighlighted)
67
+ token.classList.toggle('slidev-code-dishonored', !isHighlighted)
68
+
69
+ // for backward compatibility
70
+ token.classList.toggle('highlighted', isHighlighted)
71
+ token.classList.toggle('dishonored', !isHighlighted)
72
+ }
73
+ }
74
+ }
@@ -1,12 +1,13 @@
1
1
  import type { App } from 'vue'
2
- import { computed, reactive, ref } from 'vue'
2
+ import { computed, reactive, ref, shallowRef } from 'vue'
3
3
  import type { ComputedRef } from '@vue/reactivity'
4
4
  import type { configs } from '../env'
5
5
  import * as nav from '../logic/nav'
6
6
  import { isDark } from '../logic/dark'
7
- import { injectionCurrentPage, injectionRenderContext, injectionSlidevContext } from '../constants'
7
+ import { injectionClicksContext, injectionCurrentPage, injectionRenderContext, injectionSlidevContext } from '../constants'
8
8
  import { useContext } from '../composables/useContext'
9
9
  import type { SlidevContextNav } from '../composables/useNav'
10
+ import { useFixedClicks } from '../composables/useClicks'
10
11
 
11
12
  export interface SlidevContext {
12
13
  nav: SlidevContextNav
@@ -21,6 +22,7 @@ export function createSlidevContext() {
21
22
  app.provide(injectionRenderContext, ref('none'))
22
23
  app.provide(injectionSlidevContext, context)
23
24
  app.provide(injectionCurrentPage, computed(() => context.nav.currentSlideNo))
25
+ app.provide(injectionClicksContext, shallowRef(useFixedClicks()))
24
26
 
25
27
  // allows controls from postMessages
26
28
  if (__DEV__) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "0.48.0-beta.21",
4
+ "version": "0.48.0-beta.22",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -27,14 +27,14 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@antfu/utils": "^0.7.7",
30
- "@iconify-json/carbon": "^1.1.30",
30
+ "@iconify-json/carbon": "^1.1.31",
31
31
  "@iconify-json/ph": "^1.1.11",
32
32
  "@iconify-json/svg-spinners": "^1.1.2",
33
33
  "@shikijs/monaco": "^1.1.7",
34
34
  "@shikijs/vitepress-twoslash": "^1.1.7",
35
35
  "@slidev/rough-notation": "^0.1.0",
36
36
  "@typescript/ata": "^0.9.4",
37
- "@unhead/vue": "^1.8.10",
37
+ "@unhead/vue": "^1.8.11",
38
38
  "@unocss/reset": "^0.58.5",
39
39
  "@vueuse/core": "^10.9.0",
40
40
  "@vueuse/math": "^10.9.0",
@@ -52,13 +52,13 @@
52
52
  "prettier": "^3.2.5",
53
53
  "recordrtc": "^5.6.2",
54
54
  "shiki": "^1.1.7",
55
- "shiki-magic-move": "^0.1.0",
55
+ "shiki-magic-move": "^0.3.0",
56
56
  "typescript": "^5.3.3",
57
57
  "unocss": "^0.58.5",
58
58
  "vue": "^3.4.21",
59
59
  "vue-router": "^4.3.0",
60
- "@slidev/types": "0.48.0-beta.21",
61
- "@slidev/parser": "0.48.0-beta.21"
60
+ "@slidev/parser": "0.48.0-beta.22",
61
+ "@slidev/types": "0.48.0-beta.22"
62
62
  },
63
63
  "devDependencies": {
64
64
  "vite": "^5.1.4"
package/state/index.ts CHANGED
@@ -14,7 +14,7 @@ export const breakpoints = useBreakpoints({
14
14
  })
15
15
  export const windowSize = useWindowSize()
16
16
  export const magicKeys = useMagicKeys()
17
- export const isScreenVertical = computed(() => windowSize.height.value - windowSize.width.value / slideAspect > 120)
17
+ export const isScreenVertical = computed(() => windowSize.height.value - windowSize.width.value / slideAspect.value > 120)
18
18
  export const fullscreen = useFullscreen(isClient ? document.body : null)
19
19
 
20
20
  export const activeElement = useActiveElement()
package/styles/code.css CHANGED
@@ -44,9 +44,9 @@ html:not(.dark) .shiki span {
44
44
  overflow: auto;
45
45
  }
46
46
 
47
- .slidev-code .line.highlighted {
47
+ .slidev-code .slidev-code-highlighted {
48
48
  }
49
- .slidev-code .line.dishonored {
49
+ .slidev-code .slidev-code-dishonored {
50
50
  opacity: 0.3;
51
51
  pointer-events: none;
52
52
  }