forked from RasaHQ/rasa
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprogram_output.js
153 lines (130 loc) · 5.52 KB
/
program_output.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
This plugin gives us the ability to insert the output of
a program in a code-block in the docs, by using the following
syntax:
```text [rasa --help]
```
It is inspired by `remark-source` and also relies on a pre-build phase,
before docusaurus is started (or built). It allows us to support separate
versions of the docs (and of the program outputs).
Caveat: this plugin operates at 2 separate points in time:
1. During the "pre-build" phase, pre-generating files inside the
sources/ folder. This phase is run on the "main" version of the repo
(main branch, release branches, tags, etc...), which doesn't have
several versions of the docs site. This is why `getProgramOutputs()` is
version-agnostic and simply outputs files in `mainSourceDir`.
2. At the build phase, happening on the `documentation` branch. In this context,
there are multiple versions of the docs, and the plugins needs to support that.
This is why the `remarkProgramOutput()` is version-aware and takes care
of loading the files from a versioned path.
*/
const fs = require('fs');
const globby = require('globby');
const visitChildren = require('unist-util-visit-children');
const { promisify } = require('util');
const exec = promisify(require('child_process').exec);
const { readFile, writeFile } = fs.promises;
const PROGRAM_OUTPUT_RE = /```[a-z]+ \[([^\]]+)\]\n```/;
const VERSIONED_DOCS_PATH_RE = /(\/versioned_docs\/version-\d+\.x)\//;
const defaultOptions = {
docsDir: './docs',
sourceDirectoryName: 'sources',
mainSourceDir: './docs/sources',
include: ['**.mdx', '**.md'],
commandPrefix: '',
};
/**
This function is use to get output of programs
requested in the docs. It parses all the docs files,
generates outputs and save them as files.
Options:
- docsDir: the directory containing the docs files
- mainSourceDir: the directory that will contain the program outputs
- sourceDirectoryName: the name (relative) of the source directory
- include: list of patterns to look for doc files
- commandPrefix: a prefix to be prepended before each command
*/
async function getProgramOutputs(options) {
options = { ...defaultOptions, ...options };
const { docsDir, include, mainSourceDir, commandPrefix } = options;
// first, gather all the docs files
const docsFiles = await globby(include, {
cwd: docsDir,
});
const seen = new Set();
// second, read every file source
let commands = await Promise.all(docsFiles.map(async (source) => {
const data = await readFile(`${docsDir}/${source}`);
const commands = [];
let group, command, stdout;
// third, find out if there is a program output to be generated
// there can be multiple outputs in the same file
const re = new RegExp(PROGRAM_OUTPUT_RE, 'gi');
while ((group = re.exec(data)) !== null) {
command = group[1];
if (seen.has(command)) {
continue;
}
seen.add(command);
// fourth, call the command to generate the output
output = await exec(`${commandPrefix} ${command}`);
commands.push([command, output.stdout]);
}
return commands;
}));
commands = commands.flat().filter(pair => pair.length > 0);
// finally, write all the command outputs as files in the `mainSourceDir`
return await Promise.all(commands.map(async ([command, output]) => {
return await writeFile(`${mainSourceDir}/${commandToFilename(command)}`, output);
}));
};
/**
Custom remark plugin to replace the following blocks:
```text [rasa --help]
```
with the actual output of the program (here `rasa --help`).
It relies on the output of `getProgramOutputs()` above,
and is inspired by `remark-sources` plugin.
*/
function remarkProgramOutput(options = {}) {
options = { ...defaultOptions, ...options };
return (root, { history }) => {
visitChildren((node, index, parent) => {
if (node && node.type === 'code') {
const content = readCommandOutput(node.meta, options, history[0]);
if (content !== undefined) {
node.value = content;
}
}
})(root);
};
}
function readCommandOutput(meta, { mainSourceDir, sourceDirectoryName }, filename) {
if (!meta) {
return undefined;
}
if (meta[0] !== '[' || meta[meta.length - 1] !== ']') {
return undefined;
}
meta = meta.slice(1, -1);
const sourceFile = `${getVersionedSourceDir(mainSourceDir, sourceDirectoryName, filename)}/${commandToFilename(meta)}`
try {
return fs.readFileSync(sourceFile, { encoding: 'utf8' });
} catch (e) {
throw new Error(`Failed to read file: ${sourceFile} for meta ${meta}`);
}
}
function commandToFilename(command) {
return command.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '.txt';
}
/**
By analyzing the `filename` argument, we can guess if the file we want
to load is in a specific version of the docs, or the main one.
*/
function getVersionedSourceDir(mainSourceDir, sourceDirectoryName, filename) {
const re = new RegExp(VERSIONED_DOCS_PATH_RE, 'gi');
const match = re.exec(filename);
return match === null ? mainSourceDir : `.${match[1]}/${sourceDirectoryName}`;
}
module.exports = getProgramOutputs;
module.exports.remarkProgramOutput = remarkProgramOutput;