Skip to content

Commit

Permalink
Merge pull request #117 from jannis-baum/issue/103-api-enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
jannis-baum authored Aug 1, 2024
2 parents da5023c + a21cc05 commit 9dd90af
Show file tree
Hide file tree
Showing 26 changed files with 399 additions and 99 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ jobs:
- run: yarn
- run: yarn lint

test:
name: Test
runs-on: ubuntu-latest
steps:
- name: set up node
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: checkout
uses: actions/checkout@v4
- run: yarn
- run: yarn test

build-linux:
name: Build Linux
needs: [lint]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ issue](https://github.com/jannis-baum/vivify/issues/new/choose) or
- `<kbd>` tags, e.g. to style keyboard shortcuts

You can find examples for all supported features in the files in the
[`tests/`](tests) directory. In case you are looking at these on GitHub, keep in
mind that GitHub doesn't support some of the features that Vivify supports so
some things may look off.
[`tests/rendering`](tests/rendering) directory. In case you are looking at these
on GitHub, keep in mind that GitHub doesn't support some of the features that
Vivify supports so some things may look off.

### Editor Support

Expand Down
4 changes: 2 additions & 2 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ above:
## Testing rendering

You can find files to test Vivify's rendering/parsing capabilities in the
[`tests/`](tests/) directory. Please make sure to add to this in case you add
anything new related to this.
[`tests/rendering`](tests/rendering) directory. Please make sure to add to this
in case you add anything new related to this.

## Writing Markdown

Expand Down
19 changes: 14 additions & 5 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Vivify offers various configuration options. It aims to have sensible defaults
while being built for maximal customizability.

## Configuration file

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:
Expand All @@ -19,13 +21,9 @@ following optional keys:
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)
last client disconnected (default is 10000)
- **`"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
Expand Down Expand Up @@ -63,3 +61,14 @@ following optional keys:
"includeLevel": [2, 3]
}
```

## Environment variables

In addition to these config file entries, the following options can be set
through environment variables.

