import fs from 'fs' ; import path from 'path' ; import { Marpit } from '@marp-team/marpit' ; import { isDynamicPattern } from 'globby' ; import { warn } from './cli' ; import { isError } from './error' ; import { File } from './file' ; export class Theme { readonly filename : string ; readonly overrideName ? : string ; name ? : string ; private readBuffer ? : Buffer ; private constructor ( filename , opts ) { this . filename = filename ; this . overrideName = opts . overrideName === true ? this . genUniqName ( ) : opts . overrideName ; this . name = this . overrideName ; } get buffer ( ) { return this . readBuffer ! ; } get css ( ) { const buf = this . buffer . toString ( ) ; return this . overrideName ? `${ buf }\n/* @theme ${ this . overrideName } */` : buf ; } async load ( ) { this . readBuffer = await fs . promises . readFile ( this . filename ) ; } private genUniqName ( ) { const uniq = ( ) => Math . random ( ) . toString ( 36 ) . slice ( 2 ) ; return `${ uniq ( ) }${ uniq ( ) }${ uniq ( ) }${ uniq ( ) }` ; } static async initialize ( filename , opts = { } ) { const instance = new Theme ( filename , opts ) ; await instance . load ( ) ; return instance ; } } export class ThemeSet { readonly fn : string [ ] ; readonly fnForWatch : string [ ] ; /** The key-value pair from file path to instance */ readonly themes : Map < string , Theme > = new Map ( ) ; onThemeUpdated : ( path : string ) => void = ( ) => { } ; private observedMarkdowns : Map < string , string | undefined > = new Map ( ) ; private constructor ( opts ) { this . fn = opts . fn ; this . fnForWatch = opts . fnForWatch ; } async findPath ( ) { return await ThemeSet . findPath ( this . fn ) ; } async load ( fn ) { const filename = path . resolve ( fn ) ; let theme = this . themes . get ( filename ) ; if ( theme ) { await theme . load ( ) ; } else { theme = await Theme . initialize ( filename ) ; this . add ( theme ) ; } if ( theme . name !== undefined ) this . notify ( theme . name ) ; } observe ( markdownPath , theme ) { this . observedMarkdowns . set ( markdownPath , theme ) ; } registerTo ( engine ) { for ( const theme of this . themes . values ( ) ) { try { const engineTheme = engine . themeSet . add ( theme . css ) ; theme . name = engineTheme . name ; } catch ( e ) { const fn = path . relative ( process . cwd ( ) , theme . filename ) ; const errorMsg = isError ( e ) ? ` (${ e . message })` : '' ; warn ( `Cannot register theme CSS: ${ fn }${ errorMsg }` ) ; } } } unobserve ( markdownPath ) { this . observedMarkdowns . delete ( markdownPath ) ; } private add ( theme ) { this . themes . set ( theme . filename , theme ) ; } private notify ( theme ) { this . observedMarkdowns . forEach ( ( v , path ) => { if ( v === theme ) this . onThemeUpdated ( path ) ; } ) ; } static async initialize ( baseFn , themes = [ ] ) { const fn = [ ... baseFn , ... themes . map ( ( t ) => t . filename ) ] ; const found = await ThemeSet . findPath ( fn ) ; const fnForWatch = new Set ( found . map ( ( f ) => path . resolve ( f ) ) ) ; for ( const f of fn ) { if ( ! isDynamicPattern ( f . split ( path . sep ) . join ( '/' ) ) ) { try { const stat = await fs . promises . lstat ( f ) ; if ( stat . isFile ( ) || stat . isDirectory ( ) || stat . isSymbolicLink ( ) ) fnForWatch . add ( path . resolve ( f ) ) ; } catch ( e ) { } } } const instance = new ThemeSet ( { fn , fnForWatch : [ ... fnForWatch ] } ) ; for ( const f of found ) await instance . load ( f ) ; themes . forEach ( ( t ) => instance . add ( t ) ) ; return instance ; } private static async findPath ( fn ) { return File . findPath ( { expandDirectories : { extensions : [ ] , files : [ '*.css' ] , } , } , ... fn ) ; } } export namespace Theme { export interface Options { overrideName ? : true | string ; } }