diff --git a/README.md b/README.md index 404dbef6..458c09e7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ features! support](#editor-support)) - Vivify server starts lazily and automatically shuts down when no more viewers are connected -- various [config options](#config) +- various [customization options](docs/customization.md) If you need any additional features, feel free to [open an issue](https://github.com/jannis-baum/vivify/issues/new/choose) or @@ -54,47 +54,6 @@ list! - for Vim and Neovim: [vivify.vim](https://github.com/jannis-baum/vivify.vim) -### Config - -Vivify will look for an optional config file at `~/.vivify/config.json` and -`~/.vivify.json`. This file should contain a JSON object that can have the -following optional keys: - -- **`"styles"`**\ - a path to a custom style sheet, see [the default - styles](./static/) for examples -- **`"port"`**\ - the port Vivify's server should run on; this will be overwritten by - the environment variable `VIV_PORT` (default is 31622) -- **`"timeout"`**\ - how long the server should wait in ms before shutting down after - the last client disconnected; this will be overwritten by the environment - variable `VIV_TIMEOUT` (default is 10000) -- **`"katexOptions"`**\ - [available KaTeX options](https://katex.org/docs/options.html), such as - - ```json - { - "errorColor": "#cc0000", - "macros": { - "\\RR": "\\mathbb{R}" - } - } - ``` - -- **`"pageTitle"`**\ - JavaScript code that will be evaluated to determine the viewer's page title. - Here, the variable `components` is set to a string array of path components - for the current file, e.g. `['/', 'Users', 'you', 'file.txt']`. If this - evaluation fails, the title will be *custom title error* and you will see the - error message on the page. The default title are the last two components - joined with the path separator, e.g. `you/file.txt` -- **`"mdExtensions"`**\ - An array of file extensions that Vivify will parse as Markdown. All other - files will be displayed as monospaced text with code highlighting if - available. Default Markdown extensions are `['markdown', 'md', 'mdown', - 'mdwn', 'mkd', 'mkdn']` - ## Installation Once you have Vivify installed, use it by running `viv` with any text file or diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md diff --git a/docs/customization.md b/docs/customization.md new file mode 100644 index 00000000..629f8750 --- /dev/null +++ b/docs/customization.md @@ -0,0 +1,55 @@ +# Customizing Vivify + +Vivify offers various configuration options. It aims to have sensible defaults +while being built for maximal customizability. + +Vivify will look for an optional config file at `~/.vivify/config.json` and +`~/.vivify.json`. This file should contain a JSON object that can have the +following optional keys: + +- **`"styles"`**\ + A path to a single custom style sheet, or an array of multiple style sheets + applied in order. These will be applied after Vivify's [default + styles](./static/) are applied so that there are always sensible fallbacks but + you can override everything. +- **`"scripts"`**\ + A path to a single custom JavaScript to inject into the viewing pages, or an + array of multiple custom scripts. +- **`"dirListIgnore"`**\ + A path to a file with globs to ignore in Vivify's directory viewer, or an + array of multiple paths to ignore files. The syntax here is the same as in + `.gitignore` files. +- **`"port"`**\ + The port Vivify's server should run on; this will be overwritten by + the environment variable `VIV_PORT` (default is 31622) +- **`"timeout"`**\ + How long the server should wait in milliseconds before shutting down after the + last client disconnected; this will be overwritten by the environment variable + `VIV_TIMEOUT` (default is 10000) +- **`"katexOptions"`**\ + [Available KaTeX options](https://katex.org/docs/options.html), such as + + ```json + { + "errorColor": "#cc0000", + "macros": { + "\\RR": "\\mathbb{R}" + } + } + ``` + +- **`"pageTitle"`**\ + JavaScript code that will be evaluated to determine the viewer's page title. + Here, the variable `components` is set to a string array of path components + for the current file, e.g. `['~', 'some', 'path', 'file.txt']`. If this + evaluation fails, the title will be *custom title error* and you will see the + error message on the page. The default title are the last two components + joined with the path separator, e.g. `path/file.txt` +- **`"mdExtensions"`**\ + An array of file extensions that Vivify will render as Markdown. All other + files (except for Jupyter Notebooks) will be displayed as monospaced text with + code highlighting if available. The default Markdown extensions are + `['markdown', 'md', 'mdown', 'mdwn', 'mkd', 'mkdn']` +- **`"preferHomeTilde"`**\ + Prefer using `~` as a placeholder for your home directory in URLs as well as + the `components` for `"pageTitle"` (default is `true`) diff --git a/package.json b/package.json index 3130749c..cc233cd6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@viz-js/viz": "^3.7.0", "ansi_up": "^6.0.2", "express": "^4.19.2", + "glob": "10.4.5", "highlight.js": "^11.10.0", "katex": "^0.16.11", "markdown-it": "^14.1.0", diff --git a/src/app.ts b/src/app.ts index c52d927f..8424e96e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,7 +9,7 @@ import { router as healthRouter } from './routes/health.js'; import { router as staticRouter } from './routes/static.js'; import { router as viewerRouter } from './routes/viewer.js'; import { setupSockets } from './sockets.js'; -import { pathToURL, urlToPath } from './utils/path.js'; +import { pathToURL, preferredPath, urlToPath } from './utils/path.js'; import { existsSync } from 'fs'; const app = express(); @@ -48,8 +48,8 @@ const openArgs = async () => { console.log(`File not found: ${path}`); return; } - const absolute = presolve(path); - const url = `${address}${pathToURL(absolute)}`; + const target = preferredPath(presolve(path)); + const url = `${address}${pathToURL(target)}`; await open(url); }), ); diff --git a/src/parser/config.ts b/src/parser/config.ts index 6163fe78..275f9c05 100644 --- a/src/parser/config.ts +++ b/src/parser/config.ts @@ -4,18 +4,22 @@ import path from 'path'; type Config = { styles?: string; + scripts?: string; + dirListIgnore?: string[]; port: number; + timeout: number; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ katexOptions?: any; pageTitle?: string; mdExtensions: string[]; - timeout: number; + preferHomeTilde: boolean; }; const defaultConfig: Config = { port: 31622, mdExtensions: ['markdown', 'md', 'mdown', 'mdwn', 'mkd', 'mkdn'], timeout: 10000, + preferHomeTilde: true, }; const envConfigs: [string, keyof Config][] = [ @@ -28,9 +32,23 @@ const configPaths = [ path.join(homedir(), '.vivify.json'), ]; +// read contents of file at paths or files at paths +const getFileContents = (paths: string[] | string | undefined): string => { + if (paths === undefined) return ''; + const getFileContent = (p: string): string => { + const resolved = p[0] === '~' ? path.join(homedir(), p.slice(1)) : p; + return fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf8') : ''; + }; + + if (Array.isArray(paths)) { + return paths.map(getFileContent).join('\n'); + } + return getFileContent(paths); +}; + const getConfig = (): Config => { let config = undefined; - // greedily get config + // greedily find config for (const cp of configPaths) { if (!fs.existsSync(cp)) continue; try { @@ -41,12 +59,13 @@ const getConfig = (): Config => { if (config === undefined) return defaultConfig; - // get styles - if (config.styles && config.styles.length > 0) { - const stylePath = - config.styles[0] === '~' ? path.join(homedir(), config.styles.slice(1)) : config.styles; - config.styles = fs.existsSync(stylePath) ? fs.readFileSync(stylePath, 'utf8') : ''; - } + // get styles, scripts and ignore files + config.styles = getFileContents(config.styles); + config.scripts = getFileContents(config.scripts); + config.dirListIgnore = getFileContents(config.dirListIgnore) + .split('\n') + .filter((pattern) => pattern !== '' && pattern[0] !== '#'); + // fill missing values from default config for (const [key, value] of Object.entries(defaultConfig)) { if (config[key] === undefined) config[key] = value; diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 9765ab53..a75ed59c 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -1,10 +1,11 @@ -import { Dirent, readdirSync } from 'fs'; +import { Dirent } from 'fs'; import { homedir } from 'os'; import { join as pjoin } from 'path'; import { pathToURL } from '../utils/path.js'; import config from './config.js'; import renderNotebook from './ipynb.js'; import renderMarkdown from './markdown.js'; +import { globSync } from 'glob'; export type Renderer = (content: string) => string; @@ -38,13 +39,20 @@ export function renderTextFile(content: string, path: string): string { } const dirListItem = (item: Dirent, path: string) => - `