- **`VIV_PORT`**\
The port Vivify's server should run on (default is 31622)
- **`VIV_TIMEOUT`**\
Same as `"timeout"` from config file above but takes precedence over the
setting in the config file
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
"dev": "VIV_TIMEOUT=0 VIV_PORT=3000 NODE_ENV=development nodemon --exec node --loader ts-node/esm src/app.ts",
"viv": "VIV_PORT=3000 node --loader ts-node/esm src/app.ts",
"lint": "eslint src static",
"lint-markdown": "markdownlint-cli2 --config .github/.markdownlint-cli2.yaml"
"lint-markdown": "markdownlint-cli2 --config .github/.markdownlint-cli2.yaml",
"test": "node --loader ts-node/esm tests/unit/cli.ts"
},
"type": "module",
"dependencies": {
"@viz-js/viz": "^3.7.0",
"ansi_up": "^6.0.2",
"axios": "^1.7.2",
"express": "^4.19.2",
"glob": "10.4.5",
"highlight.js": "^11.10.0",
Expand Down
50 changes: 6 additions & 44 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { createServer, get } from 'http';
import { resolve as presolve } from 'path';

import express from 'express';
import open from 'open';

import config from './parser/config.js';
import config, { address } from './config.js';
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 { router as openRouter } from './routes/_open.js';
import { setupSockets } from './sockets.js';
import { pathToURL, preferredPath, urlToPath } from './utils/path.js';
import { existsSync } from 'fs';
import { urlToPath } from './utils/path.js';
import { handleArgs } from './cli.js';

const app = express();
app.use(express.json());
Expand All @@ -21,11 +20,12 @@ app.use((req, res, next) => {
app.use('/static', staticRouter);
app.use('/health', healthRouter);
app.use('/viewer', viewerRouter);
app.use('/_open', openRouter);

const server = createServer(app);

let shutdownTimer: NodeJS.Timeout | null = null;
export const { clientsAt, messageClientsAt } = setupSockets(
export const { clientsAt, messageClients, openAndMessage } = setupSockets(
server,
() => {
if (config.timeout > 0)
Expand All @@ -39,44 +39,6 @@ export const { clientsAt, messageClientsAt } = setupSockets(
},
);

const address = `http://localhost:${config.port}`;
const handleArgs = async () => {
try {
const args = process.argv.slice(2);
const options = args.filter((arg) => arg.startsWith('-'));
for (const option of options) {
switch (option) {
case '-v':
case '--version':
console.log(`vivify-server ${process.env.VERSION ?? 'dev'}`);
break;
default:
console.log(`unknown option "${option}"`);
}
}

const paths = args.filter((arg) => !arg.startsWith('-'));
await Promise.all(
paths.map(async (path) => {
if (!existsSync(path)) {
console.log(`File not found: ${path}`);
return;
}
const target = preferredPath(presolve(path));
const url = `${address}${pathToURL(target)}`;
await open(url);
}),
);
} finally {
if (process.env['NODE_ENV'] !== 'development') {
// - viv executable waits for this string and then stops printing
// vivify-server's output and terminates
// - the string itself is not shown to the user
console.log('STARTUP COMPLETE');
}
}
};

get(`${address}/health`, async () => {
// server is already running
await handleArgs();
Expand Down
85 changes: 85 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import axios from 'axios';
import { existsSync } from 'fs';
import open from 'open';
import { resolve as presolve } from 'path';
import { address } from './config.js';
import { pathToURL, preferredPath } from './utils/path.js';

// exported for unit test
export const getPathAndLine = (
target: string,
): { path: string | undefined; line: number | undefined } => {
const exp = /^(?<path>(?:.*?)(?<!\\)(?:\\\\)*)(?::(?<line>\d+))?$/;
const groups = target.match(exp)?.groups;
if (groups === undefined || !groups['path']) {
return { path: undefined, line: undefined };
}
const path = groups['path'].replace('\\:', ':').replace('\\\\', '\\');
const line = groups['line'] ? parseInt(groups['line']) : undefined;
return { path, line };
};

export const openFileAt = async (path: string) =>
open(`${address}${pathToURL(preferredPath(path))}`);

const openTarget = async (target: string) => {
const { path, line } = getPathAndLine(target);
if (!path) {
console.log(`Invalid target: ${target}`);
return;
}
if (!existsSync(path)) {
console.log(`File not found: ${path}`);
return;
}

const resolvedPath = presolve(path);
try {
if (line !== undefined) {
await axios.post(`${address}/_open`, {
path: resolvedPath,
command: 'SCROLL',
value: line,
});
} else {
await openFileAt(resolvedPath);
}
} catch {
console.log(`Failed to open ${target}`);
}
};

export const handleArgs = async () => {
try {
const args = process.argv.slice(2);
const positionals: string[] = [];
let parseOptions = true;

for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (!(arg.startsWith('-') && parseOptions)) {
positionals.push(arg);
continue;
}
switch (arg) {
case '-v':
case '--version':
console.log(`vivify-server ${process.env.VERSION ?? 'dev'}`);
break;
case '--':
parseOptions = false;
break;
default:
console.log(`Unknown option "${arg}"`);
}
}
await Promise.all(positionals.map((target) => openTarget(target)));
} finally {
if (process.env['NODE_ENV'] !== 'development') {
// - viv executable waits for this string and then stops printing
// vivify-server's output and terminates
// - the string itself is not shown to the user
console.log('STARTUP COMPLETE');
}
}
};
19 changes: 16 additions & 3 deletions src/parser/config.ts → src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import fs from 'fs';
import { homedir } from 'os';
import path from 'path';

// NOTE: this type does not directly correspond to the config file: see
// defaultConfig, envConfigs and configFileBlocked
type Config = {
styles?: string;
scripts?: string;
Expand All @@ -18,18 +20,23 @@ type Config = {
/* eslint-enable @typescript-eslint/no-explicit-any */
};

// fills in values from config file config that are not present
const defaultConfig: Config = {
port: 31622,
mdExtensions: ['markdown', 'md', 'mdown', 'mdwn', 'mkd', 'mkdn'],
timeout: 10000,
preferHomeTilde: true,
};

// configs that are overwritten by environment variables
const envConfigs: [string, keyof Config][] = [
['VIV_PORT', 'port'],
['VIV_TIMEOUT', 'timeout'],
];

// configs that can't be set through the config file
const configFileBlocked: (keyof Config)[] = ['port'];

const configPaths = [
path.join(homedir(), '.vivify', 'config.json'),
path.join(homedir(), '.vivify.json'),
Expand All @@ -49,7 +56,7 @@ const getFileContents = (paths: string[] | string | undefined): string => {
return getFileContent(paths);
};

const getConfig = (): Config => {
const config = ((): Config => {
let config = undefined;
// greedily find config
for (const cp of configPaths) {
Expand All @@ -63,6 +70,10 @@ const getConfig = (): Config => {
// revert to default config if no config found
config = config ?? defaultConfig;

for (const key of configFileBlocked) {
delete config[key];
}

// get styles, scripts and ignore files
config.styles = getFileContents(config.styles);
config.scripts = getFileContents(config.scripts);
Expand All @@ -79,6 +90,8 @@ const getConfig = (): Config => {
if (process.env[env]) config[key] = process.env[env];
}
return config;
};
})();

export default config;

export default getConfig();
export const address = `http://localhost:${config.port}`;
2 changes: 1 addition & 1 deletion src/parser/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import anchor from 'markdown-it-anchor';
import highlight from './highlight.js';
import graphviz from './dot.js';
import githubAlerts from 'markdown-it-github-alerts';
import config from './config.js';
import config from '../config.js';
import { Renderer } from './parser.js';

const mdit = new MarkdownIt({
Expand Down
5 changes: 4 additions & 1 deletion src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Dirent } from 'fs';
import { homedir } from 'os';
import { join as pjoin, dirname as pdirname, basename as pbasename } from 'path';
import { pathToURL } from '../utils/path.js';
import config from './config.js';
import config from '../config.js';
import renderNotebook from './ipynb.js';
import renderMarkdown from './markdown.js';
import { globSync } from 'glob';
Expand Down Expand Up @@ -33,6 +33,9 @@ function textRenderer(
}
}

export const shouldRender = (mime: string): boolean =>
mime.startsWith('text/') || mime === 'application/json';

export function renderTextFile(content: string, path: string): string {
const fileEnding = path?.split('.')?.at(-1);
const renderInformation = textRenderer(fileEnding);
Expand Down
28 changes: 28 additions & 0 deletions src/routes/_open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Request, Response, Router } from 'express';
import { openAndMessage } from '../app.js';
import { openFileAt } from '../cli.js';

// this route should only be used internally between vivify processes
export const router = Router();

router.post('/', async (req: Request, res: Response) => {
const { path, command, value } = req.body;

if (!path) {
res.status(400).send('Bad request.');
return;
}

try {
if (command) {
await openAndMessage(path, `${command}: ${value}`);
} else {
await openFileAt(path);
}
} catch {
res.status(500).end();
return;
}

res.end();
});
Loading

0 comments on commit 9dd90af

Please sign in to comment.