Skip to content

Commit dc476fa

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

36 files changed

+1314
-164
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: 105 additions & 18 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,31 +180,26 @@ 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))]
176191
{
177192
// Create the dest directory, since old CI OSes don't support `rsync
178193
// --mkpath`.
179-
run_script(
180-
"mkdir",
181-
&["-p", dest.as_ref().to_str().unwrap()],
182-
"./",
183-
true,
184-
)?;
194+
run_script("mkdir", &["-p", dest.to_str().unwrap()], "./", true)?;
185195
let mut tmp;
186196
let src_combined = match files.as_ref() {
187197
Some(files_) => {
188-
tmp = src.as_ref().to_os_string();
189-
tmp.push(files_);
198+
tmp = src.to_os_string();
199+
tmp.push(OsStr::new(files_.as_ref()));
190200
tmp.as_os_str()
191201
}
192-
None => src.as_ref(),
202+
None => src,
193203
};
194204

195205
// Use bash to perform globbing, since rsync doesn't do this.
@@ -199,7 +209,7 @@ fn quick_copy_dir<P: AsRef<OsStr>>(src: P, dest: P, files: Option<P>) -> io::Res
199209
format!(
200210
"rsync --archive --delete {} {}",
201211
&src_combined.to_str().unwrap(),
202-
&dest.as_ref().to_str().unwrap()
212+
&dest.to_str().unwrap()
203213
)
204214
.as_str(),
205215
]);
@@ -410,7 +420,7 @@ fn run_test() -> io::Result<()> {
410420
fn run_build() -> io::Result<()> {
411421
// Clean out all bundled files before the rebuild.
412422
remove_dir_all_if_exists("../client/static/bundled")?;
413-
run_script("npm", &["run", "build"], "../client", true)?;
423+
run_client_build(false)?;
414424
run_script("npm", &["run", "compile"], "../extensions/VSCode", true)?;
415425
run_cmd!(
416426
cargo build --manifest-path=../builder/Cargo.toml;
@@ -419,6 +429,82 @@ fn run_build() -> io::Result<()> {
419429
Ok(())
420430
}
421431

432+
// Build the NPM Client.
433+
fn run_client_build(
434+
// True to build for distribution, not development.
435+
dist: bool,
436+
) -> io::Result<()> {
437+
let esbuild = PathBuf::from_slash("node_modules/.bin/esbuild");
438+
let distflag = if dist { "--minify" } else { "--sourcemap" };
439+
// This makes the program work from either the `server/` or `client/` directories.
440+
let rel_path = "../client";
441+
442+
// The main build for the Client.
443+
run_script(
444+
&esbuild,
445+
&[
446+
"src/CodeChatEditorFramework.mts",
447+
"src/CodeChatEditor.mts",
448+
"src/CodeChatEditor-test.mts",
449+
"src/css/CodeChatEditorProject.css",
450+
"src/css/CodeChatEditor.css",
451+
"--bundle",
452+
"--outdir=./static/bundled",
453+
distflag,
454+
"--format=esm",
455+
"--splitting",
456+
"--metafile=meta.json",
457+
"--entry-names=[dir]/[name]-[hash]",
458+
],
459+
rel_path,
460+
true,
461+
)?;
462+
// <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.
463+
run_script(
464+
&esbuild,
465+
&[
466+
"src/pdf.js/viewer.mjs",
467+
"node_modules/pdfjs-dist/build/pdf.worker.mjs",
468+
"--bundle",
469+
"--outdir=./static/bundled",
470+
distflag,
471+
"--format=esm",
472+
"--loader:.png=dataurl",
473+
"--loader:.svg=dataurl",
474+
"--loader:.gif=dataurl",
475+
],
476+
rel_path,
477+
true,
478+
)?;
479+
// Copy over the cmap (color map?) files, which the bundler doesn't handle.
480+
quick_copy_dir(
481+
format!("{rel_path}/node_modules/pdfjs-dist/cmaps/"),
482+
format!("{rel_path}/static/bundled/node_modules/pdfjs-dist/cmaps/"),
483+
None,
484+
)?;
485+
// 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.
486+
run_script(
487+
&esbuild,
488+
&[
489+
"src/HashReader.mts",
490+
"--outdir=.",
491+
"--platform=node",
492+
"--format=esm",
493+
],
494+
rel_path,
495+
true,
496+
)?;
497+
run_script("node", &["HashReader.js"], rel_path, true)?;
498+
// Finally, check the TypeScript with the (slow) TypeScript compiler.
499+
run_script(
500+
PathBuf::from_slash("node_modules/.bin/tsc"),
501+
&["-noEmit"],
502+
rel_path,
503+
true,
504+
)?;
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/package.json

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@
66
"type": "module",
77
"scripts": {
88
"test": "echo \"Error: no test specified\" && exit 1",
9-
"build": "run-script-os",
10-
"build:win32": "node_modules\\.bin\\esbuild src/HashReader.mts --outdir=. --platform=node --format=esm && node_modules\\.bin\\esbuild src/CodeChatEditorFramework.mts src/CodeChatEditor.mts src/CodeChatEditor-test.mts src/css/CodeChatEditorProject.css src/css/CodeChatEditor.css --bundle --outdir=./static/bundled --sourcemap --format=esm --splitting --metafile=meta.json --entry-names=[dir]/[name]-[hash] && node HashReader.js && tsc -noEmit",
11-
"build:default": "node_modules/.bin/esbuild src/HashReader.mts --outdir=. --platform=node --format=esm && node_modules/.bin/esbuild src/CodeChatEditorFramework.mts src/CodeChatEditor.mts src/CodeChatEditor-test.mts src/css/CodeChatEditorProject.css src/css/CodeChatEditor.css --bundle --outdir=./static/bundled --sourcemap --format=esm --splitting --metafile=meta.json --entry-names=[dir]/[name]-[hash] && node HashReader.js && tsc -noEmit",
12-
"dist": "run-script-os",
13-
"dist:win32": "node_modules\\.bin\\esbuild src/HashReader.mts --outdir=. --platform=node --format=esm && node_modules\\.bin\\esbuild src/CodeChatEditorFramework.mts src/CodeChatEditor.mts src/CodeChatEditor-test.mts src/css/CodeChatEditorProject.css src/css/CodeChatEditor.css --bundle --outdir=./static/bundled --minify --format=esm --splitting --metafile=meta.json --entry-names=[dir]/[name]-[hash] && node HashReader.js && tsc -noEmit",
14-
"dist:default": "node_modules/.bin/esbuild src/HashReader.mts --outdir=. --platform=node --format=esm && node_modules/.bin/esbuild src/CodeChatEditorFramework.mts src/CodeChatEditor.mts src/CodeChatEditor-test.mts src/css/CodeChatEditorProject.css src/css/CodeChatEditor.css --bundle --outdir=./static/bundled --minify --format=esm --splitting --metafile=meta.json --entry-names=[dir]/[name]-[hash] && node HashReader.js && tsc -noEmit"
9+
"build": "cargo run --manifest-path=../builder/Cargo.toml client-build",
10+
"dist": "cargo run --manifest-path=../builder/Cargo.toml client-build --dist"
1511
},
1612
"keywords": [],
1713
"author": "Bryan A. Jones",
@@ -29,7 +25,6 @@
2925
"eslint-config-prettier": "^10",
3026
"eslint-plugin-import": "^2",
3127
"eslint-plugin-prettier": "^5",
32-
"run-script-os": "^1",
3328
"typescript": "^5"
3429
},
3530
"dependencies": {
@@ -52,6 +47,7 @@
5247
"mathjax-modern-font": "4.0.0-beta.7",
5348
"mermaid": "^11",
5449
"npm-check-updates": "^17.1.15",
50+
"pdfjs-dist": "^5",
5551
"tinymce": "^7"
5652
},
5753
"repository": {

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

0 commit comments

Comments
 (0)