11import fs from 'node:fs/promises'
22import path from 'node:path'
3+ import { Readable } from 'node:stream'
34import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
4- import { getExtFromUrl , isUrl , readSource , run } from '../src/cli'
5+ import { getExtFromUrl , isUrl , readSource , resolveLanguage , run } from '../src/cli'
56
67describe ( 'isUrl' , ( ) => {
78 it ( 'valid URL' , ( ) => {
@@ -32,6 +33,32 @@ describe('getExtFromUrl', () => {
3233 } )
3334} )
3435
36+ describe ( 'resolveLanguage' , ( ) => {
37+ it ( 'keeps bundled languages and aliases' , ( ) => {
38+ expect ( resolveLanguage ( 'ts' ) ) . toBe ( 'ts' )
39+ expect ( resolveLanguage ( 'typescript' ) ) . toBe ( 'typescript' )
40+ expect ( resolveLanguage ( 'md' ) ) . toBe ( 'md' )
41+ expect ( resolveLanguage ( 'dotenv' ) ) . toBe ( 'dotenv' )
42+ } )
43+
44+ it ( 'keeps special languages' , ( ) => {
45+ expect ( resolveLanguage ( 'text' ) ) . toBe ( 'text' )
46+ expect ( resolveLanguage ( 'txt' ) ) . toBe ( 'txt' )
47+ expect ( resolveLanguage ( 'plain' ) ) . toBe ( 'plain' )
48+ expect ( resolveLanguage ( 'ansi' ) ) . toBe ( 'ansi' )
49+ } )
50+
51+ it ( 'falls back to text for unknown languages' , ( ) => {
52+ expect ( resolveLanguage ( 'example' ) ) . toBe ( 'text' )
53+ expect ( resolveLanguage ( 'env.example' ) ) . toBe ( 'text' )
54+ } )
55+
56+ it ( 'normalizes casing before lookup' , ( ) => {
57+ expect ( resolveLanguage ( 'TS' ) ) . toBe ( 'ts' )
58+ expect ( resolveLanguage ( 'DOTENV' ) ) . toBe ( 'dotenv' )
59+ } )
60+ } )
61+
3562describe ( 'readSource' , ( ) => {
3663 const testDir = path . join ( import . meta. dirname , '__fixtures__' )
3764 const testFile = path . join ( testDir , 'test.ts' )
@@ -184,6 +211,12 @@ describe('run', () => {
184211
185212 expect ( output . length ) . toBe ( 1 )
186213 expect ( output [ 0 ] ) . toContain ( 'all' )
214+
215+ const asHtml : string [ ] = [ ]
216+ const plain : string [ ] = [ ]
217+ await run ( [ 'node' , 'shiki' , '--format' , 'html' , noExtFile ] , msg => asHtml . push ( msg ) )
218+ await run ( [ 'node' , 'shiki' , '--lang' , 'text' , '--format' , 'html' , noExtFile ] , msg => plain . push ( msg ) )
219+ expect ( asHtml [ 0 ] ) . toBe ( plain [ 0 ] )
187220 } )
188221
189222 it ( 'remote URL without file extension falls back to plaintext' , async ( ) => {
@@ -201,4 +234,74 @@ describe('run', () => {
201234
202235 vi . unstubAllGlobals ( )
203236 } )
237+
238+ it ( 'unknown extension falls back to plaintext without throwing' , async ( ) => {
239+ const envFile = path . join ( testDir , '.env.example' )
240+ await fs . writeFile ( envFile , 'FOO=bar' )
241+
242+ const output : string [ ] = [ ]
243+ await run ( [ 'node' , 'shiki' , '--format' , 'html' , envFile ] , msg => output . push ( msg ) )
244+
245+ const plain : string [ ] = [ ]
246+ await run ( [ 'node' , 'shiki' , '--lang' , 'text' , '--format' , 'html' , envFile ] , msg => plain . push ( msg ) )
247+
248+ expect ( output . length ) . toBe ( 1 )
249+ expect ( output [ 0 ] ) . toContain ( 'FOO=bar' )
250+ expect ( output [ 0 ] ) . toBe ( plain [ 0 ] )
251+ } )
252+
253+ it ( 'highlights known extensions' , async ( ) => {
254+ const mdFile = path . join ( testDir , 'README.md' )
255+ await fs . writeFile ( mdFile , '# Title' )
256+
257+ const md : string [ ] = [ ]
258+ const mdPlain : string [ ] = [ ]
259+ await run ( [ 'node' , 'shiki' , '--format' , 'html' , mdFile ] , msg => md . push ( msg ) )
260+ await run ( [ 'node' , 'shiki' , '--lang' , 'text' , '--format' , 'html' , mdFile ] , msg => mdPlain . push ( msg ) )
261+ expect ( md [ 0 ] ) . toContain ( 'Title' )
262+ expect ( md [ 0 ] ) . not . toBe ( mdPlain [ 0 ] )
263+
264+ const ts : string [ ] = [ ]
265+ const tsPlain : string [ ] = [ ]
266+ await run ( [ 'node' , 'shiki' , '--format' , 'html' , testFile ] , msg => ts . push ( msg ) )
267+ await run ( [ 'node' , 'shiki' , '--lang' , 'text' , '--format' , 'html' , testFile ] , msg => tsPlain . push ( msg ) )
268+ expect ( ts [ 0 ] ) . toContain ( 'const' )
269+ expect ( ts [ 0 ] ) . not . toBe ( tsPlain [ 0 ] )
270+ } )
271+
272+ it ( 'respects explicit --lang over unknown extension' , async ( ) => {
273+ const envFile = path . join ( testDir , 'file.env.example' )
274+ await fs . writeFile ( envFile , 'FOO=bar' )
275+
276+ const explicit : string [ ] = [ ]
277+ const fallback : string [ ] = [ ]
278+ await run ( [ 'node' , 'shiki' , '--lang' , 'dotenv' , '--format' , 'html' , envFile ] , msg => explicit . push ( msg ) )
279+ await run ( [ 'node' , 'shiki' , '--format' , 'html' , envFile ] , msg => fallback . push ( msg ) )
280+
281+ expect ( explicit . length ) . toBe ( 1 )
282+ expect ( explicit [ 0 ] ) . toContain ( 'FOO' )
283+ expect ( explicit [ 0 ] ) . not . toBe ( fallback [ 0 ] )
284+ } )
285+
286+ it ( 'stdin without --lang uses text' , async ( ) => {
287+ function createStdin ( ) {
288+ const stdin = Readable . from ( [ 'const x = 1' ] ) as Readable & { isTTY ?: boolean }
289+ stdin . isTTY = false
290+ return stdin
291+ }
292+
293+ const output : string [ ] = [ ]
294+ await run ( [ 'node' , 'shiki' , '--format' , 'html' ] , msg => output . push ( msg ) , createStdin ( ) )
295+
296+ const plain : string [ ] = [ ]
297+ await run ( [ 'node' , 'shiki' , '--lang' , 'text' , '--format' , 'html' ] , msg => plain . push ( msg ) , createStdin ( ) )
298+
299+ const asTs : string [ ] = [ ]
300+ await run ( [ 'node' , 'shiki' , '--lang' , 'ts' , '--format' , 'html' ] , msg => asTs . push ( msg ) , createStdin ( ) )
301+
302+ expect ( output . length ) . toBe ( 1 )
303+ expect ( output [ 0 ] ) . toContain ( 'const x = 1' )
304+ expect ( output [ 0 ] ) . toBe ( plain [ 0 ] )
305+ expect ( output [ 0 ] ) . not . toBe ( asTs [ 0 ] )
306+ } )
204307} )
0 commit comments