├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── epubs ├── make-epub.sh └── portable-epubs │ ├── EPUB │ ├── img │ │ ├── adobe-digital-edition.jpg │ │ ├── after-resize.jpg │ │ ├── apple-books.jpg │ │ ├── before-resize.jpg │ │ ├── bene.png │ │ ├── history-of-writing-kindle.jpg │ │ ├── history-of-writing-pdf.jpg │ │ ├── kindle.jpg │ │ └── tags.jpg │ ├── index.xhtml │ ├── nav.xhtml │ └── package.opf │ └── META-INF │ └── container.xml ├── js ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.cjs ├── package.json ├── packages │ ├── bene-components │ │ ├── .eslintrc.cjs │ │ ├── package.json │ │ ├── src │ │ │ ├── block-links.scss │ │ │ ├── block-links.ts │ │ │ ├── code-description.scss │ │ │ ├── code-description.ts │ │ │ ├── dfn-links.scss │ │ │ ├── dfn-links.ts │ │ │ ├── main.ts │ │ │ ├── resize-handle.scss │ │ │ ├── resize-handle.ts │ │ │ └── syntax-highlight.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── bene-desktop │ │ ├── .eslintrc.cjs │ │ ├── index.html │ │ ├── package.json │ │ ├── public │ │ │ └── bene-reader │ │ ├── src │ │ │ └── index.tsx │ │ ├── styles │ │ │ └── index.css │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── bene-reader │ │ ├── .eslintrc.cjs │ │ ├── img │ │ │ ├── toolbarButton-download.svg │ │ │ ├── toolbarButton-pageDown.svg │ │ │ ├── toolbarButton-pageUp.svg │ │ │ ├── toolbarButton-sidebarToggle.svg │ │ │ ├── toolbarButton-zoomIn.svg │ │ │ └── toolbarButton-zoomOut.svg │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ └── index.tsx │ │ ├── styles │ │ │ ├── content.scss │ │ │ ├── index.scss │ │ │ └── nav.scss │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── bene-web │ │ ├── .eslintrc.cjs │ │ ├── build.mjs │ │ ├── index.html │ │ ├── package.json │ │ ├── public │ │ └── bene-reader │ │ ├── rs-utils │ │ ├── .gitignore │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ │ ├── src │ │ └── index.tsx │ │ ├── styles │ │ └── index.css │ │ ├── tsconfig.json │ │ ├── vite.config.ts │ │ └── worker.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── typedoc.json └── rs ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── crates ├── bene-app ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ └── main.rs └── tauri.conf.json └── bene-epub ├── Cargo.toml └── src ├── lib.rs ├── xhtml.rs └── zip.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | tags-ignore: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - "**" 12 | 13 | jobs: 14 | CI: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v3 19 | 20 | - name: Install wasm-pack 21 | uses: baptiste0928/cargo-install@v2 22 | with: 23 | crate: wasm-pack 24 | 25 | - name: Install Depot 26 | run: curl https://raw.githubusercontent.com/cognitive-engineering-lab/depot/main/scripts/install.sh | sh 27 | 28 | - name: Install system dependencies 29 | run: sudo apt-get install -y libgtk-3-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev 30 | 31 | - name: Test frontend 32 | run: depot test 33 | working-directory: js 34 | 35 | - name: Test backend 36 | run: cargo test 37 | working-directory: rs 38 | 39 | - name: Build Wasm package 40 | run: | 41 | cd epubs && ./make-epub.sh portable-epubs && cd .. 42 | cd js && mkdir -p packages/bene-web/public/epubs/ && ln -s $PWD/../epubs/portable-epubs.epub packages/bene-web/public/epubs/ 43 | depot -p bene-web build --release 44 | 45 | - name: Deploy to Github Pages 46 | uses: peaceiris/actions-gh-pages@v3 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | publish_dir: js/packages/bene-web/dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.epub -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bene: An EPUB Reading System 2 | 3 | Bene is a reading system for documents written in the [EPUB](https://www.w3.org/TR/epub-overview-33/) file format. You can try a live demo of Bene here: 4 | 5 | **Development Status:** Bene is a research prototype. Don't expect it to work reliably yet. 6 | 7 | ## Setup 8 | 9 | Currently, the only supported setup is installation from source. You will need at least [Rust](https://rustup.rs/) and [Depot](https://github.com/cognitive-engineering-lab/depot?tab=readme-ov-file#installation). 10 | 11 | Bene is distributed as a web app and a desktop app. 12 | 13 | ### Web App 14 | 15 | You will need [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/). Then run: 16 | 17 | ``` 18 | cd bene/js 19 | depot -p bene-web build 20 | ``` 21 | 22 | Then you can serve the web app by running: 23 | 24 | ``` 25 | cd packages/bene-web/dist 26 | python -m http.server 27 | ``` 28 | 29 | And visit . You can replace the `python` command with however you like to serve static files. 30 | 31 | ### Desktop App 32 | 33 | You will need the [Tauri](https://tauri.app/) CLI, which you can install by running: 34 | 35 | ``` 36 | cargo install tauri-cli 37 | ``` 38 | 39 | Then run: 40 | 41 | ``` 42 | cd rs 43 | cargo tauri build 44 | ``` 45 | 46 | This will generate a binary you can use on your system. I have only tested this on MacOS and it only kind of works, so I would just use the web app for now. -------------------------------------------------------------------------------- /epubs/make-epub.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -f $1.epub 3 | set -e 4 | pushd $1 5 | echo -n "application/epub+zip" > mimetype 6 | zip -0 -X ../$1.epub mimetype 7 | rm -f mimetype 8 | zip -r ../$1.epub EPUB META-INF 9 | popd 10 | # epubcheck $1.epub -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/img/adobe-digital-edition.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nota-lang/bene/dc679cf216a6a48dec4699a06cb69102d28e3afa/epubs/portable-epubs/EPUB/img/adobe-digital-edition.jpg -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/img/after-resize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nota-lang/bene/dc679cf216a6a48dec4699a06cb69102d28e3afa/epubs/portable-epubs/EPUB/img/after-resize.jpg -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/img/apple-books.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nota-lang/bene/dc679cf216a6a48dec4699a06cb69102d28e3afa/epubs/portable-epubs/EPUB/img/apple-books.jpg -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/img/before-resize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nota-lang/bene/dc679cf216a6a48dec4699a06cb69102d28e3afa/epubs/portable-epubs/EPUB/img/before-resize.jpg -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/img/bene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nota-lang/bene/dc679cf216a6a48dec4699a06cb69102d28e3afa/epubs/portable-epubs/EPUB/img/bene.png -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/img/history-of-writing-kindle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nota-lang/bene/dc679cf216a6a48dec4699a06cb69102d28e3afa/epubs/portable-epubs/EPUB/img/history-of-writing-kindle.jpg -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/img/history-of-writing-pdf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nota-lang/bene/dc679cf216a6a48dec4699a06cb69102d28e3afa/epubs/portable-epubs/EPUB/img/history-of-writing-pdf.jpg -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/img/kindle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nota-lang/bene/dc679cf216a6a48dec4699a06cb69102d28e3afa/epubs/portable-epubs/EPUB/img/kindle.jpg -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/img/tags.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nota-lang/bene/dc679cf216a6a48dec4699a06cb69102d28e3afa/epubs/portable-epubs/EPUB/img/tags.jpg -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/index.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | Portable EPUBs 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Portable EPUBs

18 | 19 | 20 | Will Crichton 21 | Brown University 22 | 23 | 24 | January 25, 2024 25 | 26 | Despite decades of advances in document rendering technology, most of the world's documents are stuck in the 1990s due to the limitations of PDF. 27 | Yet, modern document formats like HTML have yet to provide a competitive alternative to PDF. This post explores what prevents HTML documents from being portable, and I propose a way forward based on the EPUB format. To demonstrate my ideas, this post is presented using a prototype EPUB reading system. 28 | 29 |
30 |
31 |

The Good and Bad of PDF

32 | 33 |

34 | PDF is the de facto file format for reading and sharing digital documents like papers, textbooks, and flyers. People use the PDF format for several reasons: 35 |

36 | 37 |
    38 |
  • PDFs are self-contained. A PDF is a single file that contains all the images, fonts, and other data needed to render it. It's easy to pass around a PDF. A PDF is unlikely to be missing some critical dependency on your computer.
  • 39 |
  • PDFs are rendered consistently. A PDF specifies precisely how it should be rendered, so a PDF author can be confident that a reader will see the same document under any conditions.
  • 40 |
  • PDFs are stable over time. PDFs from decades ago still render the same today. PDFs have a relatively stable standard. PDFs cannot be easily edited.
  • 41 |
42 | 43 |

Yet, in the 32 years since the initial release of PDF, a lot has changed. People print out documents less and less. People use phones, tablets, and e-readers to read digital documents. The internet happened; web browsers now provide a platform for rendering rich documents. These changes have laid bare the limitations of PDF:

44 | 45 |
    46 |
  • PDFs cannot easily adapt to different screen sizes. Most PDFs are designed to mimic 8.5x11" paper (or worse, 145,161 km2). These PDFs are readable on a computer monitor, but they are less readable on a tablet, and far less readable on a phone.
  • 47 |
  • PDFs cannot be easily understood by programs. A plain PDF is just a scattered sequence of lines and characters. For accessibility, screen readers may not know which order to read through the text. For data extraction, scraping tables out of a PDF is an open area of research.
  • 48 |
  • PDFs cannot easily express interaction. PDFs were primarily designed as static documents that cannot react to user input beyond filling in forms.
  • 49 |
50 | 51 |

52 | These pros and cons can be traced back to one key fact: the PDF representation of a document is fundamentally unstructured. A PDF consists of commands like: 53 |

54 | 55 |
56 |
Move the cursor to the right by 0.5 inches.
 57 | Set the current font color to black.
 58 | Draw the text "Hello World" at the current position.
59 |
60 | 61 |

PDF commands are unstructured because a document's organization is only clear to a person looking at the rendered document, and not clear from the commands themselves. Reflowing, accessibility, data extraction, and interaction all rely on programmatically understanding the structure of a document. Hence, these aspects are not easy to integrate with PDFs.

62 | 63 |

This raises the question: how can we design digital documents with the benefits of PDFs but without the limitations?

64 |
65 | 66 | 67 |
68 |

Can't We Just Fix PDF?

69 | 70 |

A simple answer is to improve the PDF format. After all, we already have billions of PDFs — why reinvent the wheel?

71 | 72 |

The designers of PDF are well aware of its limitations. I carefully hedged each bullet with easily, because PDF does make it possible to overcome each limitation, at least partially. PDFs can be annotated with their logical structure to create a tagged PDF. Most PDF exporters will not add tags automatically — the simplest option is to use Adobe's subscription-only Acrobat Pro, which provides an Automatically tag PDF action. For example, here is a recent paper of mine with added tags:

73 | 74 |
75 | A screenshot of the Adobe Acrobat interface. The left column is an academic paper. The right column is a tree of accessibility tags generated by Acrobat. 76 |
A LaTeX-generated paper with automatically added tags.
77 |
78 | 79 |

80 | If you squint, you can see that the logical structure closely resembles the HTML document model. The document has sections, headings, paragraphs, and links. Adobe characterizes the logical structure as an accessibility feature, but it has other benefits. You may be surprised to know that Adobe Acrobat allows you to reflow tagged PDFs at different screen sizes. You may be unsurprised to know that reflowing does not always work well. For example: 81 |

82 | 83 |
84 |
85 | A screenshot of the academic paper in a single-column layout in a PDF reader. It contains two paragraphs. The second paragraph is on the right wrapped around a code snippet on the left. 86 |
87 | A section of the paper in its default fixed layout. 88 | Note that the second paragraph is wrapped around the code snippet. 89 |
90 |
91 |
92 | A screenshot of the same academic paper after using the Acrobat reflow functionality. The first paragraph has successfully line-wrapped to a smaller size. In the second paragraph, each sentence is interleaved with the code snippet it was previously wrapping. 93 |
94 | The same section of the paper after reflowing to a smaller width. 95 | Note that the code is now interleaved with the second paragraph. 96 |
97 |
98 |
99 | 100 |

101 | In theory, these issues could be fixed. If the world's PDF exporters could be modified to include logical structure. If Adobe's reflowing algorithm could be improved to fix its edge cases. If the reflowing algorithm could be specified, and if Adobe were willing to release it publicly, and if it were implemented in each PDF viewer. And that doesn't even cover interaction! So in practice, I don't think we can just fix the PDF format, at least within a reasonable time frame. 102 |

103 |
104 | 105 |
106 |

The Good and Bad of HTML

107 | 108 |

In the meantime, we already have a structured document format which can be flexibly and interactively rendered: HTML (and CSS and Javascript, but here just collectively referred to as HTML). The HTML format provides almost exactly the inverse advantages and disadvantages of PDF.

109 | 110 |
    111 |
  • HTML can more easily adapt to different screen sizes. Over the last 20 years, web developers and browser vendors have created a wide array of techniques for responsive design.
  • 112 |
  • HTML can be more easily understood by a program. HTML provides both an inherent structure plus additional attributes to support accessibility tools.
  • 113 |
  • HTML can more easily express interaction. People have used HTML to produce amazing interactive documents that would be impossible in PDF. Think: Distill.pub, Explorable Explanations, Bartosz Ciechanowski, and Bret Victor, just to name a few.
  • 114 |
115 | 116 |

117 | Again, these advantages are hedged with more easily. One can easily produce a convoluted or inaccessible HTML document. But on balance, these aspects are more true than not compared to PDF. However, HTML is lacking where PDF shines: 118 |

119 | 120 |
    121 |
  • HTML is not self-contained. HTML files may contain URL references to external files that may be hosted on a server. One can rarely download an HTML file and have it render correctly without an internet connection.
  • 122 |
  • HTML is not always rendered consistently. HTML's dynamic layout means that an author may not see the same document as a reader. Moreover, HTML layout is not fully specified, so browsers may differ in their implementation.
  • 123 |
  • HTML is not fully stable over time. Browsers try to maintain backwards compatibility (come on and slam!), but the HTML format is still evolving. The HTML standard is a living standard due to the rapidly changing needs and feature sets of modern browsers.
  • 124 |
125 | 126 |

127 | So I've been thinking: how can we design HTML documents to gain the benefits of PDFs without losing the key strengths of HTML? The rest of this document will present some early prototypes and tentative proposals in this direction. 128 |

129 |
130 | 131 |
132 |

Self-Contained HTML with EPUB

133 | 134 |

135 | First, how can we make HTML documents self-contained? This is an old problem with many potential solutions. WARC, webarchive, and MHTML are all file formats designed to contain all the resources needed to render a web page. But these formats are more designed for snapshotting an existing website, rather than serving as a single source of truth for a web document. From my research, the most sensible format for this purpose is EPUB. 136 |

137 | 138 |

139 | EPUB is a distribution and interchange format for digital publications and documents, per the EPUB 3 Overview. Reductively, an EPUB is a ZIP archive of web files: HTML, CSS, JS, and assets like images and fonts. On a technical level, what distinguishes EPUB from archival formats is that EPUB includes well-specified files that describe metadata about a document. On a social level, EPUB appears to be the HTML publication format with the most adoption and momentum in 2024, compared to moribund formats like Mobi. 140 |

141 | 142 |

The EPUB spec has all the gory details, but to give you a rough sense, a sample EPUB might have the following file structure:

143 | 144 |
145 |
sample.epub
146 | ├── META-INF
147 | │   └── container.xml
148 | └── EPUB
149 |     ├── package.opf
150 |     ├── nav.xhtml
151 |     ├── chapter1.xhtml
152 |     ├── chapter2.xhtml
153 |     └── img
154 |         └── sample.jpg
155 |
156 | 157 |

An EPUB contains content documents (like chapter1.xhtml and chapter2.xhtml) which contain the core HTML content. Content documents can contain relative links to assets in the EPUB, like img/sample.jpg. The navigation document (nav.xhtml) provides a table of contents, and the package document (package.opf) provides metadata about the document. These files collectively define one rendition of the whole document, and the container file (container.xml) points to each rendition contained in the EPUB.

158 | 159 |

The EPUB format optimizes for machine-readable content and metadata. HTML content is required to be in XML format (hence, XHTML). Document metadata like the title and author is provided in structured form in the package document. The navigation document has a carefully prescribed tag structure so the TOC can be consistently extracted.

160 | 161 |

Overall, EPUB's structured format makes it a solid candidate for a single-file HTML document container. However, EPUB is not a silver bullet. EPUB is quite permissive in what kinds of content can be put into a content document.

162 | 163 |

For example, a major issue for self-containment is that EPUB content can embed external assets. A content document can legally include an image or font file whose src is a URL to a hosted server. This is not hypothetical, either; as of the time of writing, Google Doc's EPUB exporter will emit CSS that will @include external Google Fonts files. The problem is that such an EPUB will not render correctly without an internet connection, nor will it render correctly if Google changes the URLs of its font files.

164 | 165 |

Hence, I will propose a new format which I call a portable EPUB, which is an EPUB with additional requirements and recommendations to improve PDF-like portability. The first requirement is:

166 | 167 |
168 | Local asset requirement: All assets (like images, scripts, and fonts) embedded in a content document of a portable EPUB must refer to local files included in the EPUB. Hyperlinks to external files are permissible. 169 |
170 |
171 | 172 |
173 |

Consistency vs. Flexibility in Rendering

174 | 175 |

176 | There is a fundamental tension between consistency and flexibility in document rendering. A PDF is consistent because it is designed to render in one way: one layout, one choice of fonts, one choice of colors, one pagination, and so on. Consistency is desirable because an author can be confident that their document will look good for a reader (or at least, not look bad). Consistency has subtler benefits — because a PDF is chunked into a consistent set of pages, a passage can be cited by referring to the page containing the passage. 177 |

178 | 179 |

On the other hand, flexibility is desirable because people want to read documents under different conditions. Device conditions include screen size (from phone to monitor) and screen capabilities (E-ink vs. LCD). Some readers may prefer larger fonts or higher contrasts for visibility, alternative color schemes for color blindness, or alternative font faces for dyslexia. Sufficiently flexible documents can even permit readers to select a level of detail appropriate for their background (here's an example).

180 | 181 |

182 | Finding a balance between consistency and flexibility is arguably the most fundamental design challenge in attempting to replace PDF with EPUB. To navigate this trade-off, we first need to talk about EPUB reading systems, or the tools that render an EPUB for human consumption. To get a sense of variation between reading systems, I tried rendering this post as an EPUB (without any styling, just HTML) on four systems: Calibre, Adobe Digital Editions, Apple Books, and Amazon Kindle. This is how the first page looks on each system (omitting Calibre because it looked the same as Adobe Digital Editions): 183 |

184 | 185 |
186 |
187 | A screenshot of this post in the Adobe Digital Editions interface. It has Times New Roman font and a classic web 1.0 kind of look. 188 |
Adobe Digital Editions
189 |
190 | 191 |
192 | A screenshot of this post in the Apple Books interface. It has more tasteful spacing and fonts. 193 |
Apple Books
194 |
195 | 196 |
197 | A photograph of a Kindle displaying this post. It has much more spacing and a smaller screen. 198 |
Amazon Kindle
199 |
200 |
201 | 202 |

203 | Calibre and Adobe Digital Editions both render the document in a plain web view, as if you opened the HTML file directly in the browser. Apple Books applies some styling, using the New York font by default and changing link decorations. Amazon Kindle increases the line height and also uses my Kindle's globally-configured default font, Bookerly. 204 |

205 | 206 |

207 | As you can see, an EPUB may look quite different on different reading systems. The variation displayed above seems reasonable to me. But how different is too different? For instance, I was recently reading A History of Writing on my Kindle. Here's an example of how a figure in the book renders on the Kindle: 208 |

209 | 210 |
211 | A photograph of a Kindle displaying an excerpt from A History of Writing. A figure in the document is awkwardly positioned with an image that is too small. 212 |
A figure in the EPUB version of A History of Writing on my Kindle
213 |
214 | 215 |

When I read this page, I thought, wow, this looks like crap. The figure is way too small (although you can long-press the image and zoom), and the position of the figure seems nonsensical. I found a PDF version online, and indeed the PDF's figure has a proper size in the right location:

216 | 217 |
218 | A screenshot of the same passage in a PDF viewer. The figure size and placement is more appropriate. 219 |
A figure in the PDF version of A History of Writing on my Mac
220 |
221 | 222 |

This is not a fully fair comparison, but it nonetheless exemplifies an author's reasonable concern today with EPUB: what if it makes my document looks like crap?

223 |
224 | 225 |
226 |

Principles for Consistent EPUB Rendering

227 | 228 |

229 | I think the core solution for consistently rendering EPUBs comes down to this: 230 |

231 | 232 |
    233 |
  1. The document format (i.e., portable EPUB) needs to establish a subset of HTML (call it portable HTML) which could represent most, but not all, documents.
  2. 234 |
  3. Reading systems need to guarantee that a document within the subset will always look reasonable under all reading conditions.
  4. 235 |
  5. If a document uses features outside this subset, then the document author is responsible for ensuring the readability of the document.
  6. 236 |
237 | 238 |

239 | If someone wants to write a document such as this post, then that person need not be a frontend web developer to feel confident that their document will render reasonably. Conversely, if someone wants to stuff the entire Facebook interface into an EPUB, then fine, but it's on them to ensure the document is responsive. 240 |

241 | 242 |

For instance, one simple version of portable HTML could be described by this grammar:

243 | 244 |
245 |
Document ::= <article> Block* </article>
246 | Block    ::= <p> Inline* </p> | <figure> Block* </figure>
247 | Inline   ::= text | <strong> Inline* </strong>
248 |
249 | 250 |

251 | The EPUB spec already defines a comparable subset for navigation documents. 252 | I am essentially proposing to extend this idea for content documents, but as a soft constraint rather than a hard constraint. Finding the right subset of HTML will take some experimentation, so I can only gesture toward the broad solution here. 253 |

254 | 255 | 256 |
257 | Portable HTML rendering requirement: if a document only uses features in the portable HTML subset, then a portable EPUB reading system must guarantee that the document will render reasonably. 258 |
259 | 260 |
261 | Portable HTML generation principle: when possible, systems that generate portable EPUBs should output portable HTML. 262 |
263 | 264 |

A related challenge is to define when a particular rendering is good or reasonable, so one could evaluate either a document or a reading system on its conformance to spec. For instance, if document content is accidentally rendered in an inaccesible location off-screen, then that would be a bad rendering. A more aggressive definition might say that any rendering which violates accessibility guidelines is a bad rendering. Again, finding the right standard for rendering quality will take some experimentation.

265 | 266 |

If an author is particularly concerned about providing a single canonical rendering of their document, one fallback option is to provide a fixed-layout rendition. The EPUB format permits a rendition to specify that it should be rendered in fixed viewport size and optionally a fixed pagination. A fixed-layout rendition could then manually position all content on the page, similar to a PDF. Of course, this loses the flexibility of a reflowable rendition. But an EPUB could in theory provide multiple renditions, offering users the choice of whichever best suits their reading conditions and aesthetic preferences.

267 | 268 |
269 | Fixed-layout fallback principle: systems that generate portable EPUBs can consider providing both a reflowable and fixed-layout rendition of a document. 270 |
271 | 272 |

It's possible that the reading system, the document author, and the reader can each express preferences about how a document should render. If these preferences are conflicting, then the renderer should generally prioritize the reader over the author, and the author over the reading system. This is an ideal use case for the "cascading" aspect of CSS:

273 | 274 |
275 | Cascading styles principle: both documents and reading systems should express stylistic preferences (such as font face, font size, and document width) as CSS styles which can be overriden (e.g., do not use !important). The reading system should load the CSS rules such that the priority order is reading system styles < document styles < reader styles. 276 |
277 |
278 | 279 |
280 |

A Lighter EPUB Reading System

281 |

282 | The act of working with PDFs is relatively fluid. I can download a PDF, quickly open it in a PDF reading system like Preview, and keep or discard the PDF as needed. But EPUB reading systems feel comparatively clunky. Loading an EPUB into Apple Books or Calibre will import the EPUB into the application's library, which both copies and potentially decompresses the file. Loading an EPUB on a Kindle requires waiting several minutes for the Send to Kindle service to complete. 283 |

284 | 285 |

Worse, EPUB reading systems often don't give you appropriate control over rendering an EPUB. For example, to emulate the experience of reading a book, most reading systems will chunk an EPUB into pages. A reader cannot scroll the document but rather turn the page, meaning textually-adjacent content can be split up between pages. Whether a document is paginated or scrolled should be a reader's choice, but 3/4 reading systems I tested would only permit pagination (Calibre being the exception). 286 |

287 | 288 |

Therefore I decided to build a lighter EPUB reading system, Bene. You're using it right now. This document is an EPUB — you can download it by clicking the button in the top-right corner. The styling and icons are mostly borrowed from pdf.js. Bene is implemented in Tauri, so it can work as both a desktop app and a browser app. Please appreciate this picture of Bene running as a desktop app:

289 | 290 |
291 | A screenshot of a MacOS window containing the same interface as this page. It displays the same post. 292 |
The Bene reading system running as a desktop app. Wow! It works!
293 |
294 | 295 |

Bene is designed to make opening and reading an EPUB feel fast and non-committal. The app is much quicker to open on my Macbook (<1sec) than other desktop apps. It decompresses files on-the-fly so no additional disk space is used. The backend is implemented in Rust and compiled to Wasm for the browser version.

296 | 297 | 298 |

The general design goal of Bene is to embody my ideals for a portable EPUB reader. That is, a utilitarian interface into an EPUB that satisfies my additional requirements for portability. Bene allows you to configure document rendering by changing the font size (try the +/- buttons in the top bar) and the viewer width (if you're on desktop, move your mouse over the right edge of the document, and drag the handle). Long-term, I want Bene to also provide richer document interactions than a standard EPUB reader, which means we must discuss scripting.

299 |
300 | 301 |
302 |

Defensively Scripting EPUBs

303 | 304 |

305 | To some people, the idea of code in their documents is unappealing. Last time one of my document-related projects was posted to Hacker News, the top comment was complaining about dynamic documents. The sentiment is understandable — concerns include: 306 |

307 | 308 |
    309 |
  • Bad code: your document shouldn't crash or glitch due to a failure in a script.
  • 310 |
  • Bad browsers: your document shouldn't fail to render when a browser updates.
  • 311 |
  • Bad actors: a malicious document shouldn't be able to pwn your computer.
  • 312 |
  • Bad interfaces: a script shouldn't cause your document to become unreadable.
  • 313 |
314 | 315 |

Yet, document scripting provides many opportunities for improving how we communicate information. For one example, if you haven't yet, try hovering your mouse over any instance of the term portable EPUB (or long press it on a touch screen). You should see a tooltip appear with the term's definition. The goal of these tooltips is to simplify reading a document that contains a lot of specialized notation or terminology. If you forget a definition, you can quickly look it up without having to jump around.

316 | 317 |

The key design challenge is how to permit useful scripting behaviors while limiting the downsides of scripting. One strategy is as follows:

318 | 319 |
320 | Structure over scripts principle: documents should prefer structural annotations over scripts where possible. Documents should rely on reading systems to utilize structure where possible. 321 |
322 | 323 |

As an example of this principle, consider how the portable EPUB definition and references are expressed in this document:

324 | 325 |
326 |
327 |
<p><dfn-container>Hence, I will propose a new format which I call a <dfn id="portable-epub">portable EPUB</dfn>, which is an EPUB with additional requirements and recommendations to improve PDF-like portability.</dfn-container> The first requirement is:</p>
328 |
Creating a definition
329 |
330 | 331 |
332 |
For one example, if you haven't yet, try hovering your mouse over any instance of the term <a href="#portable-epub" data-target="dfn">portable EPUB</a> (or long press it on a touch screen).
333 |
Referencing a definition
334 |
335 |
336 | 337 |

The definition uses the <dfn> element wrapped in a custom <dfn-container> element to indicate the scope of the definition. The reference to the definition uses a standard anchor with an addition data-target attribute to emphasize that a definition is being linked. The document itself does not provide a script. The Bene reading system automatically detects these annotations and provides the tooltip interaction.

338 |
339 | 340 |
341 |

Encapsulating Scripts with Web Components

342 | 343 |

But what if a document wants to provide an interactive component that isn't natively supported by the reading system? For instance, I have recently been working with The Rust Programming Language, a textbook that explains the different features of Rust. It contains a lot of passages like this one:

344 | 345 |
346 | 347 |
fn main() {
348 |     let x = 5;
349 |     let x = x + 1;
350 |     {
351 |         let x = x * 2;
352 |         println!("The value of x in the inner scope is: {x}");
353 |     }
354 |     println!("The value of x is: {x}");
355 | }
356 |

This program first binds x to a value of 5. Then it creates a new variable 357 | x by repeating let x =, taking the original value and adding 1 so the 358 | value of x is then 6. Then, within an inner scope created with the curly 359 | brackets, the third let statement also shadows x and creates a new 360 | variable, multiplying the previous value by 2 to give x a value of 12. 361 | When that scope is over, the inner shadowing ends and x returns to being 6. 362 | When we run this program, it will output the following:

363 |
364 | 365 |

A challenge in reading this passage is finding the correspondences between the prose and the code. An interactive code reading component can help you track those correspondences, like this (try mousing-over or clicking-on each sentence):

366 | 367 |
368 | 369 |
fn main() {
370 |     let x = 5;
371 |     let x = x + 1;
372 |     {
373 |         let x = x * 2;
374 |         println!("The value of x in the inner scope is: {x}");
375 |     }
376 |     println!("The value of x is: {x}");
377 | }
378 |

379 | This program first binds x to a value of 5. 380 | Then it creates a new variable x by repeating let x =, 381 | taking the original value and adding 1 382 | so the value of x is then 6. 383 | Then, within an inner scope created with the curly brackets, 384 | the third let statement also shadows x and creates 385 | a new variable, 386 | multiplying the previous value by 2 387 | to give x a value of 12. 388 | When that scope is over, the inner shadowing ends and x returns to being 6. 389 |

390 |
391 |
392 | 393 |

The interactive code description component is used as follows:

394 | 395 |
396 | 397 |
<code-description>
398 |   <pre><code>fn main() {
399 |     let <span id="code-1">x</span> = <span id="code-2">5</span>;
400 |     <!-- rest of the code... -->
401 | }</code></pre>
402 |   <p>
403 |     <code-step>This program first binds <a href="#code-1"><code>x</code></a> to a value of <a href="#code-2"><code>5</code></a>.</code-step>
404 |     <!-- rest of the prose... -->
405 |   </p>
406 | </code-description>
407 |
408 | 409 |

Again, the document content contains no actual script. It contains a custom element <code-description>, and it contains a series of annotations as spans and anchors. The <code-description> element is implemented as a web component.

410 | 411 |

Web components are a programming model for writing encapsulated interactive fragments of HTML, CSS, and Javascript. Web components are one of many ways to write componentized HTML, such as React, Solid, Svelte, and Angular. I see web components as the most suitable as a framework for portable EPUBs because: 412 |

413 | 414 |
    415 |
  • Web components are a standardized technology. Its key features like custom elements (for specifying the behavior of novel elements) and shadow trees (for encapsulating a custom element from the rest of the document) are part of the official HTML and DOM specifications. This improves the likelihood that future browsers will maintain backwards compatibility with web components written today.
  • 416 |
  • Web components are designed for tight encapusulation. The shadow tree mechanism ensures that styling applied within a custom component cannot accidentally affect other components on the page.
  • 417 |
  • Web components have a decent ecosystem to leverage. As far as I can tell, web components are primarily used by Google, which has created notable frameworks like Lit.
  • 418 |
  • Web components provide a clear fallback mechanism. If a renderer does not support Javascript, or if a renderer loses the ability to render web components, then an HTML renderer will simply ignore custom tags and render their contents.
  • 419 |
420 | 421 |

422 | Thus, I propose one principle and one requirement: 423 |

424 | 425 |
426 | Encapsulated scripts principle: interactive components should be implemented as web components when possible, or otherwise be carefully designed to avoid conflicting with the base document or other components. 427 |
428 | 429 |
430 | Components fallback requirement: interactive components must provide a fallback mechanism for rendering a reasonable substitute if Javascript is disabled. 431 |
432 |
433 |
434 |

Where To Go From Here?

435 | 436 |

437 | Every time I have told someone I want to replace PDF, the statement has been met with extreme skepticism. Hopefully this document has convinced you that HTML-via-EPUB could potentially be a viable and desirable document format for the future. 438 |

439 | 440 |

My short-term goal is to implement a few more documents in the portable EPUB format, such as my PLDI paper. That will challenge both the file format and the reading system to be flexible enough to support each document type. In particular, each document should look good under a range of reading conditions (screen sizes, font sizes and faces, etc.).

441 | 442 |

My long-term goal is to design a document language that makes it easy to generate portable EPUBs. Writing XHTML by hand is not reasonable. I designed Nota before I was thinking about EPUBs, so its next iteration will be targeted at this new format.

443 | 444 |

445 | If you have any thoughts about how to make this work or why I'm wrong, let me know by email or Twitter or Mastodon or wherever this gets posted. If you would like to help out, please reach out! This is just a passion project in my free time (for now...), so any programming or document authoring assistance could provide a lot of momentum to the project. 446 |

447 |
448 |
449 |

But What About...

450 | 451 |

A brief postscript for a few things I haven't touched on.

452 | 453 |

454 | ...security? You might dislike the idea that document authors can run arbitrary Javascript on your personal computer. But then again, you presumably use both a PDF reader and a web browser on the daily, and those both run Javascript. What I'm proposing is not really any less secure than our current state of affairs. If anything, I'd hope that browsers are more battle-hardened than PDF viewers regarding code execution. Certainly the designers of EPUB reading systems should be careful to not give documents any additional capabilities beyond those already provided by the browser. 455 |

456 | 457 |

...privacy? Modern web sites use many kinds of telemetry and cookies to track user behavior. I strongly believe that EPUBs should not follow this trend. Telemetry must at least require the explicit consent of the user, and even that may be too generous. Companies will inevitably do things like offer discounts in exchange for requiring your consent to telemetry, similar to Amazon's Kindle ads policy. Perhaps it is better to preempt this behavior by banning all tracking.

458 | 459 |

...aesthetics? People often intuit that LaTeX-generated PDFs look prettier than HTML documents, or even prettier than PDFs created by other software. This is because Donald Knuth took his job very seriously. In particular, the Knuth-Plass line-breaking algorithm tends to produce better-looking justified text than whatever algorithm is used by browsers.

460 |

There's two ways to make progress here. One is for browsers to provide more typography tools. Allegedly, text-wrap: pretty is supposed to help, but in my brief testing it doesn't seem to improve line-break quality. The other way is to pre-calculate line breaks, which would only work for fixed-layout renditions.

461 | 462 |

...page citations? I think we just have to give up on citing content by pages. Instead, we should mandate a consistent numbering scheme for block elements within a document, and have people cite using that scheme. (Allison Morrell points out this is already the standard in the Canadian legal system.) For example, Bene will auto-number all blocks. If you're on a desktop, try hovering your mouse in the left column next to the top-right of any paragraph.

463 | 464 |

...annotations? Ideally it should be as easy to mark up an EPUB as a PDF. The Web Annotations specification seems to be a good starting point for annotating EPUBs. Web Annotations seem designed for annotations on "targetable" objects, like a labeled element or a range of text. It's not yet clear how to deal with free-hand annotations, especially on reflowable documents.

465 |
466 |
467 | 468 | -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/nav.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Navigation 7 | 8 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /epubs/portable-epubs/EPUB/package.opf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | code.google.com.epub-samples.wasteland-basic 5 | Portable EPUBs 6 | Will Crichton 7 | en-US 8 | 2024-01-24 9 | 2023-01-24T00:00:00Z 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /epubs/portable-epubs/META-INF/container.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /js/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es2021": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended" 7 | ], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": 13, 11 | "sourceType": "module" 12 | }, 13 | "plugins": [ 14 | "@typescript-eslint", 15 | "prettier" 16 | ], 17 | "ignorePatterns": [ 18 | "*.d.ts" 19 | ], 20 | "rules": { 21 | "no-empty-pattern": "off", 22 | "no-undef": "off", 23 | "no-unused-vars": "off", 24 | "no-cond-assign": "off", 25 | "@typescript-eslint/no-unused-vars": [ 26 | "error", 27 | { 28 | "argsIgnorePattern": "^_", 29 | "varsIgnorePattern": "^_" 30 | } 31 | ], 32 | "no-constant-condition": [ 33 | "error", 34 | { 35 | "checkLoops": false 36 | } 37 | ], 38 | "prettier/prettier": "error" 39 | } 40 | } -------------------------------------------------------------------------------- /js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | docs 4 | 5 | packages/bene-desktop/src/bindings.ts -------------------------------------------------------------------------------- /js/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | arrowParens: "avoid", 4 | importOrder: ["", "^[./]"], 5 | importOrderSeparation: true, 6 | importOrderSortSpecifiers: true, 7 | importOrderParserPlugins: ["typescript", "decorators-legacy", "jsx"], 8 | parser: "typescript", 9 | plugins: [require("@trivago/prettier-plugin-sort-imports")], 10 | }; 11 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 5 | "@types/node": "^20.10.5", 6 | "@typescript-eslint/eslint-plugin": "^6.14.0", 7 | "@typescript-eslint/parser": "^6.14.0", 8 | "eslint": "^8.56.0", 9 | "eslint-plugin-prettier": "^4.2.1", 10 | "fast-glob": "^3.3.2", 11 | "fs-extra": "^11.2.0", 12 | "prettier": "^2.8.8", 13 | "sass": "^1.69.7", 14 | "typedoc": "^0.25.4", 15 | "typescript": "^5.3.3", 16 | "vite": "^5.0.10", 17 | "vite-plugin-solid": "^2.8.0", 18 | "vitest": "^1.0.4" 19 | }, 20 | "pnpm": { 21 | "overrides": { 22 | "rollup": "npm:@rollup/wasm-node" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /js/packages/bene-components/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "../../.eslintrc.cjs", 3 | "env": { 4 | "browser": true 5 | } 6 | } -------------------------------------------------------------------------------- /js/packages/bene-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bene-components", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "main": "dist/bene-components.iife.js", 6 | "depot": { 7 | "platform": "browser" 8 | }, 9 | "devDependencies": { 10 | "@codemirror/lang-html": "^6.4.7", 11 | "@codemirror/lang-rust": "^6.0.1", 12 | "@codemirror/state": "^6.4.0", 13 | "@codemirror/view": "^6.23.0", 14 | "@types/lodash": "^4.14.202", 15 | "codemirror": "^6.0.1", 16 | "jsdom": "^23.0.1", 17 | "lit": "^3.1.0", 18 | "lodash": "^4.17.21", 19 | "tippy.js": "^6.3.7" 20 | } 21 | } -------------------------------------------------------------------------------- /js/packages/bene-components/src/block-links.scss: -------------------------------------------------------------------------------- 1 | @mixin mobile { 2 | @media (max-width: 600px) { 3 | @content; 4 | } 5 | } 6 | 7 | a.block-link { 8 | position: relative; 9 | display: block; 10 | height: 0; 11 | 12 | > a { 13 | display: block; 14 | position: absolute; 15 | left: -37px; 16 | width: 30px; 17 | height: 30px; 18 | opacity: 0; 19 | cursor: pointer; 20 | transition: opacity 0.1s; 21 | color: #aaa; 22 | text-align: center; 23 | 24 | @include mobile { 25 | display: none; 26 | } 27 | 28 | &:hover { 29 | opacity: 1; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /js/packages/bene-components/src/block-links.ts: -------------------------------------------------------------------------------- 1 | import "./block-links.scss"; 2 | 3 | function initBlockLinks() { 4 | let sections = document.querySelectorAll("section"); 5 | sections.forEach((section, i) => { 6 | let updates = Array.from(section.childNodes) 7 | .filter(node => node instanceof HTMLElement) 8 | .map<[HTMLAnchorElement, HTMLElement]>((node, j) => { 9 | let el = node as HTMLElement; 10 | let id = el.id !== "" ? el.id : `block-${i + 1}-${j + 1}`; 11 | let outerAnchor = document.createElement("a"); 12 | let innerAnchor = document.createElement("a"); 13 | innerAnchor.innerText = "§"; 14 | innerAnchor.setAttribute("href", `#${id}`); 15 | 16 | outerAnchor.appendChild(innerAnchor); 17 | outerAnchor.classList.add("block-link"); 18 | outerAnchor.setAttribute("id", id); 19 | return [outerAnchor, el]; 20 | }); 21 | updates.forEach(([anchor, node]) => section.insertBefore(anchor, node)); 22 | }); 23 | } 24 | 25 | initBlockLinks(); 26 | -------------------------------------------------------------------------------- /js/packages/bene-components/src/code-description.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | 3 | // print(', '.join(['rgb(' + ','.join([str(int(ch * 255)) for ch in c]) + ')' for c in sns.color_palette('tab10')])) 4 | $colors: (rgb(31,119,180), rgb(255,127,14), rgb(44,160,44), rgb(214,39,40), rgb(148,103,189), rgb(140,86,75), rgb(227,119,194), rgb(127,127,127), rgb(188,189,34), rgb(23,190,207)); 5 | 6 | @each $color in $colors { 7 | $index: index($colors, $color) - 1; 8 | 9 | .code-highlight.highlight-#{$index} { 10 | border-bottom: 2px solid $color; 11 | 12 | &::before { 13 | content: "#{$index + 1}"; 14 | } 15 | 16 | &.emphasize, &:hover { 17 | background-color: rgba($color, 0.1); 18 | } 19 | } 20 | 21 | .code-delimiter.start.highlight-#{$index}::after { 22 | content: "#{$index + 1}"; 23 | } 24 | 25 | .code-delimiter.highlight-#{$index}::before { 26 | color: color.scale($color, $lightness: -30%); 27 | } 28 | } 29 | 30 | .code-delimiter, .code-highlight { 31 | position: relative; 32 | transition: background-color 0.2s; 33 | 34 | &::before, &::after { 35 | position: absolute; 36 | } 37 | } 38 | 39 | .code-highlight::before, .code-delimiter::after { 40 | font-size: 10px; 41 | font-family: monospace; 42 | color: #555; 43 | } 44 | 45 | .cm-editor .code-highlight::before { 46 | top: -1.2em; 47 | right: 0; 48 | } 49 | 50 | .cm-editor .code-delimiter::after { 51 | top: -1.2em; 52 | left: -1ch; 53 | } 54 | 55 | .description .code-highlight::before { 56 | top: -2em; 57 | right: 0; 58 | } 59 | 60 | .code-delimiter.start::before { 61 | content: "⟨"; 62 | left: -1ch; 63 | } 64 | 65 | .code-delimiter.end::before { 66 | content: "⟩"; 67 | right: -1ch; 68 | } 69 | 70 | .description { 71 | line-height: 2em; 72 | 73 | > :last-child { 74 | margin-bottom: 0; 75 | } 76 | 77 | a { 78 | text-decoration: none; 79 | color: inherit; 80 | white-space: nowrap; 81 | } 82 | 83 | code-step { 84 | padding: calc(0.5 * (1lh - 1em)) 0; 85 | 86 | &::before, &::after { 87 | color: #ccc; 88 | } 89 | 90 | &::before { 91 | content: "["; 92 | } 93 | 94 | &::after { 95 | content: "]"; 96 | } 97 | 98 | &:hover::before, &:hover::after { 99 | color: #e45757; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /js/packages/bene-components/src/code-description.ts: -------------------------------------------------------------------------------- 1 | import { rust } from "@codemirror/lang-rust"; 2 | import { 3 | EditorState, 4 | Range, 5 | RangeSet, 6 | StateEffect, 7 | StateField, 8 | } from "@codemirror/state"; 9 | import { Decoration, DecorationSet } from "@codemirror/view"; 10 | import { EditorView, minimalSetup } from "codemirror"; 11 | import { LitElement, html, unsafeCSS } from "lit"; 12 | import { customElement, state } from "lit/decorators.js"; 13 | import { Ref, createRef, ref } from "lit/directives/ref.js"; 14 | import _ from "lodash"; 15 | 16 | import cssContent from "./code-description.scss?inline"; 17 | 18 | const highlightDecoration = (index: number, id: string) => 19 | Decoration.mark({ 20 | class: `code-highlight highlight-${index}`, 21 | attributes: { id: `deco-${id}` }, 22 | }); 23 | const delimiterStartDecoration = (index: number, id: string) => 24 | Decoration.mark({ 25 | class: `code-delimiter start highlight-${index}`, 26 | attributes: { id: `deco-${id}` }, 27 | }); 28 | const delimiterEndDecoration = (index: number, id: string) => 29 | Decoration.mark({ 30 | class: `code-delimiter end highlight-${index}`, 31 | attributes: { id: `deco-${id}` }, 32 | }); 33 | 34 | const setDecorations = StateEffect.define<{ 35 | decorations: Range[]; 36 | }>(); 37 | const highlightField = StateField.define({ 38 | create() { 39 | return Decoration.none; 40 | }, 41 | update(decorations, tr) { 42 | decorations = decorations.map(tr.changes); 43 | for (let e of tr.effects) 44 | if (e.is(setDecorations)) { 45 | decorations = RangeSet.of( 46 | _.sortBy(e.value.decorations, deco => deco.from) 47 | ); 48 | } 49 | return decorations; 50 | }, 51 | provide: f => EditorView.decorations.from(f), 52 | }); 53 | 54 | interface Span { 55 | id: string; 56 | start: number; 57 | end: number; 58 | multiline: boolean; 59 | } 60 | type Spans = { 61 | [id: string]: Span; 62 | }; 63 | 64 | @customElement("code-description") 65 | export class CodeDescription extends LitElement { 66 | descRef: Ref = createRef(); 67 | editor: EditorView | undefined; 68 | code: string; 69 | spans: Spans; 70 | steps: HTMLElement[]; 71 | 72 | @state() 73 | protected step: number = 0; 74 | 75 | static styles = unsafeCSS(cssContent); 76 | 77 | parseCode(codeEl: HTMLElement, newlines: number[]): Spans { 78 | let spans: Spans = {}; 79 | let index = 0; 80 | function scan(el: HTMLElement) { 81 | el.childNodes.forEach(node => { 82 | if (node.nodeType == Node.TEXT_NODE) { 83 | index += node.textContent!.length; 84 | } else if (node.nodeType == Node.ELEMENT_NODE) { 85 | let el = node as HTMLElement; 86 | let start = index; 87 | scan(el); 88 | let end = index; 89 | 90 | let id = el.getAttribute("id"); 91 | let multiline = newlines.some(idx => start <= idx && idx < end); 92 | if (id) spans[id] = { id, start, end, multiline }; 93 | } else { 94 | console.warn("Unexpected node type", node.nodeType); 95 | } 96 | }); 97 | } 98 | scan(codeEl); 99 | return spans; 100 | } 101 | 102 | parseDescription(descEl: HTMLElement): HTMLElement[] { 103 | return Array.from(descEl.querySelectorAll("span[*|type=code-step]")); 104 | } 105 | 106 | constructor() { 107 | super(); 108 | 109 | let preEl = this.children.item(0); 110 | if (!preEl || !(preEl instanceof HTMLElement)) 111 | throw new Error("Missing pre element"); 112 | let codeEl = preEl.querySelector("code"); 113 | if (!codeEl) throw new Error("Missing code element"); 114 | 115 | this.code = preEl.innerText; 116 | let newlines = Array.from(this.code.matchAll(/\n/g)).map( 117 | match => match.index! 118 | ); 119 | 120 | this.spans = this.parseCode(codeEl, newlines); 121 | 122 | let descEl = this.children.item(1); 123 | if (!descEl || !(descEl instanceof HTMLElement)) 124 | throw new Error("Missing description element"); 125 | this.steps = this.parseDescription(descEl); 126 | } 127 | 128 | firstUpdated() { 129 | let steps = this.renderRoot.querySelectorAll("code-step"); 130 | steps.forEach(step => { 131 | let anchors = Array.from(step.querySelectorAll("a")); 132 | 133 | let index = 0; 134 | let groupedAnchors: { 135 | [id: string]: { index: number; anchors: HTMLAnchorElement[] }; 136 | } = {}; 137 | for (let a of anchors) { 138 | let id = _.last(a.href.split("#"))!; 139 | if (!(id in groupedAnchors)) { 140 | groupedAnchors[id] = { index, anchors: [] }; 141 | index += 1; 142 | } 143 | groupedAnchors[id].anchors.push(a); 144 | } 145 | 146 | let sortedIds = _.sortBy( 147 | Object.entries(groupedAnchors), 148 | ([_id, { index }]) => index 149 | ).map(([id]) => id); 150 | 151 | let spans = sortedIds.map(id => this.spans[id]); 152 | let decorations = spans.flatMap((span, i) => { 153 | let contains = spans.some( 154 | other => 155 | span.id != other.id && 156 | span.start <= other.start && 157 | other.end <= span.end 158 | ); 159 | if (span.multiline || contains) { 160 | return [ 161 | delimiterStartDecoration(i, span.id).range( 162 | span.start, 163 | span.start + 1 164 | ), 165 | delimiterEndDecoration(i, span.id).range(span.end - 1, span.end), 166 | ]; 167 | } else { 168 | return [highlightDecoration(i, span.id).range(span.start, span.end)]; 169 | } 170 | }); 171 | 172 | sortedIds.forEach(id => { 173 | for (let a of groupedAnchors[id].anchors) { 174 | a.addEventListener("mouseenter", () => { 175 | let deco = this.editor!.dom.querySelector(`#deco-${id}`); 176 | if (!deco) return; 177 | deco.classList.add("emphasize"); 178 | }); 179 | a.addEventListener("mouseleave", () => { 180 | let deco = this.editor!.dom.querySelector(`#deco-${id}`); 181 | if (!deco) return; 182 | deco.classList.remove("emphasize"); 183 | }); 184 | } 185 | }); 186 | 187 | step.addEventListener("mouseenter", () => { 188 | sortedIds.forEach((id, i) => { 189 | for (let a of groupedAnchors[id].anchors) { 190 | a.classList.add("code-highlight", `highlight-${i}`); 191 | } 192 | }); 193 | 194 | this.editor!.dispatch({ effects: setDecorations.of({ decorations }) }); 195 | }); 196 | step.addEventListener("mouseleave", () => { 197 | sortedIds.forEach((id, i) => { 198 | for (let a of groupedAnchors[id].anchors) { 199 | a.classList.remove("code-highlight", `highlight-${i}`); 200 | } 201 | }); 202 | 203 | this.editor!.dispatch({ 204 | effects: setDecorations.of({ decorations: [] }), 205 | }); 206 | }); 207 | }); 208 | } 209 | 210 | connectedCallback() { 211 | super.connectedCallback(); 212 | 213 | let doc = this.code; 214 | 215 | let root = this.shadowRoot; 216 | if (!root) throw new Error("Shadow root is null"); 217 | 218 | let extensions = [ 219 | minimalSetup, 220 | rust(), 221 | EditorState.readOnly.of(true), 222 | highlightField, 223 | EditorView.baseTheme({ 224 | ".cm-scroller": { lineHeight: "1.7" }, 225 | ".cm-content": { fontSize: "90%" }, 226 | }), 227 | ]; 228 | 229 | this.editor = new EditorView({ doc, root, extensions }); 230 | } 231 | 232 | render() { 233 | let descEl = this.children.item(1); 234 | return html` 235 |
${this.editor!.dom}
236 |
${descEl}
237 | `; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /js/packages/bene-components/src/dfn-links.scss: -------------------------------------------------------------------------------- 1 | @import 'tippy.js/themes/light-border.css'; 2 | @import 'tippy.js/dist/svg-arrow.css'; 3 | 4 | a[data-target="dfn"] { 5 | text-decoration: none; 6 | color: inherit; 7 | border-bottom: 1px dotted #555; 8 | 9 | -webkit-touch-callout: none; // prevents link preview from appearing for definition links 10 | 11 | &:hover { 12 | text-decoration: none; 13 | } 14 | } 15 | 16 | .tippy-box { 17 | border-radius: 4px; 18 | 19 | .tippy-content { 20 | padding: 0.5rem 1rem; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /js/packages/bene-components/src/dfn-links.ts: -------------------------------------------------------------------------------- 1 | import tippy, { roundArrow } from "tippy.js"; 2 | 3 | import "./dfn-links.scss"; 4 | 5 | const TIPPY_THEME = "light-border"; 6 | 7 | function initDefinitionLinks() { 8 | let definitionEls = document.querySelectorAll("dfn"); 9 | let definitions: { [id: string]: string } = {}; 10 | definitionEls.forEach(el => { 11 | let parent = el.closest("dfn-container, p"); 12 | if (parent === null) { 13 | console.warn("Missing parent for definition", el); 14 | return; 15 | } 16 | definitions[el.id] = parent.innerText; 17 | }); 18 | 19 | let links = document.querySelectorAll( 20 | 'a[data-target="dfn"]' 21 | ); 22 | links.forEach(link => { 23 | let id = link.href.split("#")[1]; 24 | let content = definitions[id]; 25 | if (content === undefined) { 26 | console.warn("Missing definition for reference", id); 27 | return; 28 | } 29 | tippy(link, { 30 | content, 31 | arrow: roundArrow, 32 | theme: TIPPY_THEME, 33 | placement: "auto", 34 | interactive: true, 35 | delay: [200, 0], 36 | touch: ["hold", 500], 37 | }); 38 | }); 39 | } 40 | 41 | initDefinitionLinks(); 42 | -------------------------------------------------------------------------------- /js/packages/bene-components/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./block-links"; 2 | import "./code-description"; 3 | import "./dfn-links"; 4 | import "./resize-handle"; 5 | import "./syntax-highlight"; 6 | 7 | function retryHash() { 8 | // setTimeout to ensure this doesn't run until the next (tick? paint?), 9 | // so all modifications made by imported scripts are in the document 10 | setTimeout(() => { 11 | var requestedHash = location.hash.slice(1); 12 | location.hash = ""; 13 | location.hash = requestedHash; 14 | }); 15 | } 16 | retryHash(); 17 | 18 | console.debug("Loaded component script."); 19 | -------------------------------------------------------------------------------- /js/packages/bene-components/src/resize-handle.scss: -------------------------------------------------------------------------------- 1 | @mixin mobile { 2 | @media (max-width: 600px) { 3 | @content; 4 | } 5 | } 6 | 7 | .resize-handle-container { 8 | position: absolute; 9 | top: 0; 10 | right: 0; 11 | height: 100%; 12 | cursor: ew-resize; 13 | opacity: 0; 14 | transition: opacity 0.1s; 15 | background-color: #f5f5f5; 16 | 17 | @include mobile { 18 | display: none; 19 | } 20 | 21 | &:hover { 22 | opacity: 1; 23 | } 24 | 25 | .resize-handle { 26 | position: sticky; 27 | width: 10px; 28 | height: 50px; 29 | top: calc(50% - 50px/2); 30 | background-color: rgb(229, 231, 235); 31 | background-repeat: no-repeat; 32 | background-position: 50% center; 33 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); 34 | } 35 | } -------------------------------------------------------------------------------- /js/packages/bene-components/src/resize-handle.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, unsafeCSS } from "lit"; 2 | import { customElement } from "lit/decorators.js"; 3 | import { Ref, createRef, ref } from "lit/directives/ref.js"; 4 | 5 | import cssContent from "./resize-handle.scss?inline"; 6 | 7 | @customElement("resize-handle") 8 | export class ResizeHandle extends LitElement { 9 | article: HTMLElement; 10 | handleRef: Ref = createRef(); 11 | 12 | static styles = unsafeCSS(cssContent); 13 | 14 | constructor() { 15 | super(); 16 | this.article = document.querySelector("article")!; 17 | } 18 | 19 | onMouseDown(event: MouseEvent) { 20 | event.stopPropagation(); 21 | event.preventDefault(); 22 | 23 | let handle = this.handleRef.value!; 24 | let handleBounds = handle.getBoundingClientRect(); 25 | let deltaX = handleBounds.right - event.x; 26 | 27 | let self = this; 28 | function onMouseMove(event: MouseEvent) { 29 | let width = Math.abs(event.x + deltaX - window.innerWidth / 2) * 2; 30 | self.article.style.maxWidth = `${width}px`; 31 | } 32 | 33 | function onMouseUp() { 34 | document.removeEventListener("mousemove", onMouseMove); 35 | document.removeEventListener("mouseup", onMouseUp); 36 | } 37 | 38 | document.addEventListener("mousemove", onMouseMove); 39 | document.addEventListener("mouseup", onMouseUp); 40 | } 41 | 42 | render() { 43 | return html`
47 |
48 |
`; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /js/packages/bene-components/src/syntax-highlight.ts: -------------------------------------------------------------------------------- 1 | import { html } from "@codemirror/lang-html"; 2 | import { rust } from "@codemirror/lang-rust"; 3 | import { EditorState, Extension } from "@codemirror/state"; 4 | import { EditorView, minimalSetup } from "codemirror"; 5 | import { LitElement } from "lit"; 6 | import { customElement, property } from "lit/decorators.js"; 7 | 8 | @customElement("syntax-highlight") 9 | export class SyntaxHighlight extends LitElement { 10 | editor: EditorView | undefined; 11 | 12 | @property() 13 | language?: string; 14 | 15 | @property({ attribute: "word-wrap", type: Boolean }) 16 | wordWrap = false; 17 | 18 | connectedCallback() { 19 | super.connectedCallback(); 20 | 21 | let doc = this.children.item(0)!.querySelector("code")!.innerText; 22 | let root = this.shadowRoot; 23 | if (!root) throw new Error("Shadow root is null"); 24 | 25 | let extensions = [ 26 | minimalSetup, 27 | EditorState.readOnly.of(true), 28 | EditorView.baseTheme({ 29 | ".cm-content": { fontSize: "90%" }, 30 | }), 31 | ]; 32 | 33 | if (this.language) { 34 | let languages: { [lang: string]: () => Extension } = { rust, html }; 35 | let langConstructor = languages[this.language]; 36 | if (langConstructor) extensions.push(langConstructor()); 37 | else console.warn(`Missing language package: ${langConstructor}`); 38 | } 39 | 40 | if (this.wordWrap) extensions.push(EditorView.lineWrapping); 41 | 42 | this.editor = new EditorView({ doc, root, extensions }); 43 | } 44 | 45 | render() { 46 | return this.editor!.dom; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /js/packages/bene-components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "src" 5 | ], 6 | "compilerOptions": { 7 | "noEmit": true, 8 | "types": [ 9 | "vite/client" 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /js/packages/bene-components/vite.config.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { resolve } from "path"; 3 | import { defineConfig } from "vite"; 4 | 5 | let manifest = JSON.parse(fs.readFileSync("package.json", "utf-8")); 6 | export default defineConfig(({ mode }) => ({ 7 | build: { 8 | lib: { 9 | entry: resolve(__dirname, "src/main.ts"), 10 | name: "BeneComponents", 11 | formats: ["iife"], 12 | }, 13 | rollupOptions: { 14 | external: Object.keys(manifest.dependencies || {}), 15 | }, 16 | }, 17 | define: { 18 | "process.env.NODE_ENV": JSON.stringify(mode), 19 | }, 20 | test: { 21 | environment: "jsdom", 22 | deps: { 23 | inline: [/^(?!.*vitest).*$/], 24 | }, 25 | }, 26 | })); 27 | -------------------------------------------------------------------------------- /js/packages/bene-desktop/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "../../.eslintrc.cjs", 3 | "ignorePatterns": ["src/bindings.ts"], 4 | "env": { 5 | "browser": true 6 | } 7 | } -------------------------------------------------------------------------------- /js/packages/bene-desktop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /js/packages/bene-desktop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bene-desktop", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "depot": { 6 | "platform": "browser", 7 | "no-server": true 8 | }, 9 | "devDependencies": { 10 | "@tauri-apps/cli": "^2.0.0-alpha.18", 11 | "jsdom": "^23.0.1", 12 | "normalize.css": "^8.0.1", 13 | "@tauri-apps/api": "^2.0.0-alpha.12", 14 | "solid-js": "^1.8.7", 15 | "bene-reader": "workspace:*" 16 | } 17 | } -------------------------------------------------------------------------------- /js/packages/bene-desktop/public/bene-reader: -------------------------------------------------------------------------------- 1 | ../node_modules/bene-reader/dist -------------------------------------------------------------------------------- /js/packages/bene-desktop/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api/primitives"; 2 | 3 | window.addEventListener("message", async event => { 4 | let readerIframe = document.getElementById("reader")! as HTMLIFrameElement; 5 | let readerWindow = readerIframe.contentWindow!; 6 | 7 | let message = event.data; 8 | console.log("Parent received message", message); 9 | 10 | if (message.type == "ready") { 11 | let epubResult; 12 | try { 13 | let epub = await invoke("epub", {}); 14 | epubResult = { status: "ok", data: { metadata: epub, url: undefined } }; 15 | } catch (e: any) { 16 | epubResult = { status: "error", error: e.toString() }; 17 | } 18 | readerWindow.postMessage({ type: "loaded-epub", data: epubResult }, "*"); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /js/packages/bene-desktop/styles/index.css: -------------------------------------------------------------------------------- 1 | @import "normalize.css/normalize.css"; 2 | 3 | iframe { 4 | border: none; 5 | width: 100dvw; 6 | height: 100dvh; 7 | position: absolute; 8 | left: 0; 9 | top: 0; 10 | } -------------------------------------------------------------------------------- /js/packages/bene-desktop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "src" 5 | ], 6 | "compilerOptions": { 7 | "noEmit": true, 8 | "types": [ 9 | "vite/client" 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /js/packages/bene-desktop/vite.config.ts: -------------------------------------------------------------------------------- 1 | import fg from "fast-glob"; 2 | import path from "path"; 3 | import { Plugin, defineConfig } from "vite"; 4 | import solidPlugin from "vite-plugin-solid"; 5 | 6 | // Ensures that bene-desktop rebuilds when bene-reader rebuilds 7 | let watchPublicPlugin: Plugin = { 8 | name: "watch-public-plugin", 9 | async buildStart() { 10 | let files = await fg("public/**/*"); 11 | for (let file of files) { 12 | this.addWatchFile(path.resolve(file)); 13 | } 14 | }, 15 | }; 16 | 17 | export default defineConfig(({ mode }) => ({ 18 | plugins: [solidPlugin(), watchPublicPlugin], 19 | appType: "mpa", 20 | base: "./", 21 | define: { 22 | "process.env.NODE_ENV": JSON.stringify(mode), 23 | }, 24 | test: { 25 | environment: "jsdom", 26 | deps: { 27 | inline: [/^(?!.*vitest).*$/], 28 | }, 29 | }, 30 | })); 31 | -------------------------------------------------------------------------------- /js/packages/bene-reader/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "../../.eslintrc.cjs", 3 | "env": { 4 | "browser": true 5 | } 6 | } -------------------------------------------------------------------------------- /js/packages/bene-reader/img/toolbarButton-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /js/packages/bene-reader/img/toolbarButton-pageDown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /js/packages/bene-reader/img/toolbarButton-pageUp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /js/packages/bene-reader/img/toolbarButton-sidebarToggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /js/packages/bene-reader/img/toolbarButton-zoomIn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /js/packages/bene-reader/img/toolbarButton-zoomOut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /js/packages/bene-reader/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /js/packages/bene-reader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bene-reader", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "depot": { 6 | "platform": "browser", 7 | "no-server": true 8 | }, 9 | "devDependencies": { 10 | "@solid-primitives/scheduled": "^1.4.1", 11 | "@types/lodash": "^4.14.202", 12 | "jsdom": "^23.0.1", 13 | "lodash": "^4.17.21", 14 | "normalize.css": "^8.0.1", 15 | "solid-js": "^1.8.7" 16 | }, 17 | "dependencies": { 18 | "bene-components": "workspace:*" 19 | } 20 | } -------------------------------------------------------------------------------- /js/packages/bene-reader/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { debounce, throttle } from "@solid-primitives/scheduled"; 2 | import componentStyleUrl from "bene-components/dist/style.css?url"; 3 | import componentScriptUrl from "bene-components?url"; 4 | import _ from "lodash"; 5 | import { 6 | createContext, 7 | createEffect, 8 | createSignal, 9 | on, 10 | onMount, 11 | useContext, 12 | } from "solid-js"; 13 | import { SetStoreFunction, createStore } from "solid-js/store"; 14 | import { render } from "solid-js/web"; 15 | 16 | import navCssUrl from "../styles/nav.scss?worker&url"; 17 | 18 | // import { Rendition, commands } from "./bindings"; 19 | 20 | // type Result = { status: "ok"; data: T } | { status: "error"; error: E }; 21 | // type Epub = { renditions: Rendition[] }; 22 | 23 | function insertJs(doc: Document, url: string) { 24 | let script = doc.createElement("script"); 25 | script.setAttribute("type", "text/javascript"); 26 | script.setAttribute("src", url); 27 | doc.body.appendChild(script); 28 | } 29 | 30 | function insertCss(doc: Document, url: string) { 31 | let link = doc.createElement("link"); 32 | link.setAttribute("rel", "stylesheet"); 33 | link.setAttribute("type", "text/css"); 34 | link.setAttribute("href", url); 35 | doc.head.appendChild(link); 36 | } 37 | 38 | interface PageInfo { 39 | currentPage: number; 40 | numPages: number; 41 | pageHeight: number; 42 | container: Window; 43 | } 44 | 45 | interface State { 46 | renditionIndex: number; 47 | chapterIndex: number; 48 | fontSize: number; 49 | showNav: boolean; 50 | width: number; 51 | pageInfo?: PageInfo; 52 | epub: any; 53 | url?: URL; 54 | initialPath?: string; 55 | 56 | rendition(): any; 57 | chapterId(): string; 58 | } 59 | 60 | let epubUrl = (rendition: any, href: string) => 61 | `epub-content/${rendition.root ? rendition.root + "/" : ""}${href}`; 62 | 63 | let StateContext = createContext<[State, SetStoreFunction] | undefined>( 64 | undefined 65 | ); 66 | 67 | const DEBOUNCE_TIME = 250; 68 | 69 | function Toolbar() { 70 | let [state, setState] = useContext(StateContext)!; 71 | 72 | function setFont(delta: number) { 73 | let fontSize = Math.max(state.fontSize + delta, 1); 74 | setState({ fontSize }); 75 | } 76 | 77 | function setPage(delta: number) { 78 | let pageInfo = state.pageInfo; 79 | if (!pageInfo) return; 80 | let currentPage = _.clamp( 81 | pageInfo.currentPage + delta, 82 | 1, 83 | pageInfo.numPages 84 | ); 85 | 86 | pageInfo.container.scrollTo({ 87 | top: pageInfo.pageHeight * (currentPage - 1), 88 | }); 89 | 90 | setState({ 91 | pageInfo: { 92 | ...pageInfo, 93 | currentPage, 94 | }, 95 | }); 96 | } 97 | 98 | function downloadEpub(url: URL) { 99 | let a = document.createElement("a"); 100 | a.href = url.toString(); 101 | a.download = _.last(url.pathname.split("/"))!; 102 | document.body.appendChild(a); 103 | a.click(); 104 | document.body.removeChild(a); 105 | } 106 | 107 | return ( 108 |
109 |
110 |
137 |
138 |
163 |
164 | {state.url ? ( 165 |
172 |
173 | ); 174 | } 175 | 176 | function Nav(props: { navigateEvent: EventTarget }) { 177 | let [state] = useContext(StateContext)!; 178 | let navUrl = () => { 179 | let rend = state.rendition(); 180 | let items = rend.package.manifest.item!; 181 | let href = items.find((item: any) => 182 | item["@properties"] 183 | ? item["@properties"].split(" ").includes("nav") 184 | : false 185 | )!["@href"]; 186 | return epubUrl(rend, href); 187 | }; 188 | 189 | let iframeRef: HTMLIFrameElement | undefined; 190 | onMount(() => { 191 | let iframe = iframeRef!; 192 | 193 | iframe.addEventListener("load", () => { 194 | let navDoc = iframe.contentDocument!; 195 | insertJs(navDoc, navCssUrl); 196 | 197 | navDoc.querySelectorAll("nav a").forEach(node => 198 | node.addEventListener("click", event => { 199 | event.preventDefault(); 200 | let href = (event.target as any).href; 201 | props.navigateEvent.dispatchEvent( 202 | new CustomEvent("navigate", { detail: href }) 203 | ); 204 | }) 205 | ); 206 | 207 | let navEl = navDoc.querySelector("nav")!; 208 | let navWidth = getComputedStyle(iframe).getPropertyValue("--nav-width"); 209 | // TODO: make this react to changes in nav-width 210 | navEl.style.width = navWidth; 211 | }); 212 | }); 213 | 214 | return ( 215 |