A ReactJS version of the Pretext UI
This interface is a replacement for the current jquery-based UI for Pretext books when rendered on the web.
To enable the react interface for your PreTeXt project, you need to set the react.debug.global
(or react.debug.local
) to yes
. You can do this by adding the following inside a your html <target>
element.
<stringparam key="debug.react.global" value="yes"/>
and recompile your book. The recompiled version will be using the React css and Javascript served directly from github.
To get started, make sure you have nodejs
(>= v16) and npm
(Node Package Manager; should
be installed by default with Node) installed. Then, in the pretext-react
directory, run
git submodule init
git submodule update
npm install
npm run start
to make a live dev-server. After this, when you update code, node
will tell your
web browser to automatically reload with the new code. However, this may cause issues with
internally-cached data, so you may have to refresh the web browser.
If you want to run your book to use the local version of your react build, set the debug.react.local
to yes
instead of of debug.react.global
.
Windows file systems do not have symbolic links, and so the public/
directory, which should be
a symbolic link, will show as a file rather than a link. The easiest workaround is to use WSL2
(Windows Subsystem for Linux 2), which will emulate a Linux environment and file system, and to run
nodejs
inside of that environment. Alternatively, you can explicitly copy a PreTeXt output folder
to public/
(thereby avoiding the symbolic link issue).
By default, pretext-react
has a copy of precompiled versions of the pretext sample article and book, symbolically
linked from the pretext-react-compiled-article
subdirectory. By default public/
is refers to the pretext sample article.
To view the sample book, navigate to /pretext-react-compiled-article/html-book-dev/index.html
. To view the testing document (used for unit tests),
navigate to /pretext-react-compiled-article/html-testing/index.html
.
To test out your own content, create a folder or symbolic link in the root project directory with your
compiled pretext (make sure you have debug.react.local
set to yes
). Then, run npm run start
and navigate to
/your-folder/index.html
. (By default, you will be sent to /public/index.html
, which points to the pre-compiled sample
article.)
To build run
npm run build
The resulting build will be placed in a build/
folder. The Javascript output will be located
at build/static/js/main.js
and the css will be located at build/static/css/main.css
. These
scripts assume that all of your Pretext html
files have
<script type="module" defer="defer" src="/static/js/main.js"></script>
<link href="./static/css/main.css" rel="stylesheet" />
in their <header>
.
If you want to use the React frontend with a specific Pretext source, you need to compile it with the following
stringparams
* `debug.react.local yes`
* `html-css-shellfile shell_min.css`
* `html-css-bannerfile banner_min.css`
* `html-css-tocfile toc_min.css`
* `html-css-navbarfile navbar_min.css`
Using xsltproc
this might look something like
xsltproc --stringparam debug.react.local yes --stringparam html-css-shellfile shell_min.css --stringparam html-css-bannerfile banner_min.css --stringparam html-css-tocfile toc_min.css --stringparam html-css-navbarfile navbar_min.css -stringparam debug.datedfiles no -xinclude ../tmp/pretext/xsl/pretext-html.xsl ../src/html-testing/source/main.ptx
pretext-react
is written in TypeScript, specifically the TSX variant of typescript.
TSX allows a mixture of TypeScript code and what appears to be HTML. It is a TypeScript variant of the popular JSX. See here
for a short introduction to JSX.
pretext-react
replaces the "shell" around the pretext content. Additionally, it is responsible
for the interactive knowl elements and navigation components.
When the page is loaded, pretext-react
does the following.
-
Extracts necessary information from the current page (mainly the body content) and fetches
doc-manifest.xml
which contains table of contents information. -
Renders the "shell" (the TOC and nav buttons).
-
Processes and display the page contents. React likes to "own" the page that it renders. For this reason, we process the source with the unifiedjs framework and call the code in
hast-react.ts
to convert the HAST (HTML Abstract Syntax Tree) into a tree of React components. Along the way, certain components are replaced with their full-featured equivalents. For example, links are replaced with<InternalLink />
elements (provided they are internal links!). These replaces are located insrc/components/replaces
.
Of note, when the user clicks on a knowl, it is handled by React components (the ones coming from replacers/knowls.tsx
).
Redux via Redux Toolkit is used to manage global state. It allows several components to access the same data while staying in sync. It also minimizes re-renders by asking components to re-render only when their data changes.
If you install Redux Devtools for Firefox or Chrome you can inspect the contents of the page's global store.
In the Redux global store, table of contents information/navigation information is stored. In addition, the HTML source of each cached page is stored. This means the page doesn't need to be re-downloaded when the user returns to it!
The code is written using React Function-based components (not class-based components). Files aim to be less than 200 lines of code
with each file containing one component (or a couple closely-related components). The TypeScript compiler will combine code
that is imported via import
statements into a single file in the end, so there is no concern that the end-user will have to load
hundreds of files.
Here's a brief overview of the important files and folders:
-
index.tsx
- This is the entry point for the App. It sets up only what is required to get the app started (e.g., stuff that needs to be done before the app starts). -
components-for-page/shell.tsx
- This is what renders the app. Every component that you see is a child of a component from here. -
state-management/
- Setup for Redux/global state management. -
state-management/redux-slices/
- Global state management. Each "piece" of state is broken off into its own features. For instance, state related to caching vs. state related to the TOC, etc. -
components-for-shell/
- This is where the React components go which are used for the Shell, that is the non-text part of the page (e.g., the table of contents, the nav buttons, etc.) -
components-for-page/
- This is where the components used in the page's contents go. E.g., the components which render knowls, etc. are located here. -
components-common/
- This is where the components that are used in both the shell and the page content are. These are generally more abstract components. -
replacers/
- Replacers manipulate the HTML tree and insert React components where needed. See the README in thereplacers/
folder for more details.
Automated testing is done with Jest and the Puppeteer library, which runs a headless
version of Chrome that can be interacted with via Javascript. This allows for full UI tests that measure actual
browser behavior. These tests are called End to End (e2e
) tests and are located in tests/*e2e.test.js
.
To run the tests, you first must build the react interface with
npm run build
To run the tests one time (like is done on the CI server), run
npm run test:e2e-with-setup-and-teardown
In development it is useful to have tests re-run every time you modify a test file. This can be done via
npm run test:serve
npm run test:e2e-watch
This will rerun the tests every time a file is changed (though if you change a React file, you will need to rerun npm run build
).
It is often helpful to run the test server (npm run test:serve
) and the tests (npm run test:e2e-watch
) in separate terminals
so their output is not intermixed.
The test server run with npm run test:serve
will serve files by default from pretext-react-compiled-article/html-testing
and will fall back to build/
if the files cannot be found.
Headless testing is hard because you don't really know what the page "looks like" at a particular time. However, adding
await page.screenshot({ path: `test-screenshot.png` });
will save a snapshot (image) of the current page state for extra debugging ease!