valaxy 1.0.0-beta.2 → 1.0.0-rc.10

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/bin/valaxy.mjs CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
+ import process from 'node:process'
4
5
  import { run } from '../dist/node/cli/index.mjs'
5
6
 
6
- function main() {
7
- run()
7
+ async function main() {
8
+ await run()
8
9
  }
9
10
 
10
- main()
11
+ main().catch((error) => {
12
+ console.error(error)
13
+ process.exitCode = 1
14
+ })
@@ -103,7 +103,7 @@ const hasWarning = computed(() => isClient && location.protocol !== 'https:')
103
103
  </div>
104
104
  <div v-else>
105
105
  <ValaxyDeprecatedContent :html="decryptedContent" />
106
- <div w-full text-center mt-8>
106
+ <div class="decrypt-action mb-8 sm:mb-0" w-full text-center mt-8>
107
107
  <button m-auto class="btn" font-bold @click="encryptAgain">
108
108
  Encrypt Again
109
109
  </button>
@@ -65,7 +65,7 @@ defineExpose({
65
65
  </div>
66
66
  <div v-else>
67
67
  <slot :photos="photos" />
68
- <div w-full text-center mt-8>
68
+ <div class="decrypt-action mb-8 sm:mb-0" w-full text-center mt-8>
69
69
  <button m-auto class="btn" font-bold @click="encryptAgain">
70
70
  Encrypt Again
71
71
  </button>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import type { Post } from 'valaxy'
3
- import { onContentUpdated, runContentUpdated, useCodePen, useCollapseCode, useCopyCode, useMediumZoom, wrapTable } from 'valaxy'
3
+ import { onContentUpdated, runContentUpdated, setIframeAspectRatios, useCodePen, useCollapseCode, useCopyCode, useMediumZoom, wrapTable } from 'valaxy'
4
4
  import { onMounted, onUpdated, ref } from 'vue'
5
5
  import { useI18n } from 'vue-i18n'
6
6
  import { useCodeGroups } from '../composables/codeGroups'
@@ -13,9 +13,14 @@ const props = defineProps<{
13
13
 
14
14
  const { t } = useI18n()
15
15
 
16
- const contentRef = ref()
16
+ const contentRef = ref<HTMLElement>()
17
17
  onContentUpdated(() => {
18
- wrapTable(contentRef.value)
18
+ const content = contentRef.value
19
+ if (!content)
20
+ return
21
+
22
+ wrapTable(content)
23
+ setIframeAspectRatios(content)
19
24
  })
20
25
 
21
26
  onMounted(() => {
@@ -41,8 +46,8 @@ useVanillaLazyLoad()
41
46
  </script>
42
47
 
43
48
  <template>
44
- <article v-if="$slots.default" :class="frontmatter.markdownClass || 'markdown-body'">
45
- <slot ref="contentRef" @vue:updated="runContentUpdated" />
49
+ <article v-if="$slots.default" ref="contentRef" :class="frontmatter.markdownClass || 'markdown-body'">
50
+ <slot @vue:updated="runContentUpdated" />
46
51
 
47
52
  <div v-if="frontmatter.url" text="center">
48
53
  <a
@@ -75,4 +80,12 @@ useVanillaLazyLoad()
75
80
  .medium-zoom-image--opened {
76
81
  z-index: 999;
77
82
  }
83
+
84
+ // Keep dimensioned embeds proportional when their width is constrained by the
85
+ // article. :where() deliberately leaves explicit author styles in control.
86
+ :where(iframe[data-va-responsive-iframe]) {
87
+ max-width: 100%;
88
+ height: auto;
89
+ aspect-ratio: var(--va-iframe-aspect-ratio);
90
+ }
78
91
  </style>
@@ -5,10 +5,11 @@ import { useI18n } from 'vue-i18n'
5
5
  import { useRoute } from 'vue-router'
6
6
  import { useFrontmatter, useLocale, useValaxyHead, useValaxyI18n } from '../../composables'
7
7
  import { useTimezone } from '../../composables/global'
8
+ import { useSiteConfig } from '../../config'
9
+ import { resolveSiteUrl } from '../../utils'
8
10
  // https://github.com/vueuse/head
9
11
  // you can use this to manipulate the document head in any components,
10
12
  // they will be rendered correctly in the html results with vite-ssg
11
- import { useSiteConfig } from '../../config'
12
13
 
13
14
  export function useValaxyApp() {
14
15
  const siteConfig = useSiteConfig()
@@ -27,8 +28,16 @@ export function useValaxyApp() {
27
28
  }
28
29
 
29
30
  // seo
30
- const siteUrl = computed(() => fm.value.url || siteConfig.value.url)
31
+ const siteUrl = computed(() => resolveSiteUrl(
32
+ siteConfig.value.url,
33
+ fm.value.url || route.path,
34
+ ))
31
35
  const description = computed(() => $tO(fm.value.excerpt) || $tO(fm.value.description) || $t(siteConfig.value.description))
36
+ const ogImage = computed(() => resolveSiteUrl(
37
+ siteConfig.value.url,
38
+ fm.value.ogImage || fm.value.cover || fm.value.firstImage || siteConfig.value.favicon,
39
+ ))
40
+ const seoTitle = computed(() => $tO(fm.value.title) || $t(siteConfig.value.title))
32
41
 
33
42
  useSeoMeta({
34
43
  description,
@@ -36,10 +45,15 @@ export function useValaxyApp() {
36
45
  ogLocale: computed(() => locale.value || fm.value.lang || siteConfig.value.lang || 'en'),
37
46
  ogLocaleAlternate: computed(() => siteConfig.value.languages.filter(l => l !== locale.value)),
38
47
  ogSiteName: computed(() => $t(siteConfig.value.title)),
39
- ogTitle: computed(() => $tO(fm.value.title) || $t(siteConfig.value.title)),
40
- ogImage: computed(() => fm.value.ogImage || fm.value.cover || fm.value.firstImage || siteConfig.value.favicon),
48
+ ogTitle: seoTitle,
49
+ ogImage,
50
+ ogImageAlt: seoTitle,
41
51
  ogType: 'website',
42
52
  ogUrl: siteUrl,
53
+ twitterCard: 'summary_large_image',
54
+ twitterDescription: description,
55
+ twitterImage: ogImage,
56
+ twitterTitle: seoTitle,
43
57
  })
44
58
 
45
59
  // for SEO
@@ -48,7 +62,7 @@ export function useValaxyApp() {
48
62
  // Personal Website or Blog
49
63
  definePerson({
50
64
  name: $t(siteConfig.value.author.name),
51
- url: siteUrl.value,
65
+ url: siteConfig.value.url,
52
66
  image: $t(siteConfig.value.author.avatar),
53
67
  sameAs: siteConfig.value.social.map(s => s.link),
54
68
  }),
@@ -1,16 +1,27 @@
1
1
  import { useHead } from '@unhead/vue'
2
2
  import { computed, onMounted } from 'vue'
3
+ import { useRoute } from 'vue-router'
3
4
  import pkg from '../../../package.json' with { type: 'json' }
4
5
 
5
6
  import { useFrontmatter, useValaxyI18n } from '../../composables'
6
7
  import { useSiteConfig } from '../../config'
8
+ import { resolveSiteUrl, withBase } from '../../utils'
7
9
 
8
10
  export function useValaxyHead() {
9
11
  const { $t, $tO, locale } = useValaxyI18n()
10
12
 
11
13
  const fm = useFrontmatter()
14
+ const route = useRoute()
12
15
  const siteConfig = useSiteConfig()
13
16
  const $title = computed(() => $tO(fm.value.title))
17
+ const canonicalUrl = computed(() => {
18
+ const siteUrl = siteConfig.value.url
19
+ if (!/^https?:\/\//.test(siteUrl))
20
+ return ''
21
+
22
+ const pagePath = fm.value.url || route.path
23
+ return resolveSiteUrl(siteUrl, pagePath)
24
+ })
14
25
 
15
26
  useHead({
16
27
  htmlAttrs: {
@@ -24,7 +35,7 @@ export function useValaxyHead() {
24
35
  link: [
25
36
  {
26
37
  rel: 'icon',
27
- href: siteConfig.value.favicon,
38
+ href: withBase(siteConfig.value.favicon),
28
39
  type: siteConfig.value.favicon?.endsWith('svg') ? 'image/svg+xml' : 'image/png',
29
40
  },
30
41
  ],
@@ -48,6 +59,10 @@ export function useValaxyHead() {
48
59
  },
49
60
  })
50
61
 
62
+ useHead(computed(() => canonicalUrl.value
63
+ ? { link: [{ rel: 'canonical', href: canonicalUrl.value }] }
64
+ : {}))
65
+
51
66
  // Add mac detection class on client side only, after hydration
52
67
  // to avoid SSR/client mismatch on the <html> element
53
68
  onMounted(() => {
@@ -1,6 +1,8 @@
1
+ import type { Zoom, ZoomSelector } from 'medium-zoom'
2
+ import type { MaybeRefOrGetter } from 'vue'
1
3
  import mediumZoom from 'medium-zoom'
2
4
  import { useSiteConfig } from 'valaxy'
3
- import { onMounted } from 'vue'
5
+ import { computed, onBeforeUnmount, onMounted, toValue } from 'vue'
4
6
 
5
7
  interface ZoomRectLike {
6
8
  left: number
@@ -97,55 +99,70 @@ function freezeOpenedImage(element: HTMLElement): SavedZoomImageStyle | null {
97
99
  /**
98
100
  * @description image preview by medium-zoom
99
101
  */
100
- export function useMediumZoom() {
102
+ export function useMediumZoom(selector?: MaybeRefOrGetter<ZoomSelector | null | undefined>) {
101
103
  const siteConfig = useSiteConfig()
102
- const mediumZoomConfig = siteConfig.value.mediumZoom
103
-
104
- if (mediumZoomConfig.enable) {
105
- onMounted(() => {
106
- const zoom = mediumZoom(
107
- mediumZoomConfig.selector || '.markdown-body img',
108
- {
109
- background: 'var(--medium-zoom-c-bg, rgba(0, 0, 0, 0.8))',
110
- ...mediumZoomConfig.options,
111
- },
112
- )
113
-
114
- let savedStyles: SavedZoomImageStyle[] = []
115
-
116
- zoom.on('opened', () => {
117
- savedStyles = Array
118
- .from(document.querySelectorAll<HTMLElement>('.medium-zoom-image--opened'))
119
- .map(freezeOpenedImage)
120
- .filter((style): style is SavedZoomImageStyle => style !== null)
104
+ const enabled = computed(() => siteConfig.value.mediumZoom.enable)
105
+ let savedStyles: SavedZoomImageStyle[] = []
106
+ let zoom: Zoom | undefined
107
+
108
+ function restoreSavedStyles() {
109
+ if (!savedStyles.length)
110
+ return
111
+
112
+ for (const style of savedStyles) {
113
+ const { element } = style
114
+ if (!document.body.contains(element))
115
+ continue
116
+
117
+ applyWithoutTransformTransition(element, () => {
118
+ element.style.transform = style.transform
119
+ element.style.width = style.width
120
+ element.style.height = style.height
121
121
  })
122
122
 
123
- zoom.on('close', () => {
124
- if (!savedStyles.length)
125
- return
126
-
127
- for (const style of savedStyles) {
128
- const { element } = style
129
- if (!document.body.contains(element))
130
- continue
131
-
132
- // Restore original medium-zoom styles (without transition) so the element
133
- // is back to the state medium-zoom expects for its close animation.
134
- applyWithoutTransformTransition(element, () => {
135
- element.style.transform = style.transform
136
- element.style.width = style.width
137
- element.style.height = style.height
138
- })
139
-
140
- // medium-zoom sets `transform = ''` BEFORE dispatching the close event,
141
- // but we just overwrote it above. Re-apply `''` to actually trigger the
142
- // close transition; otherwise transitionend never fires and the cloned
143
- // image element is never removed from the DOM.
144
- element.style.transform = ''
145
- }
146
-
147
- savedStyles = []
148
- })
123
+ // medium-zoom clears transform before emitting close. Restore that state
124
+ // after replacing the frozen dimensions so its transition can finish.
125
+ element.style.transform = ''
126
+ }
127
+
128
+ savedStyles = []
129
+ }
130
+
131
+ function handleOpened() {
132
+ savedStyles = Array
133
+ .from(document.querySelectorAll<HTMLElement>('.medium-zoom-image--opened'))
134
+ .map(freezeOpenedImage)
135
+ .filter((style): style is SavedZoomImageStyle => style !== null)
136
+ }
137
+
138
+ onMounted(() => {
139
+ if (!enabled.value)
140
+ return
141
+
142
+ const mediumZoomConfig = siteConfig.value.mediumZoom
143
+ const target = selector === undefined
144
+ ? mediumZoomConfig.selector || '.markdown-body img'
145
+ : toValue(selector)
146
+ if (!target)
147
+ return
148
+
149
+ zoom = mediumZoom(target, {
150
+ background: 'var(--medium-zoom-c-bg, rgba(0, 0, 0, 0.8))',
151
+ ...mediumZoomConfig.options,
149
152
  })
153
+ zoom.on('opened', handleOpened)
154
+ zoom.on('close', restoreSavedStyles)
155
+ })
156
+
157
+ onBeforeUnmount(() => {
158
+ restoreSavedStyles()
159
+ zoom?.off('opened', handleOpened)
160
+ zoom?.off('close', restoreSavedStyles)
161
+ zoom?.detach()
162
+ zoom = undefined
163
+ })
164
+
165
+ return {
166
+ enabled,
150
167
  }
151
168
  }
@@ -15,7 +15,6 @@ export * from './features'
15
15
  export * from './helper'
16
16
  export * from './layout'
17
17
  export * from './locale'
18
-
19
18
  export * from './outline'
20
19
  export * from './post'
21
20
 
package/client/index.html CHANGED
@@ -2,11 +2,11 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
5
  <!-- head -->
7
6
  </head>
8
7
  <body>
9
8
  <div id="app"></div>
9
+ <div id="valaxy-teleports"></div>
10
10
  <script type="module" src="./main.ts"></script>
11
11
  <!-- body -->
12
12
  </body>
@@ -10,10 +10,6 @@ a {
10
10
  font-weight: 500;
11
11
  }
12
12
 
13
- * {
14
- outline: none;
15
- }
16
-
17
13
  hr {
18
14
  margin: 1rem;
19
15
  }
@@ -0,0 +1,34 @@
1
+ const RESPONSIVE_IFRAME_ATTRIBUTE = 'data-va-responsive-iframe'
2
+ const IFRAME_ASPECT_RATIO_PROPERTY = '--va-iframe-aspect-ratio'
3
+
4
+ function parsePositiveDimension(value: string | null) {
5
+ if (value === null)
6
+ return
7
+
8
+ const dimension = Number(value)
9
+ if (Number.isFinite(dimension) && dimension > 0)
10
+ return dimension
11
+ }
12
+
13
+ /**
14
+ * Make dimensioned iframes responsive without assuming a fixed video ratio.
15
+ * The generated custom property is consumed by the low-specificity markdown
16
+ * styles, so an author's explicit height or aspect-ratio still takes priority.
17
+ */
18
+ export function setIframeAspectRatios(container: ParentNode = document) {
19
+ container.querySelectorAll<HTMLIFrameElement>('iframe').forEach((iframe) => {
20
+ const width = parsePositiveDimension(iframe.getAttribute('width'))
21
+ const height = parsePositiveDimension(iframe.getAttribute('height'))
22
+
23
+ if (width && height) {
24
+ iframe.setAttribute(RESPONSIVE_IFRAME_ATTRIBUTE, '')
25
+ iframe.style.setProperty(IFRAME_ASPECT_RATIO_PROPERTY, `${width} / ${height}`)
26
+ return
27
+ }
28
+
29
+ if (iframe.hasAttribute(RESPONSIVE_IFRAME_ATTRIBUTE)) {
30
+ iframe.removeAttribute(RESPONSIVE_IFRAME_ATTRIBUTE)
31
+ iframe.style.removeProperty(IFRAME_ASPECT_RATIO_PROPERTY)
32
+ }
33
+ })
34
+ }
@@ -3,6 +3,8 @@ export * from './cdn'
3
3
  export * from './code'
4
4
  export * from './content'
5
5
  export * from './helper'
6
+ export * from './iframe'
7
+ export * from './path'
6
8
  export * from './router'
7
9
  export * from './time'
8
10
  export * from './types'
@@ -0,0 +1,29 @@
1
+ import { EXTERNAL_URL_RE } from '../../shared'
2
+
3
+ /**
4
+ * Join two URL paths while resolving a slash collision.
5
+ */
6
+ function joinPath(base: string, path: string): string {
7
+ return `${base}${path}`.replace(/\/+/g, '/')
8
+ }
9
+
10
+ /**
11
+ * Append Vite's resolved base to an internal root-absolute URL.
12
+ * External and relative URLs are returned unchanged.
13
+ */
14
+ export function withBase(path: string): string {
15
+ return EXTERNAL_URL_RE.test(path) || !path.startsWith('/')
16
+ ? path
17
+ : joinPath(import.meta.env.BASE_URL, path)
18
+ }
19
+
20
+ /**
21
+ * Resolve an internal path against the configured canonical site URL.
22
+ * Root-absolute paths remain relative to a site URL's deployment subpath.
23
+ */
24
+ export function resolveSiteUrl(siteUrl: string, path: string): string {
25
+ if (!path || EXTERNAL_URL_RE.test(path) || !/^https?:\/\//.test(siteUrl))
26
+ return path
27
+
28
+ return new URL(path.replace(/^\/+/, ''), `${siteUrl.replace(/\/+$/, '')}/`).href
29
+ }
@@ -1,4 +1,3 @@
1
- import * as yargs from 'yargs';
2
1
  import { Argv } from 'yargs';
3
2
  import { LogLevel, ViteDevServer } from 'vite';
4
3
 
@@ -11,7 +10,11 @@ declare function startValaxyDev({ root, port, remote, log, open, }: {
11
10
  }): Promise<ViteDevServer>;
12
11
  declare function registerDevCommand(cli: Argv): void;
13
12
 
14
- declare const cli: yargs.Argv<{}>;
15
- declare function run(): void;
13
+ interface CreateCliOptions {
14
+ userRoot?: string;
15
+ }
16
+ declare function createCli(argv?: string[], options?: CreateCliOptions): Promise<Argv<object>>;
17
+ declare function run(argv?: string[], options?: CreateCliOptions): Promise<void>;
16
18
 
17
- export { cli, registerDevCommand, run, startValaxyDev };
19
+ export { createCli, registerDevCommand, run, startValaxyDev };
20
+ export type { CreateCliOptions };
@@ -1,9 +1,9 @@
1
+ import 'node:path';
1
2
  import 'node:process';
2
3
  import 'yargs';
3
4
  import 'yargs/helpers';
4
- export { c as cli, I as registerDevCommand, W as run, Y as startValaxyDev } from '../../shared/valaxy._QBHi6cy.mjs';
5
+ export { c as createCli, J as registerDevCommand, Y as run, _ as startValaxyDev } from '../../shared/valaxy.B2Ey6Ph7.mjs';
5
6
  import 'node:os';
6
- import 'node:path';
7
7
  import 'consola';
8
8
  import 'consola/utils';
9
9
  import 'fast-glob';
@@ -26,7 +26,7 @@ import '@valaxyjs/utils';
26
26
  import 'node:fs/promises';
27
27
  import 'dayjs';
28
28
  import 'feed';
29
- import 'markdown-it';
29
+ import 'markdown-exit';
30
30
  import 'table';
31
31
  import 'hookable';
32
32
  import 'node:fs';
@@ -38,7 +38,6 @@ import 'vitepress-plugin-group-icons';
38
38
  import 'p-map';
39
39
  import 'node:buffer';
40
40
  import 'minisearch';
41
- import 'markdown-it-async';
42
41
  import '@shikijs/transformers';
43
42
  import 'shiki';
44
43
  import 'css-i18n';