@slidev/client 52.8.0 → 52.9.1

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.
@@ -84,7 +84,7 @@ useEventListener('keypress', (e) => {
84
84
  keyboardBuffer.value += String(num)
85
85
 
86
86
  // beyond the number of slides, reset
87
- if (+keyboardBuffer.value >= slides.value.length) {
87
+ if (+keyboardBuffer.value > slides.value.length) {
88
88
  keyboardBuffer.value = ''
89
89
  return
90
90
  }
@@ -1,5 +1,4 @@
1
1
  <script setup lang="ts">
2
- import { getHighlighter } from '#slidev/shiki'
3
2
  import { ref, shallowRef } from 'vue'
4
3
  import { useIME } from '../composables/useIME'
5
4
 
@@ -11,14 +10,21 @@ const { composingContent, onInput, onCompositionEnd } = useIME(content)
11
10
 
12
11
  const textareaEl = ref<HTMLTextAreaElement | null>(null)
13
12
 
14
- const highlight = shallowRef<Awaited<ReturnType<typeof getHighlighter>> | null>(null)
15
- getHighlighter().then(h => highlight.value = h)
13
+ const highlight = shallowRef<((code: string) => string) | null>(null)
14
+ import('../setup/shiki').then(async (m) => {
15
+ const { getEagerHighlighter, defaultHighlightOptions } = await m.default()
16
+ const highlighter = await getEagerHighlighter()
17
+ highlight.value = (code: string) => highlighter.codeToHtml(code, {
18
+ ...defaultHighlightOptions,
19
+ lang: 'markdown',
20
+ })
21
+ })
16
22
  </script>
17
23
 
18
24
  <template>
19
25
  <div class="absolute left-3 right-0 inset-y-2 font-mono overflow-x-hidden overflow-y-auto cursor-text">
20
26
  <div v-if="highlight" class="relative w-full h-max min-h-full">
21
- <div class="relative w-full h-max" v-html="`${highlight(composingContent, 'markdown')}&nbsp;`" />
27
+ <div class="relative w-full h-max" v-html="`${highlight(composingContent)}&nbsp;`" />
22
28
  <textarea
23
29
  ref="textareaEl" v-model="composingContent" :placeholder="props.placeholder"
24
30
  class="absolute inset-0 resize-none text-transparent bg-transparent focus:outline-none caret-black dark:caret-white overflow-y-hidden"
@@ -52,7 +52,6 @@ const contentStyle = computed(() => ({
52
52
  ...props.contentStyle,
53
53
  'height': `${slideHeight.value}px`,
54
54
  'width': `${slideWidth.value}px`,
55
- 'transform': `translate(-50%, -50%) scale(${scale.value})`,
56
55
  '--slidev-slide-scale': scale.value,
57
56
  }))
58
57
 
@@ -117,6 +116,8 @@ const snapshot = computed(() => {
117
116
  }
118
117
 
119
118
  .slidev-slide-content {
119
+ --slidev-slide-container-scale: var(--slidev-slide-scale);
120
+ transform: translate(-50%, -50%) scale(var(--slidev-slide-scale));
120
121
  @apply absolute left-1/2 top-1/2 overflow-hidden bg-main;
121
122
  }
122
123
  </style>
@@ -32,19 +32,9 @@ provideLocal(injectionRenderContext, ref(props.renderContext))
32
32
  provideLocal(injectionClicksContext, toRef(props, 'clicksContext'))
33
33
  provideLocal(injectionSlideZoom, zoom)
34
34
 
35
- const zoomStyle = computed(() => {
36
- return zoom.value === 1
37
- ? undefined
38
- : {
39
- width: `${100 / zoom.value}%`,
40
- height: `${100 / zoom.value}%`,
41
- transformOrigin: 'top left',
42
- transform: `scale(${zoom.value})`,
43
- }
44
- })
45
35
  const style = computed<CSSProperties>(() => ({
46
- ...zoomStyle.value,
47
36
  'user-select': configs.selectable ? undefined : 'none',
37
+ '--slidev-slide-zoom-scale': zoom.value === 1 ? undefined : zoom.value,
48
38
  }))
49
39
  </script>
50
40
 
@@ -69,5 +59,14 @@ const style = computed<CSSProperties>(() => ({
69
59
  .slidev-page {
70
60
  position: absolute;
71
61
  inset: 0;
62
+
63
+ /* Zoom handling */
64
+ --slidev-slide-zoom-scale: 1;
65
+ width: calc(100% / var(--slidev-slide-zoom-scale));
66
+ height: calc(100% / var(--slidev-slide-zoom-scale));
67
+ transform-origin: top left;
68
+ scale: var(--slidev-slide-zoom-scale);
69
+ /* slide scale = container scale * zoom scale */
70
+ --slidev-slide-scale: calc(var(--slidev-slide-container-scale) * var(--slidev-slide-zoom-scale));
72
71
  }
73
72
  </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "52.8.0",
4
+ "version": "52.9.1",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  "@vueuse/core": "^14.0.0",
43
43
  "@vueuse/math": "^14.0.0",
44
44
  "@vueuse/motion": "^3.0.3",
45
+ "ansis": "^4.2.0",
45
46
  "drauu": "^0.4.3",
46
47
  "file-saver": "^2.0.5",
47
48
  "floating-vue": "^5.2.2",
@@ -61,8 +62,8 @@
61
62
  "vue": "^3.5.24",
62
63
  "vue-router": "^4.6.3",
63
64
  "yaml": "^2.8.1",
64
- "@slidev/parser": "52.8.0",
65
- "@slidev/types": "52.8.0"
65
+ "@slidev/parser": "52.9.1",
66
+ "@slidev/types": "52.9.1"
66
67
  },
67
68
  "devDependencies": {
68
69
  "vite": "^7.2.2"
@@ -1,6 +1,6 @@
1
1
  import type { CodeRunner, CodeRunnerOutput, CodeRunnerOutputs, CodeRunnerOutputText } from '@slidev/types'
2
+ import type { CodeToHastOptions } from 'shiki'
2
3
  import type ts from 'typescript'
3
-
4
4
  import deps from '#slidev/monaco-run-deps'
5
5
  import setups from '#slidev/setups/code-runners'
6
6
  import { createSingletonPromise } from '@antfu/utils'
@@ -15,8 +15,16 @@ export default createSingletonPromise(async () => {
15
15
  ts: runTypeScript,
16
16
  }
17
17
 
18
- const { getHighlighter } = await import('#slidev/shiki')
19
- const highlight = await getHighlighter()
18
+ const { defaultHighlightOptions, getEagerHighlighter } = await (await import('./shiki')).default()
19
+
20
+ const highlighter = await getEagerHighlighter()
21
+ const highlight = (code: string, lang: string, options?: Partial<CodeToHastOptions>) => {
22
+ return highlighter.codeToHtml(code, {
23
+ ...defaultHighlightOptions,
24
+ lang,
25
+ ...options,
26
+ })
27
+ }
20
28
 
21
29
  const run = async (code: string, lang: string, options: Record<string, unknown>): Promise<CodeRunnerOutputs> => {
22
30
  try {
package/setup/monaco.ts CHANGED
@@ -2,6 +2,7 @@ import type { MonacoSetupReturn } from '@slidev/types'
2
2
  import configs from '#slidev/configs'
3
3
  import setups from '#slidev/setups/monaco'
4
4
  import { createSingletonPromise } from '@antfu/utils'
5
+ import { shikiToMonaco } from '@shikijs/monaco'
5
6
  import { setupTypeAcquisition } from '@typescript/ata'
6
7
  import * as monaco from 'monaco-editor'
7
8
 
@@ -83,14 +84,16 @@ const setup = createSingletonPromise(async () => {
83
84
  })
84
85
  : () => { }
85
86
 
87
+ const { getEagerHighlighter, languageNames, themeOption } = await (await import('./shiki')).default()
88
+
86
89
  monaco.languages.register({ id: 'vue' })
87
90
  monaco.languages.register({ id: 'html' })
88
91
  monaco.languages.register({ id: 'css' })
89
92
  monaco.languages.register({ id: 'typescript' })
90
93
  monaco.languages.register({ id: 'javascript' })
91
-
92
- const { shiki, languages, themes, shikiToMonaco } = await import('#slidev/shiki')
93
- const highlighter = await shiki
94
+ for (const lang of languageNames) {
95
+ monaco.languages.register({ id: lang })
96
+ }
94
97
 
95
98
  const editorOptions: MonacoSetupReturn['editorOptions'] & object = {}
96
99
  for (const setup of setups) {
@@ -111,21 +114,18 @@ const setup = createSingletonPromise(async () => {
111
114
  })
112
115
 
113
116
  // Use Shiki to highlight Monaco
117
+ const highlighter = await getEagerHighlighter()
114
118
  shikiToMonaco(highlighter, monaco)
115
- if (typeof themes === 'string') {
116
- monaco.editor.setTheme(themes)
119
+ if (typeof themeOption === 'string') {
120
+ monaco.editor.setTheme(themeOption)
117
121
  }
118
122
  else {
119
123
  watchEffect(() => {
120
124
  monaco.editor.setTheme(isDark.value
121
- ? themes.dark || 'vitesse-dark'
122
- : themes.light || 'vitesse-light')
125
+ ? themeOption.dark || 'vitesse-dark'
126
+ : themeOption.light || 'vitesse-light')
123
127
  })
124
128
  }
125
- // Register all languages, otherwise Monaco will not highlight them
126
- for (const lang of languages) {
127
- monaco.languages.register({ id: lang })
128
- }
129
129
 
130
130
  return {
131
131
  monaco,
@@ -0,0 +1,125 @@
1
+ // This module also runs in the Node.js environment
2
+
3
+ import type { ResolvedSlidevUtils, ShikiContext, ShikiSetupReturn } from '@slidev/types'
4
+ import type { LanguageInput, ThemeInput, ThemeRegistrationAny } from 'shiki'
5
+ import { objectMap } from '@antfu/utils'
6
+ import { red, yellow } from 'ansis'
7
+ import { bundledLanguages, bundledThemes } from 'shiki'
8
+
9
+ export const shikiContext: ShikiContext = {
10
+ /** @deprecated */
11
+ loadTheme() {
12
+ throw new Error('`loadTheme` is no longer supported.')
13
+ },
14
+ }
15
+
16
+ export function resolveShikiOptions(options: (ShikiSetupReturn | void)[]) {
17
+ const mergedOptions: Record<string, any> = Object.assign({}, ...options)
18
+
19
+ if ('theme' in mergedOptions && 'themes' in mergedOptions)
20
+ delete mergedOptions.theme
21
+
22
+ // Rename theme to themes when provided in multiple themes format, but exclude when it's a theme object.
23
+ if (mergedOptions.theme && typeof mergedOptions.theme !== 'string' && !mergedOptions.theme.name && !mergedOptions.theme.tokenColors) {
24
+ mergedOptions.themes = mergedOptions.theme
25
+ delete mergedOptions.theme
26
+ }
27
+
28
+ // No theme at all, apply the default
29
+ if (!mergedOptions.theme && !mergedOptions.themes) {
30
+ mergedOptions.themes = {
31
+ dark: 'vitesse-dark',
32
+ light: 'vitesse-light',
33
+ }
34
+ }
35
+
36
+ if (mergedOptions.themes)
37
+ mergedOptions.defaultColor = false
38
+
39
+ const themeOption = extractThemeName(mergedOptions.theme) || extractThemeNames(mergedOptions.themes || {})
40
+ const themeNames = typeof themeOption === 'string' ? [themeOption] : Object.values(themeOption)
41
+
42
+ const themeInput: Record<string, ThemeInput> = Object.assign({}, bundledThemes)
43
+ if (typeof mergedOptions.theme === 'object' && mergedOptions.theme?.name) {
44
+ themeInput[mergedOptions.theme.name] = mergedOptions.theme
45
+ }
46
+ if (mergedOptions.themes) {
47
+ for (const theme of Object.values<ThemeRegistrationAny | string>(mergedOptions.themes)) {
48
+ if (typeof theme === 'object' && theme?.name) {
49
+ themeInput[theme.name] = theme
50
+ }
51
+ }
52
+ }
53
+
54
+ const languageNames = new Set<string>(['markdown', 'vue', 'javascript', 'typescript', 'html', 'css'])
55
+ const languageInput: Record<string, LanguageInput> = Object.assign({}, bundledLanguages)
56
+ for (const option of options) {
57
+ const langs = option?.langs
58
+ if (langs == null)
59
+ continue
60
+ if (Array.isArray(langs)) {
61
+ for (const lang of langs.flat()) {
62
+ if (typeof lang === 'function') {
63
+ console.error(red('[slidev] `langs` option returned by setup/shiki.ts cannot be an array containing functions. Please use the record format (`{ [name]: () => {...} }`) instead.'))
64
+ }
65
+ else if (typeof lang === 'string') {
66
+ // a name of a Shiki built-in language
67
+ // which can be loaded on demand without overhead, so all built-in languages are available.
68
+ // Only need to include them explicitly in browser environment.
69
+ languageNames.add(lang)
70
+ }
71
+ else if (lang.name) {
72
+ // a custom grammar object
73
+ languageNames.add(lang.name)
74
+ languageInput[lang.name] = lang
75
+ for (const alias of lang.aliases || []) {
76
+ languageNames.add(alias)
77
+ languageInput[alias] = lang
78
+ }
79
+ }
80
+ else {
81
+ console.error(red('[slidev] Invalid lang option in shiki setup:'), lang)
82
+ }
83
+ }
84
+ }
85
+ else if (typeof langs === 'object') {
86
+ // a map from name to loader or grammar object
87
+ for (const name of Object.keys(langs))
88
+ languageNames.add(name)
89
+ Object.assign(languageInput, langs)
90
+ }
91
+ else {
92
+ console.error(red('[slidev] Invalid langs option in shiki setup:'), langs)
93
+ }
94
+ }
95
+
96
+ return {
97
+ options: mergedOptions as ResolvedSlidevUtils['shikiOptions'],
98
+ themeOption,
99
+ themeNames,
100
+ themeInput,
101
+ languageNames,
102
+ languageInput,
103
+ }
104
+ }
105
+
106
+ function extractThemeName(theme?: ThemeRegistrationAny | string): string | undefined {
107
+ if (!theme)
108
+ return undefined
109
+ if (typeof theme === 'string')
110
+ return theme
111
+ if (!theme.name)
112
+ console.warn(yellow('[slidev] Theme'), theme, yellow('does not have a name, which may cause issues.'))
113
+ return theme.name
114
+ }
115
+
116
+ function extractThemeNames(themes?: Record<string, ThemeRegistrationAny | string>): Record<string, string> {
117
+ if (!themes)
118
+ return {}
119
+ return objectMap(themes, (key, theme) => {
120
+ const name = extractThemeName(theme)
121
+ if (!name)
122
+ return undefined
123
+ return [key, name]
124
+ })
125
+ }
package/setup/shiki.ts ADDED
@@ -0,0 +1,30 @@
1
+ import setups from '#slidev/setups/shiki'
2
+ import { createSingletonPromise } from '@antfu/utils'
3
+ import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript'
4
+ import { createdBundledHighlighter, createSingletonShorthands } from 'shiki/core'
5
+ import { resolveShikiOptions, shikiContext } from './shiki-options'
6
+
7
+ export default createSingletonPromise(async () => {
8
+ const { options, languageNames, languageInput, themeOption, themeNames, themeInput } = resolveShikiOptions(await Promise.all(setups.map(setup => setup(shikiContext))))
9
+
10
+ const createHighlighter = createdBundledHighlighter<string, string>({
11
+ engine: createJavaScriptRegexEngine,
12
+ langs: languageInput,
13
+ themes: themeInput,
14
+ })
15
+ const shorthands = createSingletonShorthands(createHighlighter)
16
+ const getEagerHighlighter = createSingletonPromise(() => shorthands.getSingletonHighlighter({
17
+ ...options,
18
+ langs: [...languageNames],
19
+ themes: themeNames,
20
+ }))
21
+
22
+ return {
23
+ defaultHighlightOptions: options,
24
+ getEagerHighlighter,
25
+ shorthands,
26
+ languageNames,
27
+ themeNames,
28
+ themeOption,
29
+ }
30
+ })
@@ -12,22 +12,22 @@
12
12
 
13
13
  .slide-left-enter-from,
14
14
  .slide-right-leave-to {
15
- transform: translateX(100%);
15
+ translate: 100% 0;
16
16
  }
17
17
 
18
18
  .slide-left-leave-to,
19
19
  .slide-right-enter-from {
20
- transform: translateX(-100%);
20
+ translate: -100% 0;
21
21
  }
22
22
 
23
23
  .slide-up-enter-from,
24
24
  .slide-down-leave-to {
25
- transform: translateY(100%);
25
+ translate: 0 100%;
26
26
  }
27
27
 
28
28
  .slide-up-leave-to,
29
29
  .slide-down-enter-from {
30
- transform: translateY(-100%);
30
+ translate: 0 -100%;
31
31
  }
32
32
 
33
33
  /* Fading */