Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/preview/preview.ts
12925 views
1
/*
2
* preview.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { debug, info, warning } from "../../deno_ral/log.ts";
8
import {
9
basename,
10
dirname,
11
isAbsolute,
12
join,
13
relative,
14
} from "../../deno_ral/path.ts";
15
import { existsSync } from "../../deno_ral/fs.ts";
16
17
import * as ld from "../../core/lodash.ts";
18
19
import { cssFileResourceReferences } from "../../core/css.ts";
20
import { logError } from "../../core/log.ts";
21
import { openUrl } from "../../core/shell.ts";
22
import {
23
httpContentResponse,
24
httpFileRequestHandler,
25
isBrowserPreviewable,
26
serveRedirect,
27
} from "../../core/http.ts";
28
import { HttpFileRequestOptions } from "../../core/http-types.ts";
29
import {
30
HttpDevServer,
31
httpDevServer,
32
HttpDevServerRenderMonitor,
33
} from "../../core/http-devserver.ts";
34
import { isHtmlContent, isPdfContent, isTextContent } from "../../core/mime.ts";
35
import { PromiseQueue } from "../../core/promise.ts";
36
import { inputFilesDir } from "../../core/render.ts";
37
38
import { kQuartoRenderCommand } from "../render/constants.ts";
39
40
import {
41
previewUnableToRenderResponse,
42
printWatchingForChangesMessage,
43
render,
44
renderToken,
45
} from "../render/render-shared.ts";
46
import {
47
renderServices,
48
withRenderServices,
49
} from "../render/render-services.ts";
50
import {
51
RenderFlags,
52
RenderResult,
53
RenderResultFile,
54
RenderServices,
55
} from "../render/types.ts";
56
import { renderFormats } from "../render/render-contexts.ts";
57
import { renderResultFinalOutput } from "../render/render.ts";
58
import { replacePandocArg } from "../render/flags.ts";
59
60
import { Format, isPandocFilter } from "../../config/types.ts";
61
import {
62
kPdfJsInitialPath,
63
pdfJsBaseDir,
64
pdfJsFileHandler,
65
} from "../../core/pdfjs.ts";
66
import {
67
kProjectWatchInputs,
68
ProjectContext,
69
ProjectPreview,
70
} from "../../project/types.ts";
71
import { projectOutputDir } from "../../project/project-shared.ts";
72
import { projectContext } from "../../project/project-context.ts";
73
import {
74
normalizePath,
75
pathWithForwardSlashes,
76
safeExistsSync,
77
} from "../../core/path.ts";
78
import {
79
isPositWorkbench,
80
isRStudio,
81
isServerSession,
82
isVSCodeServer,
83
vsCodeServerProxyUri,
84
} from "../../core/platform.ts";
85
import { isJupyterNotebook } from "../../core/jupyter/jupyter.ts";
86
import { watchForFileChanges } from "../../core/watch.ts";
87
import { previewMonitorResources } from "../../core/quarto.ts";
88
import { exitWithCleanup, onCleanup } from "../../core/cleanup.ts";
89
import {
90
extensionFilesFromDirs,
91
inputExtensionDirs,
92
} from "../../extension/extension.ts";
93
import { kOutputFile, kTargetFormat } from "../../config/constants.ts";
94
import { mergeConfigs } from "../../core/config.ts";
95
import { kLocalhost } from "../../core/port-consts.ts";
96
import { findOpenPort, waitForPort } from "../../core/port.ts";
97
import { inputFileForOutputFile } from "../../project/project-index.ts";
98
import { staticResource } from "../../preview/preview-static.ts";
99
import { previewTextContent } from "../../preview/preview-text.ts";
100
import {
101
previewURL,
102
printBrowsePreviewMessage,
103
rswURL,
104
} from "../../core/previewurl.ts";
105
import { notebookContext } from "../../render/notebook/notebook-context.ts";
106
import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts";
107
108
export async function resolvePreviewOptions(
109
options: ProjectPreview,
110
project?: ProjectContext,
111
): Promise<ProjectPreview> {
112
// start with project options if we have them
113
if (project?.config?.project.preview) {
114
options = mergeConfigs(project.config.project.preview, options);
115
}
116
// provide defaults
117
const resolved = mergeConfigs({
118
host: kLocalhost,
119
browser: true,
120
[kProjectWatchInputs]: !isRStudio(),
121
timeout: 0,
122
navigate: true,
123
}, options) as ProjectPreview;
124
125
// if a specific port is requested then wait for it up to 5 seconds
126
if (resolved.port) {
127
if (!await waitForPort({ port: resolved.port, hostname: resolved.host })) {
128
throw new Error(`Requested port ${options.port} is already in use.`);
129
}
130
} else {
131
resolved.port = findOpenPort();
132
}
133
134
return resolved;
135
}
136
137
interface PreviewOptions {
138
port?: number;
139
host?: string;
140
browser?: boolean;
141
[kProjectWatchInputs]?: boolean;
142
timeout?: number;
143
presentation: boolean;
144
}
145
146
export function previewInitialPath(
147
outputFile: string,
148
project: ProjectContext | undefined,
149
): string {
150
if (isPdfContent(outputFile)) {
151
return kPdfJsInitialPath;
152
}
153
if (project && !project.isSingleFile) {
154
return pathWithForwardSlashes(
155
relative(projectOutputDir(project), outputFile),
156
);
157
}
158
return "";
159
}
160
161
export async function preview(
162
file: string,
163
flags: RenderFlags,
164
pandocArgs: string[],
165
options: PreviewOptions,
166
pProject?: ProjectContext,
167
) {
168
// Reuse the project context from cmd.ts if provided, avoiding redundant
169
// context creation and transient notebook file duplication (#14281).
170
const nbContext = pProject?.notebookContext ?? notebookContext();
171
const project = pProject ??
172
(await projectContext(file, nbContext)) ??
173
(await singleFileProjectContext(file, nbContext));
174
onCleanup(() => {
175
project.cleanup();
176
});
177
178
// determine the target format if there isn't one in the command line args
179
// (current we force the use of an html or pdf based format)
180
const format = await previewFormat(file, project, flags.to, undefined);
181
setPreviewFormat(format, flags, pandocArgs);
182
183
// render for preview (create function we can pass to watcher then call it)
184
let isRendering = false;
185
const render = async (to?: string) => {
186
const renderFlags = { ...flags, to: to || flags.to };
187
const services = renderServices(nbContext);
188
try {
189
HttpDevServerRenderMonitor.onRenderStart();
190
isRendering = true;
191
const result = await renderForPreview(
192
file,
193
services,
194
renderFlags,
195
pandocArgs,
196
project,
197
);
198
HttpDevServerRenderMonitor.onRenderStop(true);
199
return result;
200
} catch (error) {
201
HttpDevServerRenderMonitor.onRenderStop(false);
202
throw error;
203
} finally {
204
isRendering = false;
205
services.cleanup();
206
}
207
};
208
const result = await render();
209
210
// resolve options (don't look at the project context b/c we
211
// don't want overlapping ports within the same project)
212
options = {
213
...options,
214
...(await resolvePreviewOptions(options)),
215
};
216
217
const ac = new AbortController();
218
// create listener and callback to stop the server
219
// const listener = Deno.listen({ port: options.port!, hostname: options.host });
220
const stopServer = () => ac.abort();
221
222
// create client reloader
223
const reloader = httpDevServer(
224
options.timeout!,
225
() => isRendering,
226
stopServer,
227
options.presentation || format === "revealjs",
228
);
229
230
// watch for changes and re-render / re-load as necessary
231
const changeHandler = createChangeHandler(
232
result,
233
reloader,
234
render,
235
options[kProjectWatchInputs]!,
236
);
237
238
// create file request handler (hook clients up to reloader, provide
239
// function to be used if a render request comes in)
240
const handler = isPdfContent(result.outputFile)
241
? pdfFileRequestHandler(
242
result.outputFile,
243
normalizePath(file),
244
flags,
245
result.format,
246
options.port!,
247
reloader,
248
changeHandler.render,
249
project,
250
)
251
: project && !project.isSingleFile
252
? projectHtmlFileRequestHandler(
253
project,
254
normalizePath(file),
255
flags,
256
result.format,
257
reloader,
258
changeHandler.render,
259
)
260
: htmlFileRequestHandler(
261
result.outputFile,
262
normalizePath(file),
263
flags,
264
result.format,
265
reloader,
266
changeHandler.render,
267
project,
268
);
269
270
// open browser if this is a browseable format
271
const initialPath = previewInitialPath(result.outputFile, project);
272
if (
273
options.browser &&
274
!isServerSession() &&
275
isBrowserPreviewable(result.outputFile)
276
) {
277
await openUrl(previewURL(options.host!, options.port!, initialPath));
278
}
279
280
// print status
281
await printBrowsePreviewMessage(options.host!, options.port!, initialPath);
282
283
// watch for src changes in dev mode
284
previewMonitorResources(stopServer);
285
286
// serve project
287
const server = Deno.serve(
288
{ signal: ac.signal, port: options.port!, hostname: options.host },
289
async (req: Request) => {
290
try {
291
return await handler(req);
292
} catch (err) {
293
if (err instanceof Error) {
294
warning(err.message);
295
}
296
throw err;
297
}
298
},
299
);
300
await server.finished;
301
}
302
303
export interface PreviewRenderRequest {
304
version: 1 | 2;
305
path: string;
306
format?: string;
307
}
308
309
export function isPreviewRenderRequest(req: Request) {
310
if (req.url.includes(kQuartoRenderCommand)) {
311
return true;
312
} else {
313
const token = renderToken();
314
if (token) {
315
return req.url.includes(token);
316
} else {
317
return false;
318
}
319
}
320
}
321
322
export function isPreviewTerminateRequest(req: Request) {
323
const kTerminateToken = "4231F431-58D3-4320-9713-994558E4CC45";
324
return req.url.includes(kTerminateToken);
325
}
326
327
export function previewRenderRequest(
328
req: Request,
329
hasClients: boolean,
330
baseDir?: string,
331
): PreviewRenderRequest | undefined {
332
// look for v1 rstudio format (requires baseDir b/c its a relative path)
333
const match = req.url.match(
334
new RegExp(`/${kQuartoRenderCommand}/(.*)$`),
335
);
336
if (match && baseDir) {
337
return {
338
version: 1,
339
path: join(baseDir, match[1]),
340
};
341
} else {
342
const token = renderToken();
343
if (token && req.url.includes(token)) {
344
const url = new URL(req.url);
345
const path = url.searchParams.get("path");
346
if (path) {
347
if (hasClients) {
348
return {
349
version: 2,
350
path,
351
format: url.searchParams.get("format") || undefined,
352
};
353
}
354
}
355
}
356
}
357
}
358
359
export async function previewRenderRequestIsCompatible(
360
request: PreviewRenderRequest,
361
project: ProjectContext,
362
format?: string,
363
) {
364
if (request.version === 1) {
365
return true; // rstudio manages its own request compatibility state
366
} else {
367
const reqFormat = await previewFormat(
368
request.path,
369
project,
370
request.format,
371
undefined,
372
);
373
return reqFormat === format;
374
}
375
}
376
377
// determine the format to preview
378
export async function previewFormat(
379
file: string,
380
project: ProjectContext,
381
format?: string,
382
formats?: Record<string, Format>,
383
) {
384
if (format) {
385
return format;
386
}
387
// const nbContext = notebookContext();
388
// project = project || (await singleFileProjectContext(file, nbContext));
389
formats = formats ||
390
await withRenderServices(
391
project.notebookContext,
392
(services: RenderServices) =>
393
renderFormats(file, services, "all", project),
394
);
395
format = Object.keys(formats)[0] || "html";
396
return format;
397
}
398
399
export function setPreviewFormat(
400
format: string,
401
flags: RenderFlags,
402
pandocArgs: string[],
403
) {
404
flags.to = format;
405
replacePandocArg(pandocArgs, "--to", format);
406
}
407
408
export function handleRenderResult(
409
file: string,
410
renderResult: RenderResult,
411
) {
412
// print output created
413
const finalOutput = renderResultFinalOutput(
414
renderResult,
415
dirname(file),
416
);
417
if (!finalOutput) {
418
throw new Error("No output created by quarto render " + basename(file));
419
}
420
info("Output created: " + finalOutput + "\n");
421
return finalOutput;
422
}
423
424
export interface RenderForPreviewResult {
425
file: string;
426
format: Format;
427
outputFile: string;
428
extensionFiles: string[];
429
resourceFiles: string[];
430
}
431
432
export async function renderForPreview(
433
file: string,
434
services: RenderServices,
435
flags: RenderFlags,
436
pandocArgs: string[],
437
project?: ProjectContext,
438
): Promise<RenderForPreviewResult> {
439
// Invalidate file cache for the file being rendered so changes are picked up.
440
// The project context persists across re-renders in preview mode, but the
441
// fileInformationCache contains file content that needs to be refreshed.
442
// Uses invalidateForFile() to also clean up transient notebook files
443
// (.quarto_ipynb) from disk before removing the cache entry (#14281).
444
if (project?.fileInformationCache) {
445
project.fileInformationCache.invalidateForFile(file);
446
}
447
448
// render
449
const renderResult = await render(file, {
450
services,
451
flags,
452
pandocArgs: pandocArgs,
453
previewServer: true,
454
setProjectDir: project !== undefined,
455
}, project);
456
if (renderResult.error) {
457
throw renderResult.error;
458
}
459
460
// print output created
461
const finalOutput = handleRenderResult(file, renderResult);
462
463
// notify user we are watching for reload
464
printWatchingForChangesMessage();
465
466
// determine files to watch for reload -- take the resource
467
// files detected during render, chase down additional references
468
// in css files, then filter out the _files dir
469
file = normalizePath(file);
470
const filesDir = join(dirname(file), inputFilesDir(file));
471
const resourceFiles = renderResult.files.reduce(
472
(resourceFiles: string[], file: RenderResultFile) => {
473
const resources = file.resourceFiles.concat(
474
cssFileResourceReferences(file.resourceFiles),
475
);
476
return resourceFiles.concat(
477
resources.filter((resFile) => !resFile.startsWith(filesDir)),
478
);
479
},
480
[],
481
);
482
483
// extension files
484
const extensionFiles = extensionFilesFromDirs(
485
inputExtensionDirs(file, project?.dir),
486
);
487
// shortcodes and filters (treat as extension files)
488
extensionFiles.push(...renderResult.files.reduce(
489
(extensionFiles: string[], file: RenderResultFile) => {
490
const shortcodes = file.format.render.shortcodes || [];
491
const filters = (file.format.pandoc.filters || []).map((filter) =>
492
isPandocFilter(filter) ? filter.path : filter
493
);
494
const ipynbFilters = file.format.execute["ipynb-filters"] || [];
495
[...shortcodes, ...filters.map((filter) => filter), ...ipynbFilters]
496
.forEach((extensionFile) => {
497
if (!isAbsolute(extensionFile)) {
498
const extensionFullPath = join(dirname(file.input), extensionFile);
499
if (existsSync(extensionFullPath)) {
500
extensionFiles.push(normalizePath(extensionFullPath));
501
}
502
}
503
});
504
return extensionFiles;
505
},
506
[],
507
));
508
509
return {
510
file,
511
format: renderResult.files[0].format,
512
outputFile: join(dirname(file), finalOutput),
513
extensionFiles,
514
resourceFiles,
515
};
516
}
517
518
export interface ChangeHandler {
519
render: () => Promise<RenderForPreviewResult | undefined>;
520
}
521
522
export function createChangeHandler(
523
result: RenderForPreviewResult,
524
reloader: { reloadClients: (reloadTarget?: string) => Promise<void> },
525
render: (to?: string) => Promise<RenderForPreviewResult | undefined>,
526
renderOnChange: boolean,
527
reloadFileFilter: (file: string) => boolean = () => true,
528
ignoreChanges?: (files: string[]) => boolean,
529
): ChangeHandler {
530
const renderQueue = new PromiseQueue<RenderForPreviewResult | undefined>();
531
let watcher: Watcher | undefined;
532
let lastResult = result;
533
534
// render handler
535
const renderHandler = async (to?: string) => {
536
try {
537
// if we have an alternate format then stop the watcher (as the alternate
538
// output format will be one of the watched resource files!)
539
if (to && watcher) {
540
watcher.stop();
541
}
542
// render
543
const result = await renderQueue.enqueue(async () => {
544
return render(to);
545
}, true);
546
if (result) {
547
sync(result);
548
}
549
550
return result;
551
} catch (e) {
552
if (e instanceof Error && e.message) {
553
// jupyter notebooks being edited in juptyerlab sometimes get an
554
// "Unexpected end of JSON input" error that remedies itself (so we ignore).
555
// this may be a result of an intermediate save result?
556
if (
557
isJupyterNotebook(result.file) &&
558
e.message.indexOf("Unexpected end of JSON input") !== -1
559
) {
560
return;
561
}
562
563
logError(e);
564
}
565
}
566
};
567
568
const sync = (result: RenderForPreviewResult) => {
569
const requiresSync = !watcher || resultRequiresSync(result, lastResult);
570
lastResult = result;
571
if (requiresSync) {
572
if (watcher) {
573
watcher.stop();
574
}
575
576
const watches: Watch[] = [];
577
if (renderOnChange) {
578
watches.push({
579
files: [result.file],
580
handler: ld.debounce(renderHandler, 50),
581
});
582
}
583
584
// re-render on extension change (as a mere reload won't reflect
585
// the changes as they do w/ e.g. css files)
586
watches.push({
587
files: result.extensionFiles,
588
handler: ld.debounce(renderHandler, 50),
589
});
590
591
// reload on output or resource changed (but wait for
592
// the render queue to finish, as sometimes pdfs are
593
// modified and even removed by pdflatex during render)
594
const reloadFiles = isPdfContent(result.outputFile)
595
? pdfReloadFiles(result)
596
: resultReloadFiles(result);
597
const reloadTarget = isPdfContent(result.outputFile)
598
? "/" + kPdfJsInitialPath
599
: "";
600
601
// https://github.com/quarto-dev/quarto-cli/issues/9547
602
// ... this fix means we'll never be able to support files
603
// fix question marks or octothorpes in their names
604
const removeUrlFragment = (file: string) =>
605
file.replace(/#.*$/, "").replace(/\?.*$/, "");
606
watches.push({
607
files: reloadFiles.filter(reloadFileFilter).map(removeUrlFragment),
608
handler: ld.debounce(async () => {
609
await renderQueue.enqueue(async () => {
610
await reloader.reloadClients(reloadTarget);
611
return undefined;
612
});
613
}, 50),
614
});
615
616
watcher = previewWatcher(watches, ignoreChanges);
617
watcher.start();
618
}
619
};
620
sync(result);
621
return {
622
render: renderHandler,
623
};
624
}
625
626
interface Watch {
627
files: string[];
628
handler: () => Promise<void>;
629
}
630
631
interface Watcher {
632
start: VoidFunction;
633
stop: VoidFunction;
634
}
635
636
function previewWatcher(
637
watches: Watch[],
638
ignoreChanges?: (files: string[]) => boolean,
639
): Watcher {
640
existsSync;
641
watches = watches.map((watch) => {
642
return {
643
...watch,
644
files: watch.files.filter((s) => existsSync(s)).map((file) => {
645
return normalizePath(file);
646
}),
647
};
648
});
649
const handlerForFile = (file: string) => {
650
const watch = watches.find((watch) => watch.files.includes(file));
651
return watch?.handler;
652
};
653
654
// create the watcher
655
const files = watches.flatMap((watch) => watch.files);
656
const fsWatcher = watchForFileChanges(files);
657
const watchForChanges = async () => {
658
for await (const event of fsWatcher) {
659
try {
660
if (
661
event.kind === "modify" &&
662
(!ignoreChanges || !ignoreChanges(event.paths))
663
) {
664
const handlers = new Set<() => Promise<void>>();
665
event.paths.forEach((path) => {
666
const handler = handlerForFile(path);
667
if (handler && !handlers.has(handler)) {
668
handlers.add(handler);
669
}
670
});
671
for (const handler of handlers) {
672
await handler();
673
}
674
}
675
} catch (e) {
676
logError(e);
677
}
678
}
679
};
680
681
return {
682
start: watchForChanges,
683
stop: () => fsWatcher.close(),
684
};
685
}
686
687
function projectHtmlFileRequestHandler(
688
context: ProjectContext,
689
inputFile: string,
690
flags: RenderFlags,
691
format: Format,
692
reloader: HttpDevServer,
693
renderHandler: (to?: string) => Promise<RenderForPreviewResult | undefined>,
694
) {
695
return httpFileRequestHandler(
696
htmlFileRequestHandlerOptions(
697
projectOutputDir(context),
698
"index.html",
699
inputFile,
700
flags,
701
format,
702
reloader,
703
renderHandler,
704
context,
705
),
706
);
707
}
708
709
function htmlFileRequestHandler(
710
htmlFile: string,
711
inputFile: string,
712
flags: RenderFlags,
713
format: Format,
714
reloader: HttpDevServer,
715
renderHandler: (to?: string) => Promise<RenderForPreviewResult | undefined>,
716
context: ProjectContext,
717
) {
718
return httpFileRequestHandler(
719
htmlFileRequestHandlerOptions(
720
dirname(htmlFile),
721
basename(htmlFile),
722
inputFile,
723
flags,
724
format,
725
reloader,
726
renderHandler,
727
context,
728
),
729
);
730
}
731
732
function htmlFileRequestHandlerOptions(
733
baseDir: string,
734
defaultFile: string,
735
inputFile: string,
736
flags: RenderFlags,
737
format: Format,
738
devserver: HttpDevServer,
739
renderHandler: (to?: string) => Promise<RenderForPreviewResult | undefined>,
740
project: ProjectContext,
741
): HttpFileRequestOptions {
742
// if we an alternate format on the fly we need to do a full re-render
743
// to get the correct state back. this flag will be set whenever
744
// we render an alternate format
745
let invalidateDevServerReRender = false;
746
return {
747
baseDir,
748
defaultFile,
749
printUrls: "404",
750
onRequest: async (req: Request) => {
751
if (devserver.handle(req)) {
752
return Promise.resolve(devserver.request(req));
753
} else if (isPreviewTerminateRequest(req)) {
754
exitWithCleanup(0);
755
} else if (req.url.endsWith("/quarto-render/")) {
756
// don't wait for the promise so the
757
// caller gets an immediate reply
758
renderHandler();
759
return Promise.resolve(httpContentResponse("rendered"));
760
} else if (isPreviewRenderRequest(req)) {
761
const outputFile = format.pandoc[kOutputFile];
762
const prevReq = previewRenderRequest(
763
req,
764
!isBrowserPreviewable(outputFile) || devserver.hasClients(),
765
);
766
if (
767
!invalidateDevServerReRender &&
768
prevReq &&
769
existsSync(prevReq.path) &&
770
normalizePath(prevReq.path) === normalizePath(inputFile) &&
771
await previewRenderRequestIsCompatible(prevReq, project, flags.to)
772
) {
773
// don't wait for the promise so the
774
// caller gets an immediate reply
775
renderHandler();
776
return Promise.resolve(httpContentResponse("rendered"));
777
} else {
778
return Promise.resolve(previewUnableToRenderResponse());
779
}
780
} else {
781
return Promise.resolve(undefined);
782
}
783
},
784
onFile: async (file: string, req: Request) => {
785
// check for static response
786
const staticResponse = await staticResource(baseDir, file);
787
if (staticResponse) {
788
const resolveBody = () => {
789
if (staticResponse.injectClient) {
790
const client = devserver.clientHtml(
791
req,
792
inputFile,
793
);
794
const contents = new TextDecoder().decode(
795
staticResponse.contents,
796
);
797
return staticResponse.injectClient(contents, client);
798
} else {
799
return staticResponse.contents;
800
}
801
};
802
const body = resolveBody();
803
804
return {
805
body,
806
contentType: staticResponse.contentType,
807
};
808
}
809
810
// the 'format' passed to this function is for the default
811
// render target, however this could be a request for another
812
// render target (e.g. a link in the 'More Formats' section)
813
// some of these formats might require rendering and/or may
814
// have extended preview behavior (e.g. preview-type: raw)
815
// in this case try to lookup the format and perform a render
816
let renderFormat = format;
817
if (project) {
818
const input = await inputFileForOutputFile(
819
project,
820
relative(baseDir, file),
821
);
822
if (input) {
823
renderFormat = input.format;
824
if (renderFormat !== format && fileRequiresRender(input.file, file)) {
825
invalidateDevServerReRender = true;
826
await renderHandler(renderFormat.identifier[kTargetFormat]);
827
}
828
}
829
}
830
831
// https://github.com/quarto-dev/quarto-cli/issues/5215
832
// return CORS requests as plain text so that OJS requests do
833
// not have formatting
834
if (
835
req.headers.get("sec-fetch-dest") === "empty" &&
836
req.headers.get("sec-fetch-mode") === "cors"
837
) {
838
return;
839
}
840
841
if (isHtmlContent(file)) {
842
// does the provide an alternate preview file?
843
if (renderFormat.formatPreviewFile) {
844
file = renderFormat.formatPreviewFile(file, renderFormat);
845
}
846
const fileContents = await Deno.readFile(file);
847
return devserver.injectClient(req, fileContents, inputFile);
848
} else if (isTextContent(file)) {
849
return previewTextContent(
850
file,
851
inputFile,
852
format,
853
req,
854
devserver.injectClient,
855
);
856
}
857
},
858
};
859
}
860
861
function fileRequiresRender(inputFile: string, outputFile: string) {
862
if (safeExistsSync(outputFile)) {
863
return (Deno.statSync(inputFile).mtime?.valueOf() || 0) >
864
(Deno.statSync(outputFile).mtime?.valueOf() || 0);
865
} else {
866
return true;
867
}
868
}
869
870
function resultReloadFiles(result: RenderForPreviewResult) {
871
return [result.outputFile].concat(result.resourceFiles);
872
}
873
874
function pdfFileRequestHandler(
875
pdfFile: string,
876
inputFile: string,
877
flags: RenderFlags,
878
format: Format,
879
port: number,
880
reloader: HttpDevServer,
881
renderHandler: () => Promise<RenderForPreviewResult | undefined>,
882
project: ProjectContext,
883
) {
884
// start w/ the html handler (as we still need it's http reload injection)
885
const pdfOptions = htmlFileRequestHandlerOptions(
886
dirname(pdfFile),
887
basename(pdfFile),
888
inputFile,
889
flags,
890
format,
891
reloader,
892
renderHandler,
893
project,
894
);
895
896
// pdf customizations
897
898
pdfOptions.baseDir = pdfJsBaseDir();
899
900
if (pdfOptions.onRequest) {
901
const onRequest = pdfOptions.onRequest;
902
pdfOptions.onRequest = async (req: Request) => {
903
if (new URL(req.url).pathname === "/") {
904
const url = isPositWorkbench()
905
? await rswURL(port, kPdfJsInitialPath)
906
: isVSCodeServer()
907
? vsCodeServerProxyUri()!.replace("{{port}}", `${port}`) +
908
kPdfJsInitialPath
909
: "/" + kPdfJsInitialPath;
910
return Promise.resolve(serveRedirect(url));
911
} else {
912
return Promise.resolve(onRequest(req));
913
}
914
};
915
}
916
917
pdfOptions.onFile = pdfJsFileHandler(() => pdfFile, pdfOptions.onFile);
918
919
return httpFileRequestHandler(pdfOptions);
920
}
921
922
function pdfReloadFiles(result: RenderForPreviewResult) {
923
return [result.outputFile];
924
}
925
926
function resultRequiresSync(
927
result: RenderForPreviewResult,
928
lastResult?: RenderForPreviewResult,
929
) {
930
if (!lastResult) {
931
return true;
932
}
933
return result.file !== lastResult.file ||
934
result.outputFile !== lastResult.outputFile ||
935
!ld.isEqual(result.extensionFiles, lastResult.extensionFiles) ||
936
!ld.isEqual(result.resourceFiles, lastResult.resourceFiles);
937
}
938
939