Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/format/reveal/format-reveal-multiplex.ts
6452 views
1
/*
2
* format-reveal-multiplex.ts
3
*
4
* Copyright (C) 2021-2022 Posit Software, PBC
5
*/
6
7
import { existsSync } from "../../deno_ral/fs.ts";
8
import { join } from "../../deno_ral/path.ts";
9
import { isSelfContainedOutput } from "../../command/render/render-info.ts";
10
import { kResourcePath } from "../../config/constants.ts";
11
12
import { Format, FormatExtras, PandocFlags } from "../../config/types.ts";
13
import { pandocIngestSelfContainedContent } from "../../core/pandoc/self-contained.ts";
14
import { dirAndStem, pathWithForwardSlashes } from "../../core/path.ts";
15
import { formatResourcePath } from "../../core/resources.ts";
16
import { lines } from "../../core/text.ts";
17
18
export const kRevealJsMultiplex = "multiplex";
19
20
export function revealMultiplexPlugin(format: Format): string | undefined {
21
if (format.metadata[kRevealJsMultiplex]) {
22
return formatResourcePath("revealjs", join("plugins", "multiplex"));
23
} else {
24
return undefined;
25
}
26
}
27
28
export function revealMuliplexPreviewFile(file: string) {
29
const speakerFile = revealSpeakerOutput(file);
30
if (existsSync(speakerFile) && existsSync(file)) {
31
// return speaker file if it's >= mod time of file
32
const modTime = Deno.statSync(file).mtime;
33
const speakerModTime = Deno.statSync(speakerFile).mtime;
34
if (modTime && speakerModTime && (speakerModTime > modTime)) {
35
return speakerFile;
36
}
37
}
38
return file;
39
}
40
41
export function revealMultiplexExtras(
42
format: Format,
43
flags: PandocFlags,
44
): FormatExtras | undefined {
45
if (format.metadata[kRevealJsMultiplex]) {
46
// create speaker version w/ master
47
return {
48
postprocessors: [async (output: string) => {
49
// determine the multiplex secret and id (could be provided by the
50
// user, contained within existing speaker output, or created from
51
// scratch via a call to the multiplex url)
52
const token = await revealMultiplexToken(format, output);
53
54
// read file
55
const content = await Deno.readTextFile(output);
56
57
// generate speaker content
58
const speakerContent = withMultiplexToken(content, token);
59
60
// generate client content (remove the secret and the speaker notes)
61
token.secret = null;
62
const clientContent = withMultiplexToken(content, token)
63
.replace(
64
/(\/\/ reveal\.js plugins\n\s*plugins: \[[^\[]+?)(RevealNotes,)([^\[]+?\])/,
65
"$1$3",
66
);
67
68
// write client version
69
await Deno.writeTextFile(output, clientContent);
70
71
// write speaker version
72
const speakerOutput = revealSpeakerOutput(output);
73
await Deno.writeTextFile(speakerOutput, speakerContent);
74
75
// determine whether this is self-contained output
76
const selfContained = isSelfContainedOutput(
77
flags,
78
format,
79
speakerOutput,
80
);
81
82
// If this is self contained, we should ingest dependencies
83
if (selfContained) {
84
await pandocIngestSelfContainedContent(
85
speakerOutput,
86
format.pandoc[kResourcePath],
87
);
88
}
89
}],
90
};
91
} else {
92
return undefined;
93
}
94
}
95
96
function revealSpeakerOutput(output: string) {
97
const [dir, stem] = dirAndStem(output);
98
return join(dir, `${stem}-speaker.html`);
99
}
100
101
interface RevealMultiplexToken {
102
secret: string | null;
103
id: string;
104
url: string;
105
}
106
107
const kDefaultMultiplexUrl = "https://multiplex.up.railway.app/";
108
109
async function revealMultiplexToken(
110
format: Format,
111
output: string,
112
): Promise<RevealMultiplexToken> {
113
// is it provided in config?
114
const multiplex = format.metadata[kRevealJsMultiplex] as RevealMultiplexToken;
115
if (
116
typeof multiplex === "object" && typeof (multiplex.secret) === "string" &&
117
typeof (multiplex.id) === "string"
118
) {
119
multiplex.url = multiplex.url || kDefaultMultiplexUrl;
120
return multiplex;
121
}
122
123
// is it located inside the -speaker file?
124
const speakerOutput = revealSpeakerOutput(output);
125
if (existsSync(speakerOutput)) {
126
const speakerContent = await Deno.readTextFile(speakerOutput);
127
const initPos = speakerContent.lastIndexOf("Reveal.initialize({");
128
if (initPos !== -1) {
129
const initLines = lines(speakerContent.substring(initPos));
130
const kMultiplexStart = "'multiplex': {";
131
const kMultiplexEnd = "},";
132
for (const line of initLines) {
133
if (line.startsWith(kMultiplexStart) && line.endsWith(kMultiplexEnd)) {
134
const multiplexJson = "{ " +
135
line.substring(
136
kMultiplexStart.length,
137
line.length - kMultiplexEnd.length,
138
) + " }";
139
const multiplex = JSON.parse(multiplexJson) as RevealMultiplexToken;
140
if (!!multiplex.id && !!multiplex.secret) {
141
return multiplex;
142
}
143
}
144
}
145
}
146
}
147
148
// provision a new token
149
const url = typeof multiplex === "object"
150
? (multiplex.url || kDefaultMultiplexUrl)
151
: kDefaultMultiplexUrl;
152
try {
153
const response = await fetch(pathWithForwardSlashes(join(url, "token")));
154
const jsonData = await response.json();
155
const multiplex = {
156
secret: jsonData.secret,
157
id: jsonData.socketId,
158
url,
159
};
160
return multiplex;
161
} catch (e) {
162
if (!(e instanceof Error)) {
163
throw e;
164
}
165
throw Error(
166
"Error attempting to provision multiplex token from '" + url + "': " +
167
e.message,
168
);
169
}
170
}
171
172
function withMultiplexToken(content: string, token: RevealMultiplexToken) {
173
return content.replace(
174
/(Reveal.initialize\({[\s\S]*?'multiplex': )({.*?},)/g,
175
`$1${JSON.stringify(token)},`,
176
);
177
}
178
179