Skip to content

Commit 70adb2d

Browse files
committed
Add: Client Simple Viewer.
View PDFs in VSCode using PDF.js.
1 parent 9d03d33 commit 70adb2d

35 files changed

+1302
-148
lines changed

builder/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ edition = "2024"
2727
clap = { version = "4.5.19", features = ["derive"] }
2828
cmd_lib = "1.9.5"
2929
current_platform = "0.2.0"
30+
path-slash = "0.2.1"
3031
regex = "1.11.1"

builder/src/main.rs

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,18 @@
3131
// -------
3232
//
3333
// ### Standard library
34-
use std::{ffi::OsStr, fs, io, path::Path, process::Command};
34+
use std::{
35+
ffi::OsStr,
36+
fs, io,
37+
path::{Path, PathBuf},
38+
process::Command,
39+
};
3540

3641
// ### Third-party
3742
use clap::{Parser, Subcommand};
3843
use cmd_lib::run_cmd;
3944
use current_platform::CURRENT_PLATFORM;
45+
use path_slash::PathBufExt;
4046
use regex::Regex;
4147

4248
// ### Local
@@ -68,6 +74,12 @@ enum Commands {
6874
Test,
6975
/// Build everything.
7076
Build,
77+
/// Build the Client.
78+
ClientBuild {
79+
/// True to build for distribution, instead of development.
80+
#[arg(short, long, default_value_t = false)]
81+
dist: bool,
82+
},
7183
/// Change the version for the client, server, and extensions.
7284
ChangeVersion {
7385
/// The new version number, such as "0.1.1".
@@ -98,17 +110,18 @@ enum Commands {
98110
// These functions are called by the build support functions.
99111
/// On Windows, scripts must be run from a shell; on Linux and OS X, scripts are
100112
/// directly executable. This function runs a script regardless of OS.
101-
fn run_script<T: AsRef<OsStr>, P: AsRef<Path> + std::fmt::Display>(
113+
fn run_script<T: AsRef<Path>, A: AsRef<OsStr>, P: AsRef<Path> + std::fmt::Display>(
102114
// The script to run.
103115
script: T,
104116
// Arguments to pass.
105-
args: &[T],
117+
args: &[A],
106118
// The directory to run the script in.
107119
dir: P,
108120
// True to report errors based on the process' exit code; false to ignore
109121
// the code.
110122
check_exit_code: bool,
111123
) -> io::Result<()> {
124+
let script = OsStr::new(script.as_ref());
112125
let mut process;
113126
if cfg!(windows) {
114127
process = Command::new("cmd");
@@ -136,9 +149,11 @@ fn run_script<T: AsRef<OsStr>, P: AsRef<Path> + std::fmt::Display>(
136149
/// programs (`robocopy`/`rsync`) to accomplish this. Very important: the `src`
137150
/// **must** end with a `/`, otherwise the Windows and Linux copies aren't
138151
/// identical.
139-
fn quick_copy_dir<P: AsRef<OsStr>>(src: P, dest: P, files: Option<P>) -> io::Result<()> {
152+
fn quick_copy_dir<P: AsRef<Path>>(src: P, dest: P, files: Option<P>) -> io::Result<()> {
140153
assert!(src.as_ref().to_string_lossy().ends_with('/'));
141154
let mut copy_process;
155+
let src = OsStr::new(src.as_ref());
156+
let dest = OsStr::new(dest.as_ref());
142157
#[cfg(windows)]
143158
{
144159
// From `robocopy /?`:
@@ -165,11 +180,11 @@ fn quick_copy_dir<P: AsRef<OsStr>>(src: P, dest: P, files: Option<P>) -> io::Res
165180
.args([
166181
"/MIR", "/MT", "/NFL", "/NDL", "/NJH", "/NJS", "/NP", "/NS", "/NC",
167182
])
168-
.arg(&src)
169-
.arg(&dest);
183+
.arg(src)
184+
.arg(dest);
170185
// Robocopy expects the files to copy after the dest.
171186
if let Some(files_) = &files {
172-
copy_process.arg(files_);
187+
copy_process.arg(OsStr::new(files_.as_ref()));
173188
}
174189
}
175190
#[cfg(not(windows))]
@@ -186,7 +201,7 @@ fn quick_copy_dir<P: AsRef<OsStr>>(src: P, dest: P, files: Option<P>) -> io::Res
186201
let src_combined = match files.as_ref() {
187202
Some(files_) => {
188203
tmp = src.as_ref().to_os_string();
189-
tmp.push(files_);
204+
tmp.push(OsStr::new(files_.as_ref()));
190205
tmp.as_os_str()
191206
}
192207
None => src.as_ref(),
@@ -410,7 +425,7 @@ fn run_test() -> io::Result<()> {
410425
fn run_build() -> io::Result<()> {
411426
// Clean out all bundled files before the rebuild.
412427
remove_dir_all_if_exists("../client/static/bundled")?;
413-
run_script("npm", &["run", "build"], "../client", true)?;
428+
run_client_build(false)?;
414429
run_script("npm", &["run", "compile"], "../extensions/VSCode", true)?;
415430
run_cmd!(
416431
cargo build --manifest-path=../builder/Cargo.toml;
@@ -419,6 +434,77 @@ fn run_build() -> io::Result<()> {
419434
Ok(())
420435
}
421436

437+
// Build the NPM Client.
438+
fn run_client_build(
439+
// True to build for distribution, not development.
440+
dist: bool,
441+
) -> io::Result<()> {
442+
let esbuild = PathBuf::from_slash("node_modules/.bin/esbuild");
443+
let distflag = if dist { "--minify" } else { "--sourcemap" };
444+
// This makes the program work from either the `server/` or `client/` directories.
445+
let rel_path = "../client";
446+
447+
// The main build for the Client.
448+
run_script(
449+
&esbuild,
450+
&[
451+
"src/CodeChatEditorFramework.mts",
452+
"src/CodeChatEditor.mts",
453+
"src/CodeChatEditor-test.mts",
454+
"src/css/CodeChatEditorProject.css",
455+
"src/css/CodeChatEditor.css",
456+
"--bundle",
457+
"--outdir=./static/bundled",
458+
distflag,
459+
"--format=esm",
460+
"--splitting",
461+
"--metafile=meta.json",
462+
"--entry-names=[dir]/[name]-[hash]",
463+
],
464+
rel_path,
465+
true,
466+
)?;
467+
// <a id="#pdf.js></a>The PDF viewer for use with VSCode. Built it separately, since it's loaded apart from the rest of the Client.
468+
run_script(
469+
&esbuild,
470+
&[
471+
"src/pdf.js/viewer.mjs",
472+
"node_modules/pdfjs-dist/build/pdf.worker.mjs",
473+
"--bundle",
474+
"--outdir=./static/bundled",
475+
distflag,
476+
"--format=esm",
477+
"--loader:.png=dataurl",
478+
"--loader:.svg=dataurl",
479+
"--loader:.gif=dataurl",
480+
],
481+
rel_path,
482+
true,
483+
)?;
484+
// Copy over the cmap (color map?) files, which the bundler doesn't handle.
485+
quick_copy_dir(
486+
format!("{rel_path}/node_modules/pdfjs-dist/cmaps/"),
487+
format!("{rel_path}/static/bundled/node_modules/pdfjs-dist/cmaps/"),
488+
None,
489+
)?;
490+
// The HashReader isn't bundled; instead, it's used to translate the JSON metafile produced by the main esbuild run to the simpler format used by the CodeChat Editor. TODO: rewrite this in Rust.
491+
run_script(
492+
&esbuild,
493+
&[
494+
"src/HashReader.mts",
495+
"--outdir=.",
496+
"--platform=node",
497+
"--format=esm",
498+
],
499+
rel_path,
500+
true,
501+
)?;
502+
run_script("node", &["HashReader.js"], rel_path, true)?;
503+
// Finally, check the TypeScript with the (slow) TypeScript compiler.
504+
run_script("node_modules\\.bin\\tsc", &["-noEmit"], rel_path, true)?;
505+
Ok(())
506+
}
507+
422508
fn run_change_version(new_version: &String) -> io::Result<()> {
423509
let replacement_string = format!("${{1}}{new_version}${{2}}");
424510
search_and_replace_file(
@@ -490,6 +576,7 @@ impl Cli {
490576
Commands::Update => run_update(),
491577
Commands::Test => run_test(),
492578
Commands::Build => run_build(),
579+
Commands::ClientBuild { dist } => run_client_build(*dist),
493580
Commands::ChangeVersion { new_version } => run_change_version(new_version),
494581
Commands::Prerelease => run_prerelease(),
495582
Commands::Postrelease { target, .. } => run_postrelease(target),

client/src/CodeChatEditor.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ const on_navigate = (navigateEvent: NavigateEvent) => {
422422
return;
423423
}
424424

425-
// If the IDE initiated this navigation via a`CurrentFile` message, then
425+
// If the IDE initiated this navigation via a `CurrentFile` message, then
426426
// allow it.
427427
if (window.CodeChatEditor.allow_navigation) {
428428
// We don't need to reset this flag, since this window will be reloaded.

client/src/CodeChatEditorFramework.mts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type ResultType = { Ok: "Void" } | { Err: string };
5050

5151
interface EditorMessageContents {
5252
Update?: UpdateMessageContents;
53-
CurrentFile?: string;
53+
CurrentFile?: [string, boolean?];
5454
RequestClose?: null;
5555
OpenUrl?: string,
5656
Result?: ResultType;
@@ -160,7 +160,8 @@ class WebSocketComm {
160160
break;
161161

162162
case "CurrentFile":
163-
const current_file = value as string;
163+
// Note that we can ignore `value[1]` (if the file is text or binary); the server only sends text files here.
164+
const current_file = value[0] as string;
164165
// If the page is still loading, then don't save. Otherwise,
165166
// save the editor contents if necessary.
166167
let cce = get_client();
@@ -274,7 +275,7 @@ class WebSocketComm {
274275
current_file = (url: URL) => {
275276
// If this points to the Server, then tell the IDE to load a new file.
276277
if (url.host === window.location.host) {
277-
this.send_message({ CurrentFile: url.toString() }, () => {
278+
this.send_message({ CurrentFile: [url.toString(), undefined] }, () => {
278279
this.set_root_iframe_src(url.toString());
279280
});
280281
} else {
@@ -308,7 +309,8 @@ const get_client = () => root_iframe?.contentWindow?.CodeChatEditor;
308309
const set_content = (contents: CodeChatForWeb) => {
309310
let client = get_client();
310311
if (client === undefined) {
311-
let cw = root_iframe!.contentWindow!;
312+
// See if this is the [simple viewer](#Client-simple-viewer). Otherwise, it's just the bare document to replace.
313+
const cw = (root_iframe!.contentDocument?.getElementById("CodeChat-contents") as HTMLIFrameElement | undefined)?.contentWindow ?? root_iframe!.contentWindow!;
312314
cw.document.open();
313315
cw.document.write(contents.source.doc);
314316
cw.document.close();

client/src/css/CodeChatEditorProject.css

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,31 @@
1717
[http://www.gnu.org/licenses/](http://www.gnu.org/licenses/).
1818
1919
`CodeChatEditorProject.css` -- Styles for the CodeChat Editor for projects
20-
========================================================================== */
20+
==========================================================================
21+
22+
This is used only to store a reused variable value. See the [CSS
23+
docs](https://drafts.csswg.org/css-variables/). */
2124
:root {
2225
--sidebar-width: 15rem;
26+
--body-padding: 0.2rem;
27+
}
28+
29+
/* See [box sizing](https://css-tricks.com/box-sizing/) for the following
30+
technique to use `border-box` sizing. */
31+
html {
32+
box-sizing: border-box;
33+
}
34+
35+
*,
36+
*:before,
37+
*:after {
38+
box-sizing: inherit;
2339
}
2440

2541
body {
42+
/* For box model simplicity, switch the padding and margin. */
43+
padding: var(--body-padding);
44+
margin: 0px;
2645
overflow: hidden;
2746
}
2847

client/src/pdf.js/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Overview
2+
3+
Example to demonstrate PDF.js library usage with a viewer optimized for mobile usage.
4+
5+
## Getting started
6+
7+
Build PDF.js using `gulp dist-install` and run `gulp server` to start a web server.
8+
You can then work with the mobile viewer at
9+
http://localhost:8888/examples/mobile-viewer/viewer.html.
10+
11+
Refer to `viewer.js` for the source code of the mobile viewer.
169 Bytes
Loading
185 Bytes
Loading
295 Bytes
Loading
166 Bytes
Loading

0 commit comments

Comments
 (0)