| 1 | import type { ResolvedFontOptions, SlideInfo } from '@slidev/types' |
| 2 | import { createHash } from 'node:crypto' |
| 3 | import { mkdtempSync, readFileSync, rmSync } from 'node:fs' |
| 4 | import { tmpdir } from 'node:os' |
| 5 | import { join, relative, resolve } from 'node:path' |
| 6 | import { fileURLToPath } from 'node:url' |
| 7 | import { slash } from '@antfu/utils' |
| 8 | import MarkdownExit from 'markdown-exit' |
| 9 | import { describe, expect, it } from 'vitest' |
| 10 | import YAML from 'yaml' |
| 11 | import { parseAspectRatio, parseRangeString } from '../packages/parser/src' |
| 12 | import { getRoots, toAtFS } from '../packages/slidev/node/resolver' |
| 13 | import { createMakeAbsoluteImportGlob, generateCoollabsFontsUrl, generateGoogleFontsUrl, stringifyMarkdownTokens, updateFrontmatterPatch } from '../packages/slidev/node/utils' |
| 14 | |
| 15 | describe('utils', () => { |
| 16 | it('page-range', () => { |
| 17 | expect(parseRangeString(5)).toEqual([1, 2, 3, 4, 5]) |
| 18 | expect(parseRangeString(5, 'all')).toEqual([1, 2, 3, 4, 5]) |
| 19 | expect(parseRangeString(2, '*')).toEqual([1, 2]) |
| 20 | expect(parseRangeString(5, '1')).toEqual([1]) |
| 21 | expect(parseRangeString(10, '1,2-3,5')).toEqual([1, 2, 3, 5]) |
| 22 | expect(parseRangeString(10, '1;2-3;5')).toEqual([1, 2, 3, 5]) |
| 23 | expect(parseRangeString(10, '6-')).toEqual([6, 7, 8, 9, 10]) |
| 24 | }) |
| 25 | |
| 26 | it('aspect-ratio', () => { |
| 27 | expect(parseAspectRatio(1)).toEqual(1) |
| 28 | expect(parseAspectRatio('16/9')).toEqual(16 / 9) |
| 29 | expect(parseAspectRatio('16 / 9 ')).toEqual(16 / 9) |
| 30 | expect(parseAspectRatio('3:4')).toEqual(3 / 4) |
| 31 | expect(parseAspectRatio('1x1')).toEqual(1) |
| 32 | expect(parseAspectRatio('1')).toEqual(1) |
| 33 | |
| 34 | expect(() => parseAspectRatio('hello')).toThrow() |
| 35 | expect(() => parseAspectRatio('1/0')).toThrow() |
| 36 | }) |
| 37 | |
| 38 | it('stringify-markdown-tokens', () => { |
| 39 | const md = MarkdownExit({ html: true }) |
| 40 | const stringify = (src: string) => stringifyMarkdownTokens(md.parseInline(src, {})) |
| 41 | |
| 42 | expect(stringify('<span style="color:red">Composable</span> Vue')).toBe('Composable Vue') |
| 43 | expect(stringify('<b>Whatever</b>')).toBe('Whatever') |
| 44 | expect(stringify('Talk about `<details/>`')).toBe('Talk about <details/>') |
| 45 | expect(stringify('What is Readonly\\<T\\> in TypeScript')).toBe('What is Readonly<T> in TypeScript') |
| 46 | expect(stringify('Welcome to<br />*Slidev*')).toBe('Welcome to Slidev') |
| 47 | }) |
| 48 | |
| 49 | it('google-fonts', () => { |
| 50 | expect( |
| 51 | generateGoogleFontsUrl({ |
| 52 | webfonts: ['Fira Code', 'PT Serif'], |
| 53 | weights: ['200', '400', '600'], |
| 54 | provider: 'google', |
| 55 | } as ResolvedFontOptions), |
| 56 | ).toMatchSnapshot() |
| 57 | |
| 58 | expect( |
| 59 | generateGoogleFontsUrl({ |
| 60 | webfonts: ['Fira Code', 'PT Serif'], |
| 61 | weights: ['200', '400', '600'], |
| 62 | italic: true, |
| 63 | provider: 'google', |
| 64 | } as ResolvedFontOptions), |
| 65 | ).toMatchSnapshot() |
| 66 | }) |
| 67 | |
| 68 | it('coollabs-fonts', () => { |
| 69 | expect( |
| 70 | generateCoollabsFontsUrl({ |
| 71 | webfonts: ['Fira Code', 'PT Serif'], |
| 72 | weights: ['200', '400', '600'], |
| 73 | provider: 'google', |
| 74 | } as ResolvedFontOptions), |
| 75 | ).toMatchSnapshot() |
| 76 | |
| 77 | expect( |
| 78 | generateCoollabsFontsUrl({ |
| 79 | webfonts: ['Fira Code', 'PT Serif'], |
| 80 | weights: ['200', '400', '600'], |
| 81 | italic: true, |
| 82 | provider: 'google', |
| 83 | } as ResolvedFontOptions), |
| 84 | ).toMatchSnapshot() |
| 85 | }) |
| 86 | |
| 87 | it('roots', async () => { |
| 88 | const { cliRoot, clientRoot, userRoot, userWorkspaceRoot } = await getRoots(resolve('slides.md')) |
| 89 | const expectRelative = (v: string) => expect(slash(relative(__dirname, v))) |
| 90 | expectRelative(cliRoot).toMatchInlineSnapshot(`"../packages/slidev"`) |
| 91 | expectRelative(clientRoot).toMatchInlineSnapshot(`"../packages/client"`) |
| 92 | expectRelative(userRoot).toMatchInlineSnapshot(`".."`) |
| 93 | expectRelative(userWorkspaceRoot).toMatchInlineSnapshot(`".."`) |
| 94 | }) |
| 95 | |
| 96 | it('proxies slidev import globs through a real filesystem module', () => { |
| 97 | const root = mkdtempSync(join(tmpdir(), 'slidev-import-glob-')) |
| 98 | const makeAbsoluteImportGlob = createMakeAbsoluteImportGlob(root) |
| 99 | |
| 100 | try { |
| 101 | const proxyUrl = makeAbsoluteImportGlob([ |
| 102 | join(root, 'setup/main.ts'), |
| 103 | ], { import: 'default' }) |
| 104 | const secondProxyUrl = makeAbsoluteImportGlob([ |
| 105 | join(root, 'theme/setup/main.ts'), |
| 106 | ], { import: 'default' }) |
| 107 | |
| 108 | expect(proxyUrl).toMatch(/^\/@fs\/.*\/node_modules\/\.slidev\/virtual\/import-glob\.[a-f0-9]{10}\.ts$/) |
| 109 | expect(secondProxyUrl).toMatch(/^\/@fs\/.*\/node_modules\/\.slidev\/virtual\/import-glob\.[a-f0-9]{10}\.ts$/) |
| 110 | expect(secondProxyUrl).not.toBe(proxyUrl) |
| 111 | const content = readFileSync(fileURLToPath(`file://${proxyUrl.slice('/@fs'.length)}`), 'utf-8') |
| 112 | const secondContent = readFileSync(fileURLToPath(`file://${secondProxyUrl.slice('/@fs'.length)}`), 'utf-8') |
| 113 | expect(proxyUrl).toBe(toAtFS(join(root, `node_modules/.slidev/virtual/import-glob.${createHash('sha256').update(content).digest('hex').slice(0, 10)}.ts`))) |
| 114 | expect(secondProxyUrl).toBe(toAtFS(join(root, `node_modules/.slidev/virtual/import-glob.${createHash('sha256').update(secondContent).digest('hex').slice(0, 10)}.ts`))) |
| 115 | expect(content) |
| 116 | .toBe('export default import.meta.glob(["../../../setup/main.ts"], {"eager":true,"exhaustive":true,"import":"default"})\n') |
| 117 | expect(secondContent) |
| 118 | .toBe('export default import.meta.glob(["../../../theme/setup/main.ts"], {"eager":true,"exhaustive":true,"import":"default"})\n') |
| 119 | } |
| 120 | finally { |
| 121 | rmSync(root, { recursive: true, force: true }) |
| 122 | } |
| 123 | }) |
| 124 | |
| 125 | it('update frontmatter patch', async () => { |
| 126 | const dragPos = { |
| 127 | foo: '1,2,3,4', |
| 128 | } |
| 129 | function createFakeSource(yaml: string) { |
| 130 | const doc = YAML.parseDocument(yaml) |
| 131 | return { |
| 132 | frontmatter: {}, |
| 133 | source: { |
| 134 | frontmatter: doc.toJSON() || {}, |
| 135 | frontmatterRaw: yaml, |
| 136 | frontmatterDoc: doc, |
| 137 | }, |
| 138 | } as SlideInfo |
| 139 | } |
| 140 | function expectFrontmatter(slide: SlideInfo) { |
| 141 | return expect(slide.source.frontmatterDoc?.toString()) |
| 142 | } |
| 143 | |
| 144 | const slide1 = createFakeSource(``) |
| 145 | updateFrontmatterPatch(slide1.source, { dragPos }) |
| 146 | expectFrontmatter(slide1).toMatchInlineSnapshot(` |
| 147 | "dragPos: |
| 148 | foo: 1,2,3,4 |
| 149 | " |
| 150 | `) |
| 151 | |
| 152 | const slide2 = createFakeSource(` |
| 153 | # comment |
| 154 | title: Hello # another comment |
| 155 | dragPos: |
| 156 | bar: 5,6,7,8 |
| 157 | `) |
| 158 | updateFrontmatterPatch(slide2.source, { dragPos }) |
| 159 | expectFrontmatter(slide2).toMatchInlineSnapshot(` |
| 160 | "# comment |
| 161 | title: Hello # another comment |
| 162 | dragPos: |
| 163 | foo: 1,2,3,4 |
| 164 | " |
| 165 | `) |
| 166 | |
| 167 | // remove a field |
| 168 | const slide3 = createFakeSource(` |
| 169 | # comment |
| 170 | title: Hello # another comment |
| 171 | dragPos: |
| 172 | bar: 5,6,7,8 |
| 173 | `) |
| 174 | updateFrontmatterPatch(slide3.source, { title: null }) |
| 175 | expectFrontmatter(slide3).toMatchInlineSnapshot(` |
| 176 | "dragPos: |
| 177 | bar: 5,6,7,8 |
| 178 | " |
| 179 | `) |
| 180 | }) |
| 181 | }) |
| 182 |