Compare commits
No commits in common. "master" and "zola-version" have entirely different histories.
master
...
zola-versi
326 changed files with 1796 additions and 71089 deletions
28
.build.yml
28
.build.yml
|
@ -1,28 +0,0 @@
|
||||||
image: alpine/edge
|
|
||||||
oauth: pages.sr.ht/PAGES:RW
|
|
||||||
packages:
|
|
||||||
- hut
|
|
||||||
- npm
|
|
||||||
- rsync
|
|
||||||
- typst
|
|
||||||
environment:
|
|
||||||
site: mzhang.io
|
|
||||||
secrets:
|
|
||||||
- 0b26b413-7901-41c3-a4e2-3c752228ffcb
|
|
||||||
sources:
|
|
||||||
- https://git.sr.ht/~mzhang/blog
|
|
||||||
tasks:
|
|
||||||
- install: |
|
|
||||||
sudo npm install -g pnpm
|
|
||||||
- build: |
|
|
||||||
cd blog
|
|
||||||
pnpm install
|
|
||||||
pnpm run build
|
|
||||||
# hugo --buildDrafts --minify --baseURL https://mzhang.io
|
|
||||||
- upload: |
|
|
||||||
cd blog/dist
|
|
||||||
tar -cvz . > ../site.tar.gz
|
|
||||||
cd ..
|
|
||||||
hut pages publish -d $site site.tar.gz
|
|
||||||
# echo "mzhang.io ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ+QmM4EO3Fwc1ZcvWV2IY9VF04T0H9brorGj9Udp" >> ~/.ssh/known_hosts
|
|
||||||
# rsync -azvrP dist/ sourcehutBuilds@mzhang.io:/mnt/storage/svcdata/blog-public
|
|
|
@ -1 +0,0 @@
|
||||||
*
|
|
6
.drone.yml
Normal file
6
.drone.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pipeline:
|
||||||
|
build:
|
||||||
|
image: j1mc/docker-zola
|
||||||
|
commands:
|
||||||
|
- zola build
|
||||||
|
|
|
@ -6,4 +6,4 @@ insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 4
|
||||||
|
|
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
||||||
use flake
|
|
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,2 +0,0 @@
|
||||||
public/katex/**/* linguist-vendored
|
|
||||||
src/**/*.md linguist-documentation=false
|
|
35
.gitignore
vendored
35
.gitignore
vendored
|
@ -1,34 +1 @@
|
||||||
# build output
|
public
|
||||||
dist/
|
|
||||||
# generated types
|
|
||||||
.astro/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
|
||||||
.env
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# macOS-specific files
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
PragmataPro-Mono-Liga-Regular-Nerd-Font-Complete.woff2
|
|
||||||
*.agdai
|
|
||||||
_build
|
|
||||||
.direnv
|
|
||||||
|
|
||||||
|
|
||||||
/result
|
|
||||||
.pnpm-store
|
|
||||||
|
|
||||||
/public/generated
|
|
||||||
|
|
||||||
.frontmatter
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
public/katex
|
|
||||||
pnpm-lock.yaml
|
|
||||||
src/styles/fork-awesome
|
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"plugins": ["prettier-plugin-astro"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "*.astro",
|
|
||||||
"options": {
|
|
||||||
"parser": "astro"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"useTabs": false,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"singleQuote": false,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"printWidth": 100
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
public
|
|
||||||
package-lock.json
|
|
||||||
src/styles/fork-awesome
|
|
||||||
pnpm-lock.yaml
|
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"astro-build.astro-vscode",
|
|
||||||
"eliostruyf.vscode-front-matter"
|
|
||||||
],
|
|
||||||
"unwantedRecommendations": []
|
|
||||||
}
|
|
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"command": "./node_modules/.bin/astro dev",
|
|
||||||
"name": "Development server",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "node-terminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"npm.packageManager": "pnpm"
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
steps:
|
|
||||||
build:
|
|
||||||
image: git.mzhang.io/michael/blog-docker-builder:6gzd1rhcl41y02yw4z9kpjgrhxifqyfs
|
|
||||||
environment:
|
|
||||||
- ASTRO_TELEMETRY_DISABLED=1
|
|
||||||
commands:
|
|
||||||
- mkdir /tmp
|
|
||||||
- rm -rf node_modules
|
|
||||||
- npm i -g pnpm@9.4.0
|
|
||||||
- npx pnpm install --frozen-lockfile
|
|
||||||
- npx pnpm run build
|
|
||||||
when:
|
|
||||||
- event: push
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
image: git.mzhang.io/michael/blog-docker-builder:6gzd1rhcl41y02yw4z9kpjgrhxifqyfs
|
|
||||||
commands:
|
|
||||||
- mc alias set $AWS_DEFAULT_REGION $AWS_ENDPOINT_URL $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY --api S3v4
|
|
||||||
- mc mirror --overwrite ./dist/ $AWS_DEFAULT_REGION/mzhang-io-website/
|
|
||||||
secrets:
|
|
||||||
- AWS_ACCESS_KEY_ID
|
|
||||||
- AWS_DEFAULT_REGION
|
|
||||||
- AWS_ENDPOINT_URL
|
|
||||||
- AWS_SECRET_ACCESS_KEY
|
|
||||||
when:
|
|
||||||
- branch: master
|
|
||||||
event: push
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Michael's Blog
|
|
||||||
|
|
||||||
https://mzhang.io
|
|
||||||
|
|
||||||
License: GPL-3.0 code / CC-BY-SA-4.0 contents
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { defineConfig } from "astro/config";
|
|
||||||
import mdx from "@astrojs/mdx";
|
|
||||||
import sitemap from "@astrojs/sitemap";
|
|
||||||
import { rehypeAccessibleEmojis } from "rehype-accessible-emojis";
|
|
||||||
import remarkReadingTime from "./plugin/remark-reading-time";
|
|
||||||
import remarkEmoji from "remark-emoji";
|
|
||||||
import remarkDescription from "astro-remark-description";
|
|
||||||
import remarkAdmonitions, { mkdocsConfig } from "./plugin/remark-admonitions";
|
|
||||||
import remarkMath from "remark-math";
|
|
||||||
import rehypeKatex from "rehype-katex";
|
|
||||||
import remarkTypst from "./plugin/remark-typst";
|
|
||||||
import rehypeLinkHeadings from "@justfork/rehype-autolink-headings";
|
|
||||||
import rehypeSlug from "rehype-slug";
|
|
||||||
|
|
||||||
import markdoc from "@astrojs/markdoc";
|
|
||||||
import remarkAgda from "./plugin/remark-agda";
|
|
||||||
|
|
||||||
const outDir = "dist";
|
|
||||||
const base = process.env.BASE ?? "/";
|
|
||||||
const publicDir = "public";
|
|
||||||
|
|
||||||
// https://astro.build/config
|
|
||||||
export default defineConfig({
|
|
||||||
site: "https://mzhang.io",
|
|
||||||
prefetch: true,
|
|
||||||
integrations: [mdx(), sitemap(), markdoc()],
|
|
||||||
|
|
||||||
outDir,
|
|
||||||
base,
|
|
||||||
trailingSlash: "always",
|
|
||||||
publicDir,
|
|
||||||
|
|
||||||
markdown: {
|
|
||||||
syntaxHighlight: "shiki",
|
|
||||||
shikiConfig: { theme: "css-variables" },
|
|
||||||
remarkPlugins: [
|
|
||||||
() => remarkAgda({ outDir, base, publicDir }),
|
|
||||||
remarkMath,
|
|
||||||
[remarkAdmonitions, mkdocsConfig],
|
|
||||||
remarkReadingTime,
|
|
||||||
remarkTypst,
|
|
||||||
remarkEmoji,
|
|
||||||
[
|
|
||||||
remarkDescription,
|
|
||||||
{
|
|
||||||
name: "excerpt",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
rehypePlugins: [
|
|
||||||
[rehypeKatex, {}],
|
|
||||||
rehypeAccessibleEmojis,
|
|
||||||
rehypeSlug,
|
|
||||||
[rehypeLinkHeadings, { behavior: "wrap" }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
17
biome.json
17
biome.json
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
|
|
||||||
"organizeImports": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 2
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
include: src/content/posts src
|
|
||||||
depend: standard-library cubical
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
22
config.toml
Normal file
22
config.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
title = "michael's blog"
|
||||||
|
base_url = "https://iptq.io"
|
||||||
|
|
||||||
|
compile_sass = true
|
||||||
|
highlight_code = true
|
||||||
|
generate_rss = true
|
||||||
|
|
||||||
|
taxonomies = [
|
||||||
|
{ name = "tags", rss = true }
|
||||||
|
]
|
||||||
|
|
||||||
|
[external_renderers]
|
||||||
|
dot = "set -e; echo -n '<p class=\"graphviz\">'; dot -Tsvg | tail -n +4; echo '</p>'"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
nav_links = [
|
||||||
|
{ url = "/", text = "home" },
|
||||||
|
{ url = "/pages/about", text = "about" },
|
||||||
|
{ url = "/pages/projects", text = "projects" },
|
||||||
|
{ url = "/setup", text = "setup" },
|
||||||
|
{ url = "/pages", text = "all pages" },
|
||||||
|
]
|
|
@ -1,16 +1,18 @@
|
||||||
---
|
+++
|
||||||
title: My new life stack
|
title = "my new life stack"
|
||||||
date: 2018-02-01
|
date = 2018-02-01
|
||||||
tags: ["arch", "linux", "setup", "computers"]
|
|
||||||
---
|
|
||||||
|
|
||||||
This is my first post on my new blog! <!--more--> I used to put a CTF challenge writeup here but decided to change it up a bit. Recently, I've been changing a lot of the technology that I use day to day. Here's some of the changes that I've made!
|
[taxonomies]
|
||||||
|
tags = ["arch", "linux", "setup", "computers"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
This is my first post on my new blog! I used to put a CTF challenge writeup here but decided to change it up a bit. Recently, I've been changing a lot of the technology that I use day to day. Here's some of the changes that I've made!
|
||||||
|
|
||||||
## Operating System
|
## Operating System
|
||||||
|
|
||||||
I've ran regular Ubuntu on my laptop for a while, then switched to Elementary OS, which I found a lot more pleasing to use. After using Elementary OS for about 6 months, some of the software on my computer started behaving strangely, and I decided it was time for some change.
|
I've ran regular Ubuntu on my laptop for a while, then switched to Elementary OS, which I found a lot more pleasing to use. After using Elementary OS for about 6 months, some of the software on my computer started behaving strangely, and I decided it was time for some change.
|
||||||
|
|
||||||
`````
|
```
|
||||||
# michael @ arch in ~ [3:20:09]
|
# michael @ arch in ~ [3:20:09]
|
||||||
$ screenfetch
|
$ screenfetch
|
||||||
-`
|
-`
|
||||||
|
@ -33,7 +35,7 @@ $ screenfetch
|
||||||
`++:. `-/+/
|
`++:. `-/+/
|
||||||
.` `/
|
.` `/
|
||||||
|
|
||||||
`````
|
```
|
||||||
|
|
||||||
I installed Arch Linux on my laptop the day before yesterday. I've used Arch Linux before, about a year ago, so setup was relatively familiar. On top of Arch Linux, I'm using the very widely recommended i3 tiling window manager, and urxvt terminal emulator.
|
I installed Arch Linux on my laptop the day before yesterday. I've used Arch Linux before, about a year ago, so setup was relatively familiar. On top of Arch Linux, I'm using the very widely recommended i3 tiling window manager, and urxvt terminal emulator.
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
---
|
+++
|
||||||
title: "Cleaning up your shell"
|
title = "cleaning up your shell"
|
||||||
date: 2018-02-25
|
date = 2018-02-25
|
||||||
tags: ["computers", "linux", "terminal"]
|
|
||||||
languages: ["bash"]
|
|
||||||
---
|
|
||||||
|
|
||||||
Is your shell loading slower than it used to? Maybe you've been sticking a bit more into your `.bashrc`/`.zshrc` than you thought. <!--more-->
|
[taxonomies]
|
||||||
|
tags = ["computers", "linux", "bash", "terminal"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
Is your shell loading slower than it used to? Maybe you've been sticking a bit more into your `.bashrc`/`.zshrc` than you thought.
|
||||||
|
|
||||||
It's only been a couple weeks since I installed my computer, and already my shell has been starting to lag. Since there's not that much I've put into my `.zshrc` file, I knew who the main culprits were. Namely, oh-my-zsh's "git" plugin and the nvm (node version manager) trying to load itself on startup. I'm not exactly in a situation where I need nvm most of the time I open my shell, so getting rid of that made my shell load a lot faster. It also means that every time I want to use node or npm, I'd have to manually call nvm, but that's not as important to me as a faster shell load time, especially since I don't really touch node that much.
|
It's only been a couple weeks since I installed my computer, and already my shell has been starting to lag. Since there's not that much I've put into my `.zshrc` file, I knew who the main culprits were. Namely, oh-my-zsh's "git" plugin and the nvm (node version manager) trying to load itself on startup. I'm not exactly in a situation where I need nvm most of the time I open my shell, so getting rid of that made my shell load a lot faster. It also means that every time I want to use node or npm, I'd have to manually call nvm, but that's not as important to me as a faster shell load time, especially since I don't really touch node that much.
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
---
|
+++
|
||||||
title: "Fixing tmux colors"
|
title = "fixing tmux colors"
|
||||||
date: 2018-04-23
|
date = 2018-04-23
|
||||||
tags: ["computers"]
|
|
||||||
---
|
[taxonomies]
|
||||||
|
tags = ["computers", "terminal"]
|
||||||
|
+++
|
||||||
|
|
||||||
Put this in your `~/.tmux.conf`.
|
Put this in your `~/.tmux.conf`.
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
---
|
+++
|
||||||
title: "Web apps"
|
title = "web apps"
|
||||||
date: 2018-05-28
|
date = 2018-05-28
|
||||||
tags: ["computers", "web", "things-that-are-bad"]
|
|
||||||
languages: ["javascript"]
|
|
||||||
---
|
|
||||||
|
|
||||||
The other day, I just turned off JavaScript from my browser. <!--more--> "fucking neckbeard", "you'll turn it back in 2 weeks", "living without JavaScript is like living without electricity" were some of the responses I got. And they might be right. But let's see why things are the way they are and what we can do about it.
|
[taxonomies]
|
||||||
|
tags = ["computers", "javascript", "web-dev", "rant", "things-that-are-bad"]
|
||||||
|
+++
|
||||||
|
|
||||||
## What is the purpose of the web?
|
The other day, I just turned off JavaScript from my browser. "fucking neckbeard", "you'll turn it back in 2 weeks", "living without JavaScript is like living without electricity" were some of the responses I got. And they might be right. But let's see why things are the way they are and what we can do about it.
|
||||||
|
|
||||||
|
## what is the purpose of the web?
|
||||||
|
|
||||||
Well, the answer's pretty obvious, right? So you can surf it. But what does that even mean anymore? In the past, surfing the web meant viewing websites. You'd open something like your favorite news website, and it'd show you some of the latest updates. Or maybe you'd open the website for some company to find out their telephone so you can contact them. In other words, it was a channel from which you could receive information.
|
Well, the answer's pretty obvious, right? So you can surf it. But what does that even mean anymore? In the past, surfing the web meant viewing websites. You'd open something like your favorite news website, and it'd show you some of the latest updates. Or maybe you'd open the website for some company to find out their telephone so you can contact them. In other words, it was a channel from which you could receive information.
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ If you wanted to do anything more complicated or that required more interaction,
|
||||||
|
|
||||||
Things aren't that way anymore. For some reason, the web browser has become the all-in-one client for every service. Instead of simply acting as a HTTP client, your browser is now also capable of running full-blown 3D games, chat rooms, real-time word processors, and [full x86 emulators, apparently](http://copy.sh/v86/). What the hell happened?
|
Things aren't that way anymore. For some reason, the web browser has become the all-in-one client for every service. Instead of simply acting as a HTTP client, your browser is now also capable of running full-blown 3D games, chat rooms, real-time word processors, and [full x86 emulators, apparently](http://copy.sh/v86/). What the hell happened?
|
||||||
|
|
||||||
## Spoiler alert: Javascript happened
|
## spoiler alert: javascript happened
|
||||||
|
|
||||||
JavaScript happened. That little _scripting_ language invented to, you know, make some hover animation on your page or have dropdowns on your menu bar. Thanks to the introduction of JavaScript (and jQuery especially), developers stopped viewing webpages as Word documents that you can share, and more like canvases. Hover animations are cute and dropdowns are useful. Sure. But when this _scripting_ language starts turning into a _systems_ language (for lack of a better term), you have a problem. When's the last time you used Perl to write an operating system?
|
JavaScript happened. That little _scripting_ language invented to, you know, make some hover animation on your page or have dropdowns on your menu bar. Thanks to the introduction of JavaScript (and jQuery especially), developers stopped viewing webpages as Word documents that you can share, and more like canvases. Hover animations are cute and dropdowns are useful. Sure. But when this _scripting_ language starts turning into a _systems_ language (for lack of a better term), you have a problem. When's the last time you used Perl to write an operating system?
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ On top of that, look at these huge frameworks that almost every company is hirin
|
||||||
|
|
||||||
Look at Facebook's home page. Just from regular use, that webpage itself can use over 4 gigabytes of RAM. It makes large amounts of network calls for data that's all just being stored in memory. And everyone who opens the Facebook website (for the first time) must download _all_ of that JavaScript. The website has its own tabs (within the page, yes) for chat windows, games, advertisements, embedded video players, and much more I probably didn't even know about. Why are we running full-blown apps in our browser?
|
Look at Facebook's home page. Just from regular use, that webpage itself can use over 4 gigabytes of RAM. It makes large amounts of network calls for data that's all just being stored in memory. And everyone who opens the Facebook website (for the first time) must download _all_ of that JavaScript. The website has its own tabs (within the page, yes) for chat windows, games, advertisements, embedded video players, and much more I probably didn't even know about. Why are we running full-blown apps in our browser?
|
||||||
|
|
||||||
## Ok but what can i do
|
## ok but what can i do
|
||||||
|
|
||||||
There's a number of things that can be done to turn this state of the web down a different path. Here's some ideas for users:
|
There's a number of things that can be done to turn this state of the web down a different path. Here's some ideas for users:
|
||||||
|
|
||||||
|
@ -38,9 +39,9 @@ For developers:
|
||||||
- Consider the impact of every library you include. Can you do without it? What if you just wrote something from scratch instead of importing a full framework to do it?
|
- Consider the impact of every library you include. Can you do without it? What if you just wrote something from scratch instead of importing a full framework to do it?
|
||||||
- Write more non-JavaScript software/libraries. Developers have only turned towards sticking JavaScript everywhere because it's easy to use, and libraries are readily available through npm.
|
- Write more non-JavaScript software/libraries. Developers have only turned towards sticking JavaScript everywhere because it's easy to use, and libraries are readily available through npm.
|
||||||
|
|
||||||
## Ok but what can you do
|
## ok but what can you do
|
||||||
|
|
||||||
I'm helping with a project called flubber, which originated as an IRC bouncer, but is slowly turning into a general messaging protocol. All-in-one messengers exist (and a particular one exists by that name exactly), but they all work by opening a browser view and just loading the page within it, so it's no different from just opening tabs in a browser. Flubber will communicate with these services through APIs, and then expose a uniform interface to clients which makes it easy to bring all into a single view. Check out my progress [here](https://github.com/iptq/flubber). Other than that, I'm also working on making my websites as light as possible in general, including this one which has no required Javascript (some pages use Katex for displaying math elements but are still readable without).
|
I'm helping with a project called flubber, which originated as an IRC bouncer, but is slowly turning into a general messaging protocol. All-in-one messengers exist (and a particular one exists by that name exactly), but they all work by opening a browser view and just loading the page within it, so it's no different from just opening tabs in a browser. Flubber will communicate with these services through APIs, and then expose a uniform interface to clients which makes it easy to bring all into a single view. Check out my progress [here](https://github.com/iptq/flubber). Other than that, I'm also working on making my websites as light as possible in general, including this one (which, by the way, has 0 JavaScript content).
|
||||||
|
|
||||||
And of course, I've disabled JavaScript in my browser.
|
And of course, I've disabled JavaScript in my browser.
|
||||||
|
|
|
@ -1,31 +1,25 @@
|
||||||
---
|
+++
|
||||||
title: "Twenty years of attacks on rsa with examples"
|
title = "twenty years of attacks on rsa.. with examples!"
|
||||||
date: 2018-10-26
|
date = 2018-10-26
|
||||||
toc: true
|
|
||||||
tags: ["ctf", "crypto", "rsa"]
|
|
||||||
languages: ["python"]
|
|
||||||
math: true
|
|
||||||
---
|
|
||||||
|
|
||||||
There's [a great paper][1] I found by Dan Boneh from 1998 highlighting the
|
[taxonomies]
|
||||||
weaknesses of the RSA cryptosystem. I found this paper to be a particularly
|
tags = ["math", "crypto", "python"]
|
||||||
enlightening read (and interestingly enough, it's been 20 years since that
|
|
||||||
paper!), so here I'm going to reiterate some of the attacks described in the
|
|
||||||
paper, but using examples with numbers in them. <!--more-->
|
|
||||||
|
|
||||||
That being said, I _am_ going to skip over the primer of how the RSA
|
[extra]
|
||||||
cryptosystem works, since there's already a great number of resources on that.
|
toc = true
|
||||||
|
+++
|
||||||
|
|
||||||
## Factoring large integers
|
# 1. introduction
|
||||||
|
|
||||||
Obviously this is a pretty bruteforce-ish way to crack the cryptosystem, and
|
There's [this great paper][1] by Dan Boneh from 1998 about the RSA cryptosystem and its weaknesses. I found this paper to be a particularly interesting read (and interestingly enough, it's been 20 years since that paper!), so here I'm going to reiterate some of the attacks described in the paper, but using examples with numbers in them. (Also please excuse the lack of proper formatting, I've yet to figure out how to get Gutenberg to accept Latex)
|
||||||
probably won't work in time for you to see the result, but can still be
|
|
||||||
considered an attack vector. This trick works by just factoring the modulus,
|
|
||||||
$N$. With $N$, finding the private exponent $d$ from the public exponent $e$ is
|
|
||||||
a piece of cake.
|
|
||||||
|
|
||||||
Let's choose some small numbers to demonstrate this one (you can follow along in
|
That being said, I _am_ going to skip over the primer of how the RSA cryptosystem works, since there's already a great number of resources on how to do that.
|
||||||
a Python REPL if you want):
|
|
||||||
|
## 1.1 factoring large integers
|
||||||
|
|
||||||
|
Obviously this is a pretty bruteforce-ish way to crack the cryptosystem, and probably won't work in time for you to see the result, but can still be considered an attack vector. This trick works by just factoring the modulus, N. With N, finding the private exponent d from the public exponent e is a piece of cake.
|
||||||
|
|
||||||
|
Let's choose some small numbers to demonstrate this one (you can follow along in a Python REPL if you want):
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> N = 881653369
|
>>> N = 881653369
|
||||||
|
@ -33,9 +27,7 @@ a Python REPL if you want):
|
||||||
>>> c = 875978376
|
>>> c = 875978376
|
||||||
```
|
```
|
||||||
|
|
||||||
$N$ is clearly factorable in this case, and we can use resources like
|
N is clearly factorable in this case, and we can use resources like [msieve][7] or [factordb][2] to find smaller primes in this case. Since we know now that `N = 20717 * 42557`, we can find the totient of N:
|
||||||
[msieve][7] or [factordb][2] to find smaller primes in this case. Since we know
|
|
||||||
now that $N = 20717 \times 42557$, we can find the totient of $N$:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> p = 20717
|
>>> p = 20717
|
||||||
|
@ -44,8 +36,7 @@ now that $N = 20717 \times 42557$, we can find the totient of $N$:
|
||||||
881590096
|
881590096
|
||||||
```
|
```
|
||||||
|
|
||||||
Now all that's left is to discover the private exponent and solve for the
|
Now all that's left is to discover the private exponent and solve for the original message! (you can find the modular inverse function I used [here][3])
|
||||||
original message! (you can find the modular inverse function I used [here][3])
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> d = modinv(e, tot)
|
>>> d = modinv(e, tot)
|
||||||
|
@ -56,30 +47,17 @@ original message! (you can find the modular inverse function I used [here][3])
|
||||||
|
|
||||||
And that's it! Now let's look at some more sophisticated attacks...
|
And that's it! Now let's look at some more sophisticated attacks...
|
||||||
|
|
||||||
## Elementary attacks
|
# 2. elementary attacks
|
||||||
|
|
||||||
These attacks are related to the _misuse_ of the RSA system. (if you can't tell,
|
These attacks are related to the _misuse_ of the RSA system. (if you can't tell, I'm mirroring the document structure of the original paper)
|
||||||
I'm mirroring the document structure of the original paper)
|
|
||||||
|
|
||||||
### Common modulus
|
## 2.1 common modulus
|
||||||
|
|
||||||
My cryptography professor gave this example as well. Suppose there was a setup
|
My cryptography professor gave this example as well. Suppose there was a setup in which the modulus was reused, maybe for convenience (although I suppose with libraries today, it'd actually be more _inconvenient_ to reuse the key). Key pairs would be issued to different users and they would share public keys with each other and keep private keys to themselves.
|
||||||
in which the modulus was reused, maybe for convenience (although I suppose with
|
|
||||||
libraries today, it'd actually be more _inconvenient_ to reuse the key). Key
|
|
||||||
pairs would be issued to different users and they would share public keys with
|
|
||||||
each other and keep private keys to themselves.
|
|
||||||
|
|
||||||
The problem here is if you have a key pair, and you got someone else's public
|
The problem here is if you have a key pair, and you got someone else's public key, you could easily derive the private key by just factoring the modulus. Let's see how this works with a real example now.
|
||||||
key, you could easily derive the private key by just factoring the modulus.
|
|
||||||
Let's see how this works with a real example now.
|
|
||||||
|
|
||||||
Since this is a big problem if you were to really use this cryptosystem, I'll be
|
Since this is a big problem if you were to really use this cryptosystem, I'll be using actual keys from an actual crypto library instead of the small numbers like in the first example to show that this works on 2048-bit RSA. The library is called [PyCrypto][4], and if you're planning on doing anything related to crypto with Python, it's a good tool to have with you. For now, I'm going to generate a 2048-bit key (by the way, in practice you probably shouldn't be using 2048-bit keys anymore, I'm just trying to spare my computer here).
|
||||||
using actual keys from an actual crypto library instead of the small numbers
|
|
||||||
like in the first example to show that this works on 2048-bit RSA. The library
|
|
||||||
is called [PyCrypto][4], and if you're planning on doing anything related to
|
|
||||||
crypto with Python, it's a good tool to have with you. For now, I'm going to
|
|
||||||
generate a 2048-bit key (by the way, in practice you probably shouldn't be using
|
|
||||||
2048-bit keys anymore, I'm just trying to spare my computer here).
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> from Crypto.PublicKey import RSA
|
>>> from Crypto.PublicKey import RSA
|
||||||
|
@ -87,10 +65,7 @@ generate a 2048-bit key (by the way, in practice you probably shouldn't be using
|
||||||
<_RSAobj @0x7f3d3226dfd0 n(2048),e,d,p,q,u,private>
|
<_RSAobj @0x7f3d3226dfd0 n(2048),e,d,p,q,u,private>
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, normally when you generate a new key, it'd generate a new modulus. For the
|
Now, normally when you generate a new key, it'd generate a new modulus. For the sake of this common modulus attack, we'll force the new key to use the same modulus. This also means we'll have to choose an exponent e other than the default choice of 65537 (see [this link][5] for documentation):
|
||||||
sake of this common modulus attack, we'll force the new key to use the same
|
|
||||||
modulus. This also means we'll have to choose an exponent $e$ other than the
|
|
||||||
default choice of 65537 (see [this link][5] for documentation):
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> N = k1.p * k1.q
|
>>> N = k1.p * k1.q
|
||||||
|
@ -106,33 +81,11 @@ default choice of 65537 (see [this link][5] for documentation):
|
||||||
<_RSAobj @0x7f3d31c7c5f8 n(2048),e,d,p,q,u,private>
|
<_RSAobj @0x7f3d31c7c5f8 n(2048),e,d,p,q,u,private>
|
||||||
```
|
```
|
||||||
|
|
||||||
Ok, now we have two keys, $k_1$ and $k_2$. Now I'll show how using only the public
|
Ok, now we have two keys, `k1` and `k2`. Now I'll show how using only the public and private key of `k1` (assuming this is the pair that we got legitimately from the crypto operator), and the public key of `k2`, which is tied to the same modulus, we can find the private key of `k2`.
|
||||||
and private key of $k_1$ (assuming this is the pair that we got legitimately from
|
|
||||||
the crypto operator), and the public key of $k_2$, which is tied to the same
|
|
||||||
modulus, we can find the private key of $k_2$.
|
|
||||||
|
|
||||||
To do this, we'll try to find the roots of the equation:
|
To do this, we'll try to find the roots of the equation `f(x) = x^2 - (p + q)x + pq`. You'll find that for values of `p` and `q`, this will produce `f(p) = p^2 - p^2 - qp + pq`, and `f(q) = q^2 - pq - q^2 + pq`. We know that `N = pq`. How can we find `p + q`? Since `phi(N) = (p - 1)(q - 1) = pq - p - q + 1`, we can find that `phi(N) = N - (p + q) + 1`, so `p + q = N - phi(N) + 1`. Now we need to use `e` and `d` to estimate `phi(N)`. Recall that `ed = 1 mod phi(N)`. This is equivalent to saying `ed = 1 + k*phi(N)`. Then `(ed - 1) / phi(N) = k`.
|
||||||
|
|
||||||
$$ f(x) = x^2 - (p + q)x + pq $$
|
It turns out that `k` is extremely close to `ed/N`: `ed/N = (1 + k*phi(N)) / N = 1/N + k*phi(N)/N`. `1/N` is basically 0, and `phi(N)` is very close to `N`, so it shouldn't change the value of `k` by very much. We now use `ed/N` to estimate `k`: `phi(N) = (ed - 1) / (ed / N)`.
|
||||||
|
|
||||||
You'll find that for values of $p$ and $q$, this will produce $f(p) = p^2 - p^2
|
|
||||||
\- qp + pq$, and $f(q) = q^2 - pq - q^2 + pq$. We know that $N = pq$. How can we
|
|
||||||
find $p + q$? Since $\phi(N) = (p - 1)(q - 1) = pq - p - q + 1$, we can find
|
|
||||||
that $\phi(N) = N - (p + q) + 1$, so $p + q = N - \phi(N) + 1$.
|
|
||||||
|
|
||||||
Now we need to use $e$ and $d$ to estimate $\phi(N)$. Recall that $ed = 1 \mod
|
|
||||||
\phi(N)$. This is equivalent to saying $ed = 1 + k\phi(N)$. Then $\frac{ed -
|
|
||||||
1}{\phi(N)} = k$.
|
|
||||||
|
|
||||||
It turns out that $k$ is extremely close to $\frac{ed}{N}$:
|
|
||||||
|
|
||||||
$$ \frac{ed}{N} = \frac{1 + k\phi(N)}{N} = \frac{1}{N} + \frac{k\phi(N)}{N} $$
|
|
||||||
|
|
||||||
$\frac{1}{N}$ is basically 0, and $\phi(N)$ is very close to $N$, so it
|
|
||||||
shouldn't change the value of $k$ by very much. We now use $\frac{ed}{N}$ to
|
|
||||||
estimate $k$:
|
|
||||||
|
|
||||||
$$ \phi(N) = \frac{ed - 1}{\frac{ed}{N}} $$
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> from decimal import Decimal, getcontext
|
>>> from decimal import Decimal, getcontext
|
||||||
|
@ -143,7 +96,7 @@ $$ \phi(N) = \frac{ed - 1}{\frac{ed}{N}} $$
|
||||||
Decimal('29977270253913673973269594877868500604696844309480395834898813292056864035968758602074842333119394545818563664205865827843973433118231606201251719390934610989873635763197929136439794366715495587924829697045618064595517091398323127000591150167969423793125376862942962617933168868125721044755585292104012767604575090001864613992237960887242026855773279634028088706121371418922552125986506064146112561599205615974813154971272528592745144988174228621487749404677959591894452249599588096076892574585613962026186332366180174253118634077603697727952204486962202338916762987146793208323561031870496718547544796269555861921652')
|
Decimal('29977270253913673973269594877868500604696844309480395834898813292056864035968758602074842333119394545818563664205865827843973433118231606201251719390934610989873635763197929136439794366715495587924829697045618064595517091398323127000591150167969423793125376862942962617933168868125721044755585292104012767604575090001864613992237960887242026855773279634028088706121371418922552125986506064146112561599205615974813154971272528592745144988174228621487749404677959591894452249599588096076892574585613962026186332366180174253118634077603697727952204486962202338916762987146793208323561031870496718547544796269555861921652')
|
||||||
```
|
```
|
||||||
|
|
||||||
Then we can get $p + q$ through the formula mentioend above:
|
Then we can get `p + q` through the formula mentioend above:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> B = Decimal(N) - phi + 1
|
>>> B = Decimal(N) - phi + 1
|
||||||
|
@ -151,8 +104,7 @@ Decimal('34642192582980793929380236893725052051755685627449634068179923908929124
|
||||||
>>> C = Decimal(N)
|
>>> C = Decimal(N)
|
||||||
```
|
```
|
||||||
|
|
||||||
Check to make sure $B$ and $C$ are integers. If they're not, try using a higher
|
Check to make sure B and C are integers. If they're not, try using a higher precision in `getcontext().prec`. Now solve the quadratic equation:
|
||||||
precision in `getcontext().prec`. Now solve the quadratic equation:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> p = (B + (B * B - 4 * C).sqrt()) / Decimal(2)
|
>>> p = (B + (B * B - 4 * C).sqrt()) / Decimal(2)
|
||||||
|
@ -163,18 +115,13 @@ Decimal('16823427526200025299629360726790945249096067335633200400252946099787748
|
||||||
True
|
True
|
||||||
```
|
```
|
||||||
|
|
||||||
We've successfully recovered $p$ and $q$ from just $N$, $e$, and $d$!
|
We've successfully recovered `p` and `q` from just `N`, `e`, and `d`!
|
||||||
|
|
||||||
### Blinding
|
## 2.2 blinding
|
||||||
|
|
||||||
This attack is actually about RSA _signatures_ (which uses the opposite keys as
|
This attack is actually about RSA _signatures_ (which uses the opposite keys as encryption: private for signing and public for verifying), and shows how you can compute the signature of a message M using the signature of a derived message M'.
|
||||||
encryption: private for signing and public for verifying), and shows how you can
|
|
||||||
compute the signature of a message $M$ using the signature of a derived message
|
|
||||||
$M'$.
|
|
||||||
|
|
||||||
Suppose Marvin wants Bob to sign the following message: `"I (Bob) owes Marvin
|
Suppose Marvin wants Bob to sign the following message: `"I (Bob) owes Marvin $100,000 USD"`. Marvin hands this to Bob saying something like, "I'll just need you to sign this with your private key." Let's generate Bob's private key:
|
||||||
$100,000 USD"`. Marvin hands this to Bob saying something like, "I'll just need
|
|
||||||
you to sign this with your private key." Let's generate Bob's private key:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> from Crypto.Util.number import bytes_to_long, long_to_bytes
|
>>> from Crypto.Util.number import bytes_to_long, long_to_bytes
|
||||||
|
@ -184,10 +131,7 @@ you to sign this with your private key." Let's generate Bob's private key:
|
||||||
>>> M = b"I (Bob) owes Marvin $100,000 USD"
|
>>> M = b"I (Bob) owes Marvin $100,000 USD"
|
||||||
```
|
```
|
||||||
|
|
||||||
Obviously, Bob, an intellectual, will refuse to sign the message. However,
|
Obviously, Bob, an intellectual, will refuse to sign the message. However, suppose Marvin now transforms his message into a more innocent looking one. He does this by turning M into `M' = (r^e)*M mod N` where r is an integer that's coprime to N:
|
||||||
suppose Marvin now transforms his message into a more innocent looking one. He
|
|
||||||
does this by turning $M$ into $M' = r^eM \mod N$ where r is an integer that's
|
|
||||||
coprime to $N$:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> from random import randint
|
>>> from random import randint
|
||||||
|
@ -197,34 +141,21 @@ coprime to $N$:
|
||||||
b'7\x90\xbc\xf9%T\xa9\xee\xf4\xe3?>]\x88\xcd\xb4\xd6D#\xfc\xcb\x0fd\xf0\x8e\xbc>\n\x06\xcd\x0f\x89\x0bp\xa7o\xd6\x02\xa6\xa7\x81\xd8\n\xae\xfb\x08\xaa|\xbd.\xc9E\xf1|\x86\xcaZ\xaa\xd4L\xafaA\x0c}\x84\x04\n\xa4\xa5\x80\xecX<\xe0\xb5\xf6\xfb\xe3\xcc\xd5BD7\xdc\xaep\x7f\xe9vi\xabB\xe2\xadE\xa41K\xc6\xb7\xae\x01\xcb\x04C\xaf\x8b\x17\x83\xffX7z\xb1\xbf\xceF\xafN(x\x00\x9f\xe1kV\xee\x0b\xbd\xc3H\r\xee9\x81\x16\xb2\x10hb.\x90\x08\xe42$Q\x92Ew+\xe1@\xf9\x17%\xce/\xbd\x00\xad\xe2\x12\x01\x93\x8b\xc4\x1bx\xe6H?\x15\xdfPE@\xf9j\xe3\xb7\x9e\xa0\x86\xd1\xd3\xb6[\xf7q\xf1\x95N\xd3>/\x06\x80\xc7\xa3\x8a\xcbDy\xc6v\x01P\x14\xa9Be\xf7~p\xc5\xaa\xac\xa0\xaf\xbe#\xe5\x18\xc6\x1d\xd5\x14\xc1\xbbYXD\x0c\x91{\xc0s\xde]\x18Z\x8bSk\x07k\xb6\x9a\xa5`Iqe~'
|
b'7\x90\xbc\xf9%T\xa9\xee\xf4\xe3?>]\x88\xcd\xb4\xd6D#\xfc\xcb\x0fd\xf0\x8e\xbc>\n\x06\xcd\x0f\x89\x0bp\xa7o\xd6\x02\xa6\xa7\x81\xd8\n\xae\xfb\x08\xaa|\xbd.\xc9E\xf1|\x86\xcaZ\xaa\xd4L\xafaA\x0c}\x84\x04\n\xa4\xa5\x80\xecX<\xe0\xb5\xf6\xfb\xe3\xcc\xd5BD7\xdc\xaep\x7f\xe9vi\xabB\xe2\xadE\xa41K\xc6\xb7\xae\x01\xcb\x04C\xaf\x8b\x17\x83\xffX7z\xb1\xbf\xceF\xafN(x\x00\x9f\xe1kV\xee\x0b\xbd\xc3H\r\xee9\x81\x16\xb2\x10hb.\x90\x08\xe42$Q\x92Ew+\xe1@\xf9\x17%\xce/\xbd\x00\xad\xe2\x12\x01\x93\x8b\xc4\x1bx\xe6H?\x15\xdfPE@\xf9j\xe3\xb7\x9e\xa0\x86\xd1\xd3\xb6[\xf7q\xf1\x95N\xd3>/\x06\x80\xc7\xa3\x8a\xcbDy\xc6v\x01P\x14\xa9Be\xf7~p\xc5\xaa\xac\xa0\xaf\xbe#\xe5\x18\xc6\x1d\xd5\x14\xc1\xbbYXD\x0c\x91{\xc0s\xde]\x18Z\x8bSk\x07k\xb6\x9a\xa5`Iqe~'
|
||||||
```
|
```
|
||||||
|
|
||||||
Now he asks Bob to sign this more... innocently-looking message. Without
|
Now he asks Bob to sign this more... innocently-looking message. Without questioning, Bob, an intellectual, signs his life away. Let's say he produces a signature `S' = (M'^d) = (r^e * M)^d = r^(ed) * M^d = r * M^d mod N`.
|
||||||
questioning, Bob, an intellectual, signs his life away. Let's say he produces a
|
|
||||||
signature
|
|
||||||
|
|
||||||
$$
|
|
||||||
\begin{aligned}
|
|
||||||
S' &= (M'^d) \\\
|
|
||||||
&= (r^e * M)^d \\\
|
|
||||||
&= r^{ed} * M^d \\\
|
|
||||||
&= r * M^d \mod N
|
|
||||||
\end{aligned}
|
|
||||||
$$
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> Sp, = bob.sign(Mp, 0)
|
>>> Sp, = bob.sign(Mp, 0)
|
||||||
4222298342813922437811434251340999736739055616654488323193778229765071846717137952694561809398626068283668428796351354154566771597532278827070832905206221261994843265685464173739776886856384806238418884247949451413559988796455422271296883338455956330421559319009950760931899199217936823999874162064553735563087382870564193673989865778229832918474778963380170967676966373703157629615331081637805594392084045827925764529711433584853942576464491576212176547485726609891593617931393545058401472883178443786988683045423150809606471425615670582973274971087459634959553685559458456237617436410759134193279063427911112115134
|
4222298342813922437811434251340999736739055616654488323193778229765071846717137952694561809398626068283668428796351354154566771597532278827070832905206221261994843265685464173739776886856384806238418884247949451413559988796455422271296883338455956330421559319009950760931899199217936823999874162064553735563087382870564193673989865778229832918474778963380170967676966373703157629615331081637805594392084045827925764529711433584853942576464491576212176547485726609891593617931393545058401472883178443786988683045423150809606471425615670582973274971087459634959553685559458456237617436410759134193279063427911112115134
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, all Marvin has to do is multiply by the modular inverse of $r$, to obtain
|
Now, all Marvin has to do is multiply by the modular inverse of r, to obtain `M^d`, the signature of the original message:
|
||||||
$M^d$, the signature of the original message:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> S = (Sp * modinv(r, N)) % N
|
>>> S = (Sp * modinv(r, N)) % N
|
||||||
6137678992536399703654836416525985142902780822513172949427421060785532284955531529418529725602418902796840570634560123808769013384654624916503940938715718120521434666716675795201896105310462331838807171312705686415521871046533303776516500490921892398440988515777575520183847518597482163414665355222659603386541869176930658730416118799866012276767364050134126722746224706026850062367243018313483359694686773566231956425606553198607719740067340776177716443517567144901614253170719278035838849363127850910135864099535083004590180745762100334268408681888925040382341592080592207557742366581814701422371311084081150092871
|
6137678992536399703654836416525985142902780822513172949427421060785532284955531529418529725602418902796840570634560123808769013384654624916503940938715718120521434666716675795201896105310462331838807171312705686415521871046533303776516500490921892398440988515777575520183847518597482163414665355222659603386541869176930658730416118799866012276767364050134126722746224706026850062367243018313483359694686773566231956425606553198607719740067340776177716443517567144901614253170719278035838849363127850910135864099535083004590180745762100334268408681888925040382341592080592207557742366581814701422371311084081150092871
|
||||||
```
|
```
|
||||||
|
|
||||||
Sure enough, if you try to verify the "original" signature against the original
|
Sure enough, if you try to verify the "original" signature against the original message, it checks out.
|
||||||
message, it checks out.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
>>> bob.verify(M, (S,))
|
>>> bob.verify(M, (S,))
|
|
@ -1,13 +1,12 @@
|
||||||
---
|
+++
|
||||||
title: "Magic forms with proc macros: Ideas"
|
title = "magic forms with proc macros: ideas"
|
||||||
date: 2019-02-01
|
date = 2019-02-01
|
||||||
tags: ["computers", "web"]
|
|
||||||
languages: ["rust"]
|
|
||||||
---
|
|
||||||
|
|
||||||
Procedural macros (proc macros for short) in Rust are incredible because they allow arbitrary pre-compile source transformation, which leads to endless possibilities (and hazards!). But if we take careful advantage of this feature, we can use it to make clean abstractions for messy boilerplate, especially in the case of web forms. <!--more-->
|
[taxonomies]
|
||||||
|
tags = ["computers", "rust", "web-dev", "macros"]
|
||||||
|
+++
|
||||||
|
|
||||||
In fact, proc macros are incredibly pervasive around Rust's ecosystem. For example, using the [`serde`][1] serialization/deserialization crate, you can simply write:
|
Procedural macros (proc macros for short) in Rust are incredible because they allow pre-compile source transformation. Many of the greatest abstractions in Rust take advantage of this feature. For example, you can
|
||||||
|
|
||||||
```rs
|
```rs
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -16,13 +15,11 @@ struct Foo {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
and code will be generated to serialize and deserialize to a multitude of formats including JSON, YAML, CBOR, etc.
|
|
||||||
|
|
||||||
It occurred to me that this feature can also be useful for generating code for rendering and validating forms (as in a place where you fill out info). **wtforms** is one of the nicest Python packages for handling form behavior in web applications, and with the power of proc macros, this functionality can be easily achieved in Rust as well.
|
It occurred to me that this feature can also be useful for generating code for rendering and validating forms (as in a place where you fill out info). **wtforms** is one of the nicest Python packages for handling form behavior in web applications, and with the power of proc macros, this functionality can be easily achieved in Rust as well.
|
||||||
|
|
||||||
In this post I'm going to outline some of the ideas I have for a wtforms-ish library for handling forms in Rust.
|
In this post I'm going to outline some of the ideas I have for a wtforms-ish library for handling forms in Rust.
|
||||||
|
|
||||||
## Code generation
|
## code generation
|
||||||
|
|
||||||
Ideally, we should be able to use this library like this:
|
Ideally, we should be able to use this library like this:
|
||||||
|
|
||||||
|
@ -42,10 +39,10 @@ What this would do is add a couple more functions to our form class. Firstly, I'
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<form>
|
<form>
|
||||||
<input type="email" name="id" />
|
<input type="email" name="id" />
|
||||||
<input type="text" name="name" />
|
<input type="text" name="name" />
|
||||||
<input type="password" name="pass" />
|
<input type="password" name="pass" />
|
||||||
<input type="submit" />
|
<input type="submit" />
|
||||||
</form>
|
</form>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -60,12 +57,12 @@ name: String,
|
||||||
This should generate the following HTML:
|
This should generate the following HTML:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<input type="text" name="name" autocomplete="off" />
|
<input type="text" name="name" autocomplete=off />
|
||||||
```
|
```
|
||||||
|
|
||||||
I realize this is probably not very flexible, since you'd really only be able to use this form in a specific context. But in reality, how much do you really lose by redefining that form?
|
I realize this is probably not very flexible, since you'd really only be able to use this form in a specific context. But in reality, how much do you really lose by redefining that form?
|
||||||
|
|
||||||
## Validation
|
## validation
|
||||||
|
|
||||||
You've already seen the `validators` attribute used above. This defines a set of validators that we'd like to verify the form against. Suppose you receive an instance of the form that looks like (in pseudo-y Rust):
|
You've already seen the `validators` attribute used above. This defines a set of validators that we'd like to verify the form against. Suppose you receive an instance of the form that looks like (in pseudo-y Rust):
|
||||||
|
|
||||||
|
@ -79,12 +76,10 @@ let instance = RegisterForm {
|
||||||
|
|
||||||
then calling something like `instance.verify()` should run all those validators we've defined on the fields and return a list of errors that go along with each of the fields. For this instance, for example, we should at least get an error that states that the password provided was way too short.
|
then calling something like `instance.verify()` should run all those validators we've defined on the fields and return a list of errors that go along with each of the fields. For this instance, for example, we should at least get an error that states that the password provided was way too short.
|
||||||
|
|
||||||
## Other interesting features
|
## other interesting features
|
||||||
|
|
||||||
- If a form fails during validation, the user is presented with the errors and a chance to retry the form. At this point, the HTML generated should fill in the values for the fields that passed the validation so the user doesn't have to fill it out again. You see this behavior on web forms sometimes.
|
- If a form fails during validation, the user is presented with the errors and a chance to retry the form. At this point, the HTML generated should fill in the values for the fields that passed the validation so the user doesn't have to fill it out again. You see this behavior on web forms sometimes.
|
||||||
|
|
||||||
## Conclusion
|
## conclusion
|
||||||
|
|
||||||
This project is a work in progress! You can see how far I am [on Github](https://github.com/mzhang28/wtforms).
|
This project is a work in progress! You can see how far I am [on Github](https://github.com/iptq/wtforms).
|
||||||
|
|
||||||
[1]: https://docs.rs/serde
|
|
|
@ -1,10 +1,12 @@
|
||||||
---
|
+++
|
||||||
title: "Accept server analogy"
|
title = "accept server analogy"
|
||||||
date: 2019-03-04
|
date = 2019-03-04
|
||||||
tags: ["computers"]
|
|
||||||
---
|
|
||||||
|
|
||||||
This is just a stupid analogy I thought of recently, but decided to write about it anyway.
|
[taxonomies]
|
||||||
|
tags = ["computers"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
This is just something stupid I thought of recently, but decided to write about it anyway.
|
||||||
|
|
||||||
If you think about it, a server waiting for clients is kind of like the host at the front of a restaurant leading guests to tables. They don't actually take orders or serve food, they just stand at the front and wait for new guests to arrive. Then there's another waiter that's specifically assigned to take that table's orders.
|
If you think about it, a server waiting for clients is kind of like the host at the front of a restaurant leading guests to tables. They don't actually take orders or serve food, they just stand at the front and wait for new guests to arrive. Then there's another waiter that's specifically assigned to take that table's orders.
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
---
|
+++
|
||||||
title: "Password managers"
|
title = "password managers"
|
||||||
date: 2020-04-01
|
date = 2020-04-01
|
||||||
tags: ["computers", "things-that-are-good", "privacy"]
|
|
||||||
---
|
[taxonomies]
|
||||||
|
tags = ["computers", "things-that-are-good", "privacy"]
|
||||||
|
+++
|
||||||
|
|
||||||
Password managers are programs that store passwords for you. With the number of accounts you keep on the web, you generally don't want to store all of them in your head. If you want to see articles on why you should use a password manager NOW, search "reasons to use a password manager" online and any of the articles you find should explain it. Here I'll add some more commentary on top of the traditional arguments.
|
Password managers are programs that store passwords for you. With the number of accounts you keep on the web, you generally don't want to store all of them in your head. If you want to see articles on why you should use a password manager NOW, search "reasons to use a password manager" online and any of the articles you find should explain it. Here I'll add some more commentary on top of the traditional arguments.
|
||||||
|
|
||||||
<!-- more -->
|
<!-- more -->
|
||||||
|
|
||||||
## Don't tick the "Remember master password box" no matter what
|
Don't tick the "Remember master password box" no matter what
|
||||||
|
---
|
||||||
|
|
||||||
How well you remember a password depends on how much you use it. If you open an account, make a password, and stay signed in for a year without ever having to re-login, you'll naturally forget the password. Same deal with password managers; the problem has just been moved another step.
|
How well you remember a password depends on how much you use it. If you open an account, make a password, and stay signed in for a year without ever having to re-login, you'll naturally forget the password. Same deal with password managers; the problem has just been moved another step.
|
||||||
|
|
||||||
The power of a password manager comes from you continually entering in the same password over and over in order to unlock your other accounts.
|
The power of a password manager comes from you continually entering in the same password over and over in order to unlock your other accounts.
|
||||||
|
|
||||||
## Password managers are good for a lot more than passwords
|
Password managers are good for a lot more than passwords
|
||||||
|
---
|
||||||
|
|
||||||
If you're willing to put sensitive passwords into your password manager, it should be a perfect place to put information that you'd want to avoid writing down in plaintext but want to access easily. This might include:
|
If you're willing to put sensitive passwords into your password manager, it should be a perfect place to put information that you'd want to avoid writing down in plaintext but want to access easily. This might include:
|
||||||
|
|
||||||
|
@ -23,7 +27,8 @@ If you're willing to put sensitive passwords into your password manager, it shou
|
||||||
- Your car's license plate number
|
- Your car's license plate number
|
||||||
- Answers to security questions, which leads into the next point:
|
- Answers to security questions, which leads into the next point:
|
||||||
|
|
||||||
## Treat your security questions as passwords
|
Treat your security questions as passwords
|
||||||
|
---
|
||||||
|
|
||||||
Save these in your password manager! "Security" questions are probably the worst idea for security and are more likely to weaken the security of your account than strengthen it. They have multiple fatal flaws (assuming you use security questions truthfully):
|
Save these in your password manager! "Security" questions are probably the worst idea for security and are more likely to weaken the security of your account than strengthen it. They have multiple fatal flaws (assuming you use security questions truthfully):
|
||||||
|
|
||||||
|
@ -33,7 +38,8 @@ Save these in your password manager! "Security" questions are probably the worst
|
||||||
|
|
||||||
Instead, just treat them as another password! Go into your password manager, generate the longest possible random password that fits into the box, and save it. Since you can give a name to the password, there's no worry of forgetting it or losing it, since it'll be stored among the vault of other passwords that you're hopefully using every day.
|
Instead, just treat them as another password! Go into your password manager, generate the longest possible random password that fits into the box, and save it. Since you can give a name to the password, there's no worry of forgetting it or losing it, since it'll be stored among the vault of other passwords that you're hopefully using every day.
|
||||||
|
|
||||||
## Don't trust extensions that fill in your password automatically
|
Don't trust extensions that fill in your password automatically
|
||||||
|
---
|
||||||
|
|
||||||
Some password managers, like LastPass, have browser extensions that automatically fill in password boxes when you open the page.
|
Some password managers, like LastPass, have browser extensions that automatically fill in password boxes when you open the page.
|
||||||
|
|
8
content/2020-04-12-drawing-bezier-curves.md
Normal file
8
content/2020-04-12-drawing-bezier-curves.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
+++
|
||||||
|
title = "drawing bezier curves"
|
||||||
|
date = 2020-04-12
|
||||||
|
draft = true
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["osu", "graphics"]
|
||||||
|
+++
|
11
content/_index.md
Normal file
11
content/_index.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
+++
|
||||||
|
title = "home"
|
||||||
|
template = "blog.html"
|
||||||
|
page_template = "post.html"
|
||||||
|
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
sort_by = "date"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
include_posts = true
|
||||||
|
+++
|
639
content/enterprise/2020-02-11-prototype/helloworld.js
Normal file
639
content/enterprise/2020-02-11-prototype/helloworld.js
Normal file
|
@ -0,0 +1,639 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
if( typeof Rust === "undefined" ) {
|
||||||
|
var Rust = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
(function( root, factory ) {
|
||||||
|
if( typeof define === "function" && define.amd ) {
|
||||||
|
define( [], factory );
|
||||||
|
} else if( typeof module === "object" && module.exports ) {
|
||||||
|
module.exports = factory();
|
||||||
|
} else {
|
||||||
|
Rust.helloworld = factory();
|
||||||
|
}
|
||||||
|
}( this, function() {
|
||||||
|
return (function( module_factory ) {
|
||||||
|
var instance = module_factory();
|
||||||
|
|
||||||
|
if( typeof process === "object" && typeof process.versions === "object" && typeof process.versions.node === "string" ) {
|
||||||
|
var fs = require( "fs" );
|
||||||
|
var path = require( "path" );
|
||||||
|
var wasm_path = path.join( __dirname, "helloworld.wasm" );
|
||||||
|
var buffer = fs.readFileSync( wasm_path );
|
||||||
|
var mod = new WebAssembly.Module( buffer );
|
||||||
|
var wasm_instance = new WebAssembly.Instance( mod, instance.imports );
|
||||||
|
return instance.initialize( wasm_instance );
|
||||||
|
} else {
|
||||||
|
var file = fetch( "helloworld.wasm", {credentials: "same-origin"} );
|
||||||
|
|
||||||
|
var wasm_instance = ( typeof WebAssembly.instantiateStreaming === "function"
|
||||||
|
? WebAssembly.instantiateStreaming( file, instance.imports )
|
||||||
|
.then( function( result ) { return result.instance; } )
|
||||||
|
|
||||||
|
: file
|
||||||
|
.then( function( response ) { return response.arrayBuffer(); } )
|
||||||
|
.then( function( bytes ) { return WebAssembly.compile( bytes ); } )
|
||||||
|
.then( function( mod ) { return WebAssembly.instantiate( mod, instance.imports ) } ) );
|
||||||
|
|
||||||
|
return wasm_instance
|
||||||
|
.then( function( wasm_instance ) {
|
||||||
|
var exports = instance.initialize( wasm_instance );
|
||||||
|
console.log( "Finished loading Rust wasm module 'helloworld'" );
|
||||||
|
return exports;
|
||||||
|
})
|
||||||
|
.catch( function( error ) {
|
||||||
|
console.log( "Error loading Rust wasm module 'helloworld':", error );
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}( function() {
|
||||||
|
var Module = {};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE = {};
|
||||||
|
|
||||||
|
// This is based on code from Emscripten's preamble.js.
|
||||||
|
Module.STDWEB_PRIVATE.to_utf8 = function to_utf8( str, addr ) {
|
||||||
|
var HEAPU8 = Module.HEAPU8;
|
||||||
|
for( var i = 0; i < str.length; ++i ) {
|
||||||
|
// Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8.
|
||||||
|
// See http://unicode.org/faq/utf_bom.html#utf16-3
|
||||||
|
// For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description and https://www.ietf.org/rfc/rfc2279.txt and https://tools.ietf.org/html/rfc3629
|
||||||
|
var u = str.charCodeAt( i ); // possibly a lead surrogate
|
||||||
|
if( u >= 0xD800 && u <= 0xDFFF ) {
|
||||||
|
u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt( ++i ) & 0x3FF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if( u <= 0x7F ) {
|
||||||
|
HEAPU8[ addr++ ] = u;
|
||||||
|
} else if( u <= 0x7FF ) {
|
||||||
|
HEAPU8[ addr++ ] = 0xC0 | (u >> 6);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | (u & 63);
|
||||||
|
} else if( u <= 0xFFFF ) {
|
||||||
|
HEAPU8[ addr++ ] = 0xE0 | (u >> 12);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 6) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | (u & 63);
|
||||||
|
} else if( u <= 0x1FFFFF ) {
|
||||||
|
HEAPU8[ addr++ ] = 0xF0 | (u >> 18);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 12) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 6) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | (u & 63);
|
||||||
|
} else if( u <= 0x3FFFFFF ) {
|
||||||
|
HEAPU8[ addr++ ] = 0xF8 | (u >> 24);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 18) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 12) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 6) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | (u & 63);
|
||||||
|
} else {
|
||||||
|
HEAPU8[ addr++ ] = 0xFC | (u >> 30);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 24) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 18) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 12) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | ((u >> 6) & 63);
|
||||||
|
HEAPU8[ addr++ ] = 0x80 | (u & 63);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.noop = function() {};
|
||||||
|
Module.STDWEB_PRIVATE.to_js = function to_js( address ) {
|
||||||
|
var kind = Module.HEAPU8[ address + 12 ];
|
||||||
|
if( kind === 0 ) {
|
||||||
|
return undefined;
|
||||||
|
} else if( kind === 1 ) {
|
||||||
|
return null;
|
||||||
|
} else if( kind === 2 ) {
|
||||||
|
return Module.HEAP32[ address / 4 ];
|
||||||
|
} else if( kind === 3 ) {
|
||||||
|
return Module.HEAPF64[ address / 8 ];
|
||||||
|
} else if( kind === 4 ) {
|
||||||
|
var pointer = Module.HEAPU32[ address / 4 ];
|
||||||
|
var length = Module.HEAPU32[ (address + 4) / 4 ];
|
||||||
|
return Module.STDWEB_PRIVATE.to_js_string( pointer, length );
|
||||||
|
} else if( kind === 5 ) {
|
||||||
|
return false;
|
||||||
|
} else if( kind === 6 ) {
|
||||||
|
return true;
|
||||||
|
} else if( kind === 7 ) {
|
||||||
|
var pointer = Module.STDWEB_PRIVATE.arena + Module.HEAPU32[ address / 4 ];
|
||||||
|
var length = Module.HEAPU32[ (address + 4) / 4 ];
|
||||||
|
var output = [];
|
||||||
|
for( var i = 0; i < length; ++i ) {
|
||||||
|
output.push( Module.STDWEB_PRIVATE.to_js( pointer + i * 16 ) );
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
} else if( kind === 8 ) {
|
||||||
|
var arena = Module.STDWEB_PRIVATE.arena;
|
||||||
|
var value_array_pointer = arena + Module.HEAPU32[ address / 4 ];
|
||||||
|
var length = Module.HEAPU32[ (address + 4) / 4 ];
|
||||||
|
var key_array_pointer = arena + Module.HEAPU32[ (address + 8) / 4 ];
|
||||||
|
var output = {};
|
||||||
|
for( var i = 0; i < length; ++i ) {
|
||||||
|
var key_pointer = Module.HEAPU32[ (key_array_pointer + i * 8) / 4 ];
|
||||||
|
var key_length = Module.HEAPU32[ (key_array_pointer + 4 + i * 8) / 4 ];
|
||||||
|
var key = Module.STDWEB_PRIVATE.to_js_string( key_pointer, key_length );
|
||||||
|
var value = Module.STDWEB_PRIVATE.to_js( value_array_pointer + i * 16 );
|
||||||
|
output[ key ] = value;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
} else if( kind === 9 ) {
|
||||||
|
return Module.STDWEB_PRIVATE.acquire_js_reference( Module.HEAP32[ address / 4 ] );
|
||||||
|
} else if( kind === 10 || kind === 12 || kind === 13 ) {
|
||||||
|
var adapter_pointer = Module.HEAPU32[ address / 4 ];
|
||||||
|
var pointer = Module.HEAPU32[ (address + 4) / 4 ];
|
||||||
|
var deallocator_pointer = Module.HEAPU32[ (address + 8) / 4 ];
|
||||||
|
var num_ongoing_calls = 0;
|
||||||
|
var drop_queued = false;
|
||||||
|
var output = function() {
|
||||||
|
if( pointer === 0 || drop_queued === true ) {
|
||||||
|
if (kind === 10) {
|
||||||
|
throw new ReferenceError( "Already dropped Rust function called!" );
|
||||||
|
} else if (kind === 12) {
|
||||||
|
throw new ReferenceError( "Already dropped FnMut function called!" );
|
||||||
|
} else {
|
||||||
|
throw new ReferenceError( "Already called or dropped FnOnce function called!" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var function_pointer = pointer;
|
||||||
|
if (kind === 13) {
|
||||||
|
output.drop = Module.STDWEB_PRIVATE.noop;
|
||||||
|
pointer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num_ongoing_calls !== 0) {
|
||||||
|
if (kind === 12 || kind === 13) {
|
||||||
|
throw new ReferenceError( "FnMut function called multiple times concurrently!" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = Module.STDWEB_PRIVATE.alloc( 16 );
|
||||||
|
Module.STDWEB_PRIVATE.serialize_array( args, arguments );
|
||||||
|
|
||||||
|
try {
|
||||||
|
num_ongoing_calls += 1;
|
||||||
|
Module.STDWEB_PRIVATE.dyncall( "vii", adapter_pointer, [function_pointer, args] );
|
||||||
|
var result = Module.STDWEB_PRIVATE.tmp;
|
||||||
|
Module.STDWEB_PRIVATE.tmp = null;
|
||||||
|
} finally {
|
||||||
|
num_ongoing_calls -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( drop_queued === true && num_ongoing_calls === 0 ) {
|
||||||
|
output.drop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
output.drop = function() {
|
||||||
|
if (num_ongoing_calls !== 0) {
|
||||||
|
drop_queued = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.drop = Module.STDWEB_PRIVATE.noop;
|
||||||
|
var function_pointer = pointer;
|
||||||
|
pointer = 0;
|
||||||
|
|
||||||
|
if (function_pointer != 0) {
|
||||||
|
Module.STDWEB_PRIVATE.dyncall( "vi", deallocator_pointer, [function_pointer] );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return output;
|
||||||
|
} else if( kind === 14 ) {
|
||||||
|
var pointer = Module.HEAPU32[ address / 4 ];
|
||||||
|
var length = Module.HEAPU32[ (address + 4) / 4 ];
|
||||||
|
var array_kind = Module.HEAPU32[ (address + 8) / 4 ];
|
||||||
|
var pointer_end = pointer + length;
|
||||||
|
|
||||||
|
switch( array_kind ) {
|
||||||
|
case 0:
|
||||||
|
return Module.HEAPU8.subarray( pointer, pointer_end );
|
||||||
|
case 1:
|
||||||
|
return Module.HEAP8.subarray( pointer, pointer_end );
|
||||||
|
case 2:
|
||||||
|
return Module.HEAPU16.subarray( pointer, pointer_end );
|
||||||
|
case 3:
|
||||||
|
return Module.HEAP16.subarray( pointer, pointer_end );
|
||||||
|
case 4:
|
||||||
|
return Module.HEAPU32.subarray( pointer, pointer_end );
|
||||||
|
case 5:
|
||||||
|
return Module.HEAP32.subarray( pointer, pointer_end );
|
||||||
|
case 6:
|
||||||
|
return Module.HEAPF32.subarray( pointer, pointer_end );
|
||||||
|
case 7:
|
||||||
|
return Module.HEAPF64.subarray( pointer, pointer_end );
|
||||||
|
}
|
||||||
|
} else if( kind === 15 ) {
|
||||||
|
return Module.STDWEB_PRIVATE.get_raw_value( Module.HEAPU32[ address / 4 ] );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.serialize_object = function serialize_object( address, value ) {
|
||||||
|
var keys = Object.keys( value );
|
||||||
|
var length = keys.length;
|
||||||
|
var key_array_pointer = Module.STDWEB_PRIVATE.alloc( length * 8 );
|
||||||
|
var value_array_pointer = Module.STDWEB_PRIVATE.alloc( length * 16 );
|
||||||
|
Module.HEAPU8[ address + 12 ] = 8;
|
||||||
|
Module.HEAPU32[ address / 4 ] = value_array_pointer;
|
||||||
|
Module.HEAPU32[ (address + 4) / 4 ] = length;
|
||||||
|
Module.HEAPU32[ (address + 8) / 4 ] = key_array_pointer;
|
||||||
|
for( var i = 0; i < length; ++i ) {
|
||||||
|
var key = keys[ i ];
|
||||||
|
var key_address = key_array_pointer + i * 8;
|
||||||
|
Module.STDWEB_PRIVATE.to_utf8_string( key_address, key );
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.from_js( value_array_pointer + i * 16, value[ key ] );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.serialize_array = function serialize_array( address, value ) {
|
||||||
|
var length = value.length;
|
||||||
|
var pointer = Module.STDWEB_PRIVATE.alloc( length * 16 );
|
||||||
|
Module.HEAPU8[ address + 12 ] = 7;
|
||||||
|
Module.HEAPU32[ address / 4 ] = pointer;
|
||||||
|
Module.HEAPU32[ (address + 4) / 4 ] = length;
|
||||||
|
for( var i = 0; i < length; ++i ) {
|
||||||
|
Module.STDWEB_PRIVATE.from_js( pointer + i * 16, value[ i ] );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// New browsers and recent Node
|
||||||
|
var cachedEncoder = ( typeof TextEncoder === "function"
|
||||||
|
? new TextEncoder( "utf-8" )
|
||||||
|
// Old Node (before v11)
|
||||||
|
: ( typeof util === "object" && util && typeof util.TextEncoder === "function"
|
||||||
|
? new util.TextEncoder( "utf-8" )
|
||||||
|
// Old browsers
|
||||||
|
: null ) );
|
||||||
|
|
||||||
|
if ( cachedEncoder != null ) {
|
||||||
|
Module.STDWEB_PRIVATE.to_utf8_string = function to_utf8_string( address, value ) {
|
||||||
|
var buffer = cachedEncoder.encode( value );
|
||||||
|
var length = buffer.length;
|
||||||
|
var pointer = 0;
|
||||||
|
|
||||||
|
if ( length > 0 ) {
|
||||||
|
pointer = Module.STDWEB_PRIVATE.alloc( length );
|
||||||
|
Module.HEAPU8.set( buffer, pointer );
|
||||||
|
}
|
||||||
|
|
||||||
|
Module.HEAPU32[ address / 4 ] = pointer;
|
||||||
|
Module.HEAPU32[ (address + 4) / 4 ] = length;
|
||||||
|
};
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Module.STDWEB_PRIVATE.to_utf8_string = function to_utf8_string( address, value ) {
|
||||||
|
var length = Module.STDWEB_PRIVATE.utf8_len( value );
|
||||||
|
var pointer = 0;
|
||||||
|
|
||||||
|
if ( length > 0 ) {
|
||||||
|
pointer = Module.STDWEB_PRIVATE.alloc( length );
|
||||||
|
Module.STDWEB_PRIVATE.to_utf8( value, pointer );
|
||||||
|
}
|
||||||
|
|
||||||
|
Module.HEAPU32[ address / 4 ] = pointer;
|
||||||
|
Module.HEAPU32[ (address + 4) / 4 ] = length;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.from_js = function from_js( address, value ) {
|
||||||
|
var kind = Object.prototype.toString.call( value );
|
||||||
|
if( kind === "[object String]" ) {
|
||||||
|
Module.HEAPU8[ address + 12 ] = 4;
|
||||||
|
Module.STDWEB_PRIVATE.to_utf8_string( address, value );
|
||||||
|
} else if( kind === "[object Number]" ) {
|
||||||
|
if( value === (value|0) ) {
|
||||||
|
Module.HEAPU8[ address + 12 ] = 2;
|
||||||
|
Module.HEAP32[ address / 4 ] = value;
|
||||||
|
} else {
|
||||||
|
Module.HEAPU8[ address + 12 ] = 3;
|
||||||
|
Module.HEAPF64[ address / 8 ] = value;
|
||||||
|
}
|
||||||
|
} else if( value === null ) {
|
||||||
|
Module.HEAPU8[ address + 12 ] = 1;
|
||||||
|
} else if( value === undefined ) {
|
||||||
|
Module.HEAPU8[ address + 12 ] = 0;
|
||||||
|
} else if( value === false ) {
|
||||||
|
Module.HEAPU8[ address + 12 ] = 5;
|
||||||
|
} else if( value === true ) {
|
||||||
|
Module.HEAPU8[ address + 12 ] = 6;
|
||||||
|
} else if( kind === "[object Symbol]" ) {
|
||||||
|
var id = Module.STDWEB_PRIVATE.register_raw_value( value );
|
||||||
|
Module.HEAPU8[ address + 12 ] = 15;
|
||||||
|
Module.HEAP32[ address / 4 ] = id;
|
||||||
|
} else {
|
||||||
|
var refid = Module.STDWEB_PRIVATE.acquire_rust_reference( value );
|
||||||
|
Module.HEAPU8[ address + 12 ] = 9;
|
||||||
|
Module.HEAP32[ address / 4 ] = refid;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// New browsers and recent Node
|
||||||
|
var cachedDecoder = ( typeof TextDecoder === "function"
|
||||||
|
? new TextDecoder( "utf-8" )
|
||||||
|
// Old Node (before v11)
|
||||||
|
: ( typeof util === "object" && util && typeof util.TextDecoder === "function"
|
||||||
|
? new util.TextDecoder( "utf-8" )
|
||||||
|
// Old browsers
|
||||||
|
: null ) );
|
||||||
|
|
||||||
|
if ( cachedDecoder != null ) {
|
||||||
|
Module.STDWEB_PRIVATE.to_js_string = function to_js_string( index, length ) {
|
||||||
|
return cachedDecoder.decode( Module.HEAPU8.subarray( index, index + length ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// This is ported from Rust's stdlib; it's faster than
|
||||||
|
// the string conversion from Emscripten.
|
||||||
|
Module.STDWEB_PRIVATE.to_js_string = function to_js_string( index, length ) {
|
||||||
|
var HEAPU8 = Module.HEAPU8;
|
||||||
|
index = index|0;
|
||||||
|
length = length|0;
|
||||||
|
var end = (index|0) + (length|0);
|
||||||
|
var output = "";
|
||||||
|
while( index < end ) {
|
||||||
|
var x = HEAPU8[ index++ ];
|
||||||
|
if( x < 128 ) {
|
||||||
|
output += String.fromCharCode( x );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var init = (x & (0x7F >> 2));
|
||||||
|
var y = 0;
|
||||||
|
if( index < end ) {
|
||||||
|
y = HEAPU8[ index++ ];
|
||||||
|
}
|
||||||
|
var ch = (init << 6) | (y & 63);
|
||||||
|
if( x >= 0xE0 ) {
|
||||||
|
var z = 0;
|
||||||
|
if( index < end ) {
|
||||||
|
z = HEAPU8[ index++ ];
|
||||||
|
}
|
||||||
|
var y_z = ((y & 63) << 6) | (z & 63);
|
||||||
|
ch = init << 12 | y_z;
|
||||||
|
if( x >= 0xF0 ) {
|
||||||
|
var w = 0;
|
||||||
|
if( index < end ) {
|
||||||
|
w = HEAPU8[ index++ ];
|
||||||
|
}
|
||||||
|
ch = (init & 7) << 18 | ((y_z << 6) | (w & 63));
|
||||||
|
|
||||||
|
output += String.fromCharCode( 0xD7C0 + (ch >> 10) );
|
||||||
|
ch = 0xDC00 + (ch & 0x3FF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += String.fromCharCode( ch );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.id_to_ref_map = {};
|
||||||
|
Module.STDWEB_PRIVATE.id_to_refcount_map = {};
|
||||||
|
Module.STDWEB_PRIVATE.ref_to_id_map = new WeakMap();
|
||||||
|
// Not all types can be stored in a WeakMap
|
||||||
|
Module.STDWEB_PRIVATE.ref_to_id_map_fallback = new Map();
|
||||||
|
Module.STDWEB_PRIVATE.last_refid = 1;
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.id_to_raw_value_map = {};
|
||||||
|
Module.STDWEB_PRIVATE.last_raw_value_id = 1;
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.acquire_rust_reference = function( reference ) {
|
||||||
|
if( reference === undefined || reference === null ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id_to_refcount_map = Module.STDWEB_PRIVATE.id_to_refcount_map;
|
||||||
|
var id_to_ref_map = Module.STDWEB_PRIVATE.id_to_ref_map;
|
||||||
|
var ref_to_id_map = Module.STDWEB_PRIVATE.ref_to_id_map;
|
||||||
|
var ref_to_id_map_fallback = Module.STDWEB_PRIVATE.ref_to_id_map_fallback;
|
||||||
|
|
||||||
|
var refid = ref_to_id_map.get( reference );
|
||||||
|
if( refid === undefined ) {
|
||||||
|
refid = ref_to_id_map_fallback.get( reference );
|
||||||
|
}
|
||||||
|
if( refid === undefined ) {
|
||||||
|
refid = Module.STDWEB_PRIVATE.last_refid++;
|
||||||
|
try {
|
||||||
|
ref_to_id_map.set( reference, refid );
|
||||||
|
} catch (e) {
|
||||||
|
ref_to_id_map_fallback.set( reference, refid );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( refid in id_to_ref_map ) {
|
||||||
|
id_to_refcount_map[ refid ]++;
|
||||||
|
} else {
|
||||||
|
id_to_ref_map[ refid ] = reference;
|
||||||
|
id_to_refcount_map[ refid ] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return refid;
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.acquire_js_reference = function( refid ) {
|
||||||
|
return Module.STDWEB_PRIVATE.id_to_ref_map[ refid ];
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.increment_refcount = function( refid ) {
|
||||||
|
Module.STDWEB_PRIVATE.id_to_refcount_map[ refid ]++;
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.decrement_refcount = function( refid ) {
|
||||||
|
var id_to_refcount_map = Module.STDWEB_PRIVATE.id_to_refcount_map;
|
||||||
|
if( 0 == --id_to_refcount_map[ refid ] ) {
|
||||||
|
var id_to_ref_map = Module.STDWEB_PRIVATE.id_to_ref_map;
|
||||||
|
var ref_to_id_map_fallback = Module.STDWEB_PRIVATE.ref_to_id_map_fallback;
|
||||||
|
var reference = id_to_ref_map[ refid ];
|
||||||
|
delete id_to_ref_map[ refid ];
|
||||||
|
delete id_to_refcount_map[ refid ];
|
||||||
|
ref_to_id_map_fallback.delete(reference);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.register_raw_value = function( value ) {
|
||||||
|
var id = Module.STDWEB_PRIVATE.last_raw_value_id++;
|
||||||
|
Module.STDWEB_PRIVATE.id_to_raw_value_map[ id ] = value;
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.unregister_raw_value = function( id ) {
|
||||||
|
delete Module.STDWEB_PRIVATE.id_to_raw_value_map[ id ];
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.get_raw_value = function( id ) {
|
||||||
|
return Module.STDWEB_PRIVATE.id_to_raw_value_map[ id ];
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.alloc = function alloc( size ) {
|
||||||
|
return Module.web_malloc( size );
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.dyncall = function( signature, ptr, args ) {
|
||||||
|
return Module.web_table.get( ptr ).apply( null, args );
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is based on code from Emscripten's preamble.js.
|
||||||
|
Module.STDWEB_PRIVATE.utf8_len = function utf8_len( str ) {
|
||||||
|
var len = 0;
|
||||||
|
for( var i = 0; i < str.length; ++i ) {
|
||||||
|
// Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! So decode UTF16->UTF32->UTF8.
|
||||||
|
// See http://unicode.org/faq/utf_bom.html#utf16-3
|
||||||
|
var u = str.charCodeAt( i ); // possibly a lead surrogate
|
||||||
|
if( u >= 0xD800 && u <= 0xDFFF ) {
|
||||||
|
u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt( ++i ) & 0x3FF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if( u <= 0x7F ) {
|
||||||
|
++len;
|
||||||
|
} else if( u <= 0x7FF ) {
|
||||||
|
len += 2;
|
||||||
|
} else if( u <= 0xFFFF ) {
|
||||||
|
len += 3;
|
||||||
|
} else if( u <= 0x1FFFFF ) {
|
||||||
|
len += 4;
|
||||||
|
} else if( u <= 0x3FFFFFF ) {
|
||||||
|
len += 5;
|
||||||
|
} else {
|
||||||
|
len += 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.prepare_any_arg = function( value ) {
|
||||||
|
var arg = Module.STDWEB_PRIVATE.alloc( 16 );
|
||||||
|
Module.STDWEB_PRIVATE.from_js( arg, value );
|
||||||
|
return arg;
|
||||||
|
};
|
||||||
|
|
||||||
|
Module.STDWEB_PRIVATE.acquire_tmp = function( dummy ) {
|
||||||
|
var value = Module.STDWEB_PRIVATE.tmp;
|
||||||
|
Module.STDWEB_PRIVATE.tmp = null;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var HEAP8 = null;
|
||||||
|
var HEAP16 = null;
|
||||||
|
var HEAP32 = null;
|
||||||
|
var HEAPU8 = null;
|
||||||
|
var HEAPU16 = null;
|
||||||
|
var HEAPU32 = null;
|
||||||
|
var HEAPF32 = null;
|
||||||
|
var HEAPF64 = null;
|
||||||
|
|
||||||
|
Object.defineProperty( Module, 'exports', { value: {} } );
|
||||||
|
|
||||||
|
function __web_on_grow() {
|
||||||
|
var buffer = Module.instance.exports.memory.buffer;
|
||||||
|
HEAP8 = new Int8Array( buffer );
|
||||||
|
HEAP16 = new Int16Array( buffer );
|
||||||
|
HEAP32 = new Int32Array( buffer );
|
||||||
|
HEAPU8 = new Uint8Array( buffer );
|
||||||
|
HEAPU16 = new Uint16Array( buffer );
|
||||||
|
HEAPU32 = new Uint32Array( buffer );
|
||||||
|
HEAPF32 = new Float32Array( buffer );
|
||||||
|
HEAPF64 = new Float64Array( buffer );
|
||||||
|
Module.HEAP8 = HEAP8;
|
||||||
|
Module.HEAP16 = HEAP16;
|
||||||
|
Module.HEAP32 = HEAP32;
|
||||||
|
Module.HEAPU8 = HEAPU8;
|
||||||
|
Module.HEAPU16 = HEAPU16;
|
||||||
|
Module.HEAPU32 = HEAPU32;
|
||||||
|
Module.HEAPF32 = HEAPF32;
|
||||||
|
Module.HEAPF64 = HEAPF64;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
imports: {
|
||||||
|
env: {
|
||||||
|
"__cargo_web_snippet_09675c7ed2827e045dc760aeac3d286437cfbe5e": function($0, $1, $2, $3) {
|
||||||
|
$1 = Module.STDWEB_PRIVATE.to_js($1);$2 = Module.STDWEB_PRIVATE.to_js($2);$3 = Module.STDWEB_PRIVATE.to_js($3);Module.STDWEB_PRIVATE.from_js($0, (function(){try{return{value:function(){return($1).setAttribute(($2),($3));}(),success:true};}catch(error){return{error:error,success:false};}})());
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_0e54fd9c163fcf648ce0a395fde4500fd167a40b": function($0) {
|
||||||
|
var r = Module.STDWEB_PRIVATE.acquire_js_reference( $0 );return (r instanceof DOMException) && (r.name === "InvalidCharacterError");
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_1edaec034bdcb0a749c6d5de76c29f6371afb5a0": function($0) {
|
||||||
|
var o = Module.STDWEB_PRIVATE.acquire_js_reference( $0 );return (o instanceof Event && o.type === "input");
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_2908dbb08792df5e699e324eec3e29fd6a57c2c9": function($0) {
|
||||||
|
var o = Module.STDWEB_PRIVATE.acquire_js_reference( $0 );return (o instanceof HTMLInputElement);
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_3c5e83d16a83fc7147ec91e2506438012952f55a": function($0) {
|
||||||
|
var o = Module.STDWEB_PRIVATE.acquire_js_reference( $0 );return (o instanceof Element);
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_614a3dd2adb7e9eac4a0ec6e59d37f87e0521c3b": function($0, $1) {
|
||||||
|
$1 = Module.STDWEB_PRIVATE.to_js($1);Module.STDWEB_PRIVATE.from_js($0, (function(){return($1).error;})());
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_6fcce0aae651e2d748e085ff1f800f87625ff8c8": function($0) {
|
||||||
|
Module.STDWEB_PRIVATE.from_js($0, (function(){return document;})());
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_72fc447820458c720c68d0d8e078ede631edd723": function($0, $1, $2) {
|
||||||
|
console.error( 'Panic location:', Module.STDWEB_PRIVATE.to_js_string( $0, $1 ) + ':' + $2 );
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_80d6d56760c65e49b7be8b6b01c1ea861b046bf0": function($0) {
|
||||||
|
Module.STDWEB_PRIVATE.decrement_refcount( $0 );
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_91749aeb589cd0f9b17cbc01b2872ba709817982": function($0, $1, $2) {
|
||||||
|
$1 = Module.STDWEB_PRIVATE.to_js($1);$2 = Module.STDWEB_PRIVATE.to_js($2);Module.STDWEB_PRIVATE.from_js($0, (function(){try{return{value:function(){return($1).createElement(($2));}(),success:true};}catch(error){return{error:error,success:false};}})());
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_97495987af1720d8a9a923fa4683a7b683e3acd6": function($0, $1) {
|
||||||
|
console.error( 'Panic error message:', Module.STDWEB_PRIVATE.to_js_string( $0, $1 ) );
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_99c4eefdc8d4cc724135163b8c8665a1f3de99e4": function($0, $1, $2, $3) {
|
||||||
|
$1 = Module.STDWEB_PRIVATE.to_js($1);$2 = Module.STDWEB_PRIVATE.to_js($2);$3 = Module.STDWEB_PRIVATE.to_js($3);Module.STDWEB_PRIVATE.from_js($0, (function(){var listener=($1);($2).addEventListener(($3),listener);return listener;})());
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_9f22d4ca7bc938409787341b7db181f8dd41e6df": function($0) {
|
||||||
|
Module.STDWEB_PRIVATE.increment_refcount( $0 );
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_ab05f53189dacccf2d365ad26daa407d4f7abea9": function($0, $1) {
|
||||||
|
$1 = Module.STDWEB_PRIVATE.to_js($1);Module.STDWEB_PRIVATE.from_js($0, (function(){return($1).value;})());
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_afafe9a462a05084fec65cacc7d6598e145ff3e3": function($0, $1, $2) {
|
||||||
|
$1 = Module.STDWEB_PRIVATE.to_js($1);$2 = Module.STDWEB_PRIVATE.to_js($2);Module.STDWEB_PRIVATE.from_js($0, (function(){return($1).createTextNode(($2));})());
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_b06dde4acf09433b5190a4b001259fe5d4abcbc2": function($0, $1) {
|
||||||
|
$1 = Module.STDWEB_PRIVATE.to_js($1);Module.STDWEB_PRIVATE.from_js($0, (function(){return($1).success;})());
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_d5e30f74cb752784e06bd97a37b1f89b6c3433a7": function($0, $1, $2) {
|
||||||
|
$1 = Module.STDWEB_PRIVATE.to_js($1);$2 = Module.STDWEB_PRIVATE.to_js($2);Module.STDWEB_PRIVATE.from_js($0, (function(){return($1).getElementById(($2));})());
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_dc2fd915bd92f9e9c6a3bd15174f1414eee3dbaf": function() {
|
||||||
|
console.error( 'Encountered a panic!' );
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_e741b9d9071097746386b2c2ec044a2bc73e688c": function($0, $1) {
|
||||||
|
$0 = Module.STDWEB_PRIVATE.to_js($0);$1 = Module.STDWEB_PRIVATE.to_js($1);($0).appendChild(($1));
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_e9638d6405ab65f78daf4a5af9c9de14ecf1e2ec": function($0) {
|
||||||
|
$0 = Module.STDWEB_PRIVATE.to_js($0);Module.STDWEB_PRIVATE.unregister_raw_value(($0));
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_f765b15a1a1b5cd266e922e6fca98dd570f17edc": function($0, $1) {
|
||||||
|
$0 = Module.STDWEB_PRIVATE.to_js($0);$1 = Module.STDWEB_PRIVATE.to_js($1);($0).textContent=($1);
|
||||||
|
},
|
||||||
|
"__cargo_web_snippet_ff5103e6cc179d13b4c7a785bdce2708fd559fc0": function($0) {
|
||||||
|
Module.STDWEB_PRIVATE.tmp = Module.STDWEB_PRIVATE.to_js( $0 );
|
||||||
|
},
|
||||||
|
"__web_on_grow": __web_on_grow
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialize: function( instance ) {
|
||||||
|
Object.defineProperty( Module, 'instance', { value: instance } );
|
||||||
|
Object.defineProperty( Module, 'web_malloc', { value: Module.instance.exports.__web_malloc } );
|
||||||
|
Object.defineProperty( Module, 'web_free', { value: Module.instance.exports.__web_free } );
|
||||||
|
Object.defineProperty( Module, 'web_table', { value: Module.instance.exports.__indirect_function_table } );
|
||||||
|
|
||||||
|
|
||||||
|
__web_on_grow();
|
||||||
|
Module.instance.exports.main();
|
||||||
|
|
||||||
|
return Module.exports;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}));
|
BIN
content/enterprise/2020-02-11-prototype/helloworld.wasm
Normal file
BIN
content/enterprise/2020-02-11-prototype/helloworld.wasm
Normal file
Binary file not shown.
101
content/enterprise/2020-02-11-prototype/index.md
Normal file
101
content/enterprise/2020-02-11-prototype/index.md
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
+++
|
||||||
|
title = "enterprise: a new ui framework"
|
||||||
|
date = 2020-02-11
|
||||||
|
template = "post.html"
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["computers", "web-dev"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
This past weekend, while on my trip to Minneapolis, I completed a very early prototype of "enterprise", a new UI framework I've been kind of envisioning over the past couple of weeks. While the UI framework is mainly targeted at web apps, the hope is that with a bit more effort, native UIs can be produced with almost no changes to existing applications. Before I begin to describe how it works, I'd like to acknowledge [Nathan Ringo][1] for his massively helpful feedback in both the brainstorming and the implementation process.
|
||||||
|
|
||||||
|
<!-- more -->
|
||||||
|
|
||||||
|
## Goals of the project
|
||||||
|
|
||||||
|
This project was born out of many frustrations with existing web frameworks. This is kind of the combination of several projects I wanted to tackle; since it's such a long-term thing I'm going to document a bit of what I want to achieve so I can stay on track. The high-level goals of the project are:
|
||||||
|
|
||||||
|
* **Complete separation of business logic from UI.** Theoretically, one could completely retarget the application to a completely different platform (mobile, web, native, something new that will pop up in 5 years), without changing any of the core logic. It does this by introducing [declarative][4]-style [DSL][2]s that are completely architecture-independent.
|
||||||
|
* **Maximally static component relationships.** Like [Svelte][3], I'm aiming to resolve as many relationships between elements as possible during compile-time, to avoid having to maintain a full virtual DOM at runtime.
|
||||||
|
|
||||||
|
With that, let's dive into the code!
|
||||||
|
|
||||||
|
## Demo: Initial Prototype
|
||||||
|
|
||||||
|
The prototype for experimenting is a simple "Hello, world" application. If you've looked at any web framework before, this is probably one of the simplest examples of bindings: type something into a box and watch as the text magically populates with whatever you wrote in the box. If you're using a WASM-compatible browser with JavaScript enabled, you should be able to try out the demo right here:
|
||||||
|
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="text/javascript" src="helloworld.js"></script>
|
||||||
|
|
||||||
|
OK, you say, but I could implement this in 3 lines of JavaScript.
|
||||||
|
|
||||||
|
```js
|
||||||
|
inputEl.addEventListener("change", () => {
|
||||||
|
spanEl.innerText = inputEl.value;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Sure, this works, but it doesn't scale. If you try to write a page full of these kind of bindings directly using JavaScript, you're either going to start running into bugs or building up a pile of unmaintainable spaghetti code. How does enterprise represent this? Well, the enterprise DSL has no concrete syntax yet, but if it did, it would look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
model {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<TextBox bind:value="name" />
|
||||||
|
Hello, {name}!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This looks a lot closer to {React, Vue, Svelte, component structure}-ish code. The idea now is that the "compiler", as I've come to call it, reads the entire specification of the code and creates a sort of dependency graph of actions. For clarity, let's assign some IDs first:
|
||||||
|
|
||||||
|
* Let the `TextBox`'s value attribute be `view_value`.
|
||||||
|
* Let the `{name}` code segment in "Hello, name" be `view_name`.
|
||||||
|
* Let the model's name field be `model_name`.
|
||||||
|
|
||||||
|
Now we can model this as:
|
||||||
|
|
||||||
|
```dot
|
||||||
|
digraph "dependency graph" {
|
||||||
|
graph[bgcolor="transparent", class="default-coloring"];
|
||||||
|
rankdir="LR";
|
||||||
|
|
||||||
|
"view_value" -> "model_name"
|
||||||
|
"model_name" -> "view_name"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The arrows in this graph indicate a dependency where changes to `view_value` propagates down the graph until everything else is changed. For the initial prototype, the data structure passed through this graph is simply a string, but with encoding magic and additional specifications, we can add rich text later. What this means the compiler then generates code that looks something like (but not exactly):
|
||||||
|
|
||||||
|
```rs
|
||||||
|
fn create_view_value(model: &mut Model) -> INode {
|
||||||
|
let el = (...);
|
||||||
|
el.add_event_listener(|evt: InputListener| {
|
||||||
|
let new_value = el.get_attribute("value");
|
||||||
|
model.name = new_value;
|
||||||
|
view_name.set_text(new_value);
|
||||||
|
});
|
||||||
|
el
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There's some complications involving the exact model representation in memory as well as how web attributes are accessed that makes the real code a bit different, but from this example you should be able to see that our "compiler" generated real code that matches our specification above.
|
||||||
|
|
||||||
|
The full code for this can be found [here][5].
|
||||||
|
|
||||||
|
## Future
|
||||||
|
|
||||||
|
Obviously not everyone's application is as simple as a linear dependency graph of simple string values. In fact, I even cheated a bit to get this prototype to function; here's some of the shortcuts I took:
|
||||||
|
|
||||||
|
* I mostly hardcoded web elements in instead of making platform-independent abstractions.
|
||||||
|
* I hardcoded the actual specification for the app myself since the DSL doesn't have a real syntax yet.
|
||||||
|
* All data are string types.
|
||||||
|
|
||||||
|
I'll be working on this some more in the coming weeks, so I'll try to keep updates posted here a bit more frequently. Next time, I'll be looking into implementing the TodoMVC example. Until then, thanks for reading!
|
||||||
|
|
||||||
|
[1]: https://remexre.xyz
|
||||||
|
[2]: https://en.wikipedia.org/wiki/Domain-specific_language
|
||||||
|
[3]: https://svelte.dev
|
||||||
|
[4]: https://en.wikipedia.org/wiki/Declarative_programming
|
||||||
|
[5]: https://git.iptq.io/michael/enterprise
|
100
content/enterprise/2020-02-17-syntax-update.md
Normal file
100
content/enterprise/2020-02-17-syntax-update.md
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
+++
|
||||||
|
title = "enterprise: syntax update"
|
||||||
|
date = 2020-02-17
|
||||||
|
template = "post.html"
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["computers", "web-dev"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
[Enterprise][1]'s frontend DSL just got a syntax! Although the major functionality hasn't really changed, I threw out the ugly verbose AST-construction syntax for a hand-rolled recursive-descent-ish parser.
|
||||||
|
|
||||||
|
<!-- more -->
|
||||||
|
|
||||||
|
The rehashed "Hello, world" example looks a bit like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
component HelloWorld {
|
||||||
|
model {
|
||||||
|
name: String = "hello",
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<input bind:value="name" />
|
||||||
|
"Hello, " {name} "!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This compiles using `cargo-web` into a working version of the last post's prototype. You'll notice that quoted literals are used to represent text rather than just typing it out directly like in XML. This is because I'm actually borrowing Rust syntax and parsing it a bit differently. If I had bare text, then everything you put would have to follow Rust's lexical rules; additionally, data about spacing would be a lot more complicated (and unstable!) to retrieve.
|
||||||
|
|
||||||
|
I could possibly have thrown the whole thing into a parser-generator, using Rust's `proc-macro::TokenTree` as tokens, but TokenTree actually gives you blocks (eg. `()` `{}` `[]`) for free, so I can parse expressions like `{name}` incredibly easily.
|
||||||
|
|
||||||
|
Syntax isn't the only thing that's changed since the last update: I've also revamped how builds work.
|
||||||
|
|
||||||
|
New Build Method
|
||||||
|
----------------
|
||||||
|
|
||||||
|
I'm switching to a build method that's rather unconventional. The original approach looked something like this.
|
||||||
|
|
||||||
|
```dot
|
||||||
|
digraph "dependency graph" {
|
||||||
|
graph[bgcolor="transparent", class="default-coloring"];
|
||||||
|
rankdir="LR";
|
||||||
|
|
||||||
|
"Component DSL" -> "AST" [label = "Parsing"];
|
||||||
|
"AST" -> "Dependency Graph" [label = "Graph traversal"];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Problem here is, when we want code to be modular, the graph traversal approach is going to need information about _all_ modules that are imported in order to
|
||||||
|
be able to produce a flat set of instructions in the final result. If I make a library for a component (say, `enterprise-router`), what should its crate's contents be?
|
||||||
|
|
||||||
|
> **Tangent**: Here's where I'm going to distract myself a little and put this into a more big-picture perspective. Ultimately, the ideal manifestation of an architecture/business-logic separation would be a DSL that completely hides all implementation of its internals.
|
||||||
|
>
|
||||||
|
> That's a pretty far-out goal, so I'm building enterprise incrementally. Sadly, large parts of the language will still rely on the language in which this framework is implemented, Rust. This means that the underlying implementation of features such as modules and async will be relying on the Rust language having these features. However, note that in the long term, a separate DSL for business logic will be planned.
|
||||||
|
|
||||||
|
So what's the solution here? Instead of visiting your component node by node when your component is defined, all the framework is going to do is parse your definition and store the AST of your component as-is. I chose here to serialize ASTs as JSON data and dump it into a static string that will be bundled into your crate.
|
||||||
|
|
||||||
|
Then, in your `build.rs` file, you'll call something like `enterprise_compiler::build(App)`, where `App` is the name of the static string containing the JSON data of the description of your app. This will actually perform the analysis process, calculating the graph of update dependencies, as well as generating the code that will go into a Rust module that you can include into your code.
|
||||||
|
|
||||||
|
Your `build.rs` file might look something like this:
|
||||||
|
|
||||||
|
```rs
|
||||||
|
#[macro_use]
|
||||||
|
extern crate enterprise_macros;
|
||||||
|
|
||||||
|
component! {
|
||||||
|
component HelloWorld {
|
||||||
|
model {
|
||||||
|
name: String = "hello",
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<input bind:value="name" />
|
||||||
|
"Hello, " {name} "!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
enterprise_compiler::process("helloworld", HelloWorld);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a string called `HelloWorld` for the HelloWorld component, and then analyze and generate the final application code for it into a file called `helloworld.rs` that you can `mod` into your application. The advantage to this approach is that external modules can just rely on Rust's crate system, since we're just fetching strings out of other crates.
|
||||||
|
|
||||||
|
Source code: [here][3].
|
||||||
|
|
||||||
|
Next Steps
|
||||||
|
----------
|
||||||
|
|
||||||
|
As mentioned in my previous post, I'm still working on implementing [TodoMVC][2], a simple Todo application that should flesh out some more of the reactive functionalities of the framework. This should solidify some more of the questions regarding interactions between data model and DOM.
|
||||||
|
|
||||||
|
I'll also try to abstract more of the system away so it's less dependent on stdweb's implementation. This means adding a notion of "backend", where different backends may have different implementations of a particular component.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1]: @/enterprise/2020-02-11-prototype/index.md
|
||||||
|
[2]: http://todomvc.com/
|
||||||
|
[3]: https://git.iptq.io/michael/enterprise/src/commit/1453885ed2c3a5159431bb41398b9b8bea4d49f5
|
7
content/enterprise/_index.md
Normal file
7
content/enterprise/_index.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
+++
|
||||||
|
template = "blog.html"
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
include_posts = true
|
||||||
|
+++
|
9
content/pages/_index.md
Normal file
9
content/pages/_index.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
+++
|
||||||
|
title = "all pages"
|
||||||
|
template = "listing.html"
|
||||||
|
page_template = "post.html"
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
include_posts = false
|
||||||
|
+++
|
16
content/pages/about.md
Normal file
16
content/pages/about.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
+++
|
||||||
|
title = "about me"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Hi there! I'm a software developer at Epic Systems in Verona, Wisconsin, and I recently graduated with a Computer Science degree from the University of Minnesota. I've got a wide variety of interests under this field. I've been doing web development for a long time and now I'm looking into security, programming language development, and software development!
|
||||||
|
|
||||||
|
In an effort to rely on less services, I started doing a lot of self-hosting and rewriting of software. Check out some of the projects I'm doing over on my public [Gitea](https://git.iptq.io)!
|
||||||
|
|
||||||
|
If you want my resume, contact me through one of these means:
|
||||||
|
|
||||||
|
## contact
|
||||||
|
- Discord: **iptq#8440**
|
||||||
|
- Email: (I sign all my Git commits with this email)
|
||||||
|
- PGP Key: [hosted on Keybase][1]
|
||||||
|
|
||||||
|
[1]: https://keybase.io/michaelz/pgp_keys.asc?fingerprint=925ecc02890d5cdae26180d4bda47a31a3c8ee6b
|
11
content/pages/projects.md
Normal file
11
content/pages/projects.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
+++
|
||||||
|
title = "projects"
|
||||||
|
+++
|
||||||
|
|
||||||
|
- **[garbage](https://github.com/iptq/garbage)**: A command-line tool for interfacing with the FreeDesktop trashcan spec.
|
||||||
|
- **[leanshot](https://github.com/iptq/leanshot)**: Linux screen capturing tool that freezes the screen before selecting.
|
||||||
|
|
||||||
|
Other projects can be found:
|
||||||
|
|
||||||
|
- [Github](https://github.com/iptq)
|
||||||
|
- [Gitea](https://git.iptq.io/explore)
|
|
@ -1,14 +1,16 @@
|
||||||
---
|
+++
|
||||||
title: "Setting up IRC with Weechat"
|
title = "setting up irc with weechat"
|
||||||
date: 2018-10-18
|
date = 2018-10-18
|
||||||
tags: ["irc"]
|
|
||||||
---
|
[taxonomies]
|
||||||
|
tags = ["computers", "irc", "setup", "things-that-are-good"]
|
||||||
|
+++
|
||||||
|
|
||||||
I've just recently discovered that weechat has a "relay" mode, which means it can act as a relay server to other clients (for example, my phone). If I leave an instance of weechat running on, say, my server that's always running, it can act as a bouncer and my phone can receive notifications for highlights as well.
|
I've just recently discovered that weechat has a "relay" mode, which means it can act as a relay server to other clients (for example, my phone). If I leave an instance of weechat running on, say, my server that's always running, it can act as a bouncer and my phone can receive notifications for highlights as well.
|
||||||
|
|
||||||
The android app I'm using is called [Weechat-Android][2]. On my laptop I'm using [Glowing Bear][5].
|
The android app I'm using is called [Weechat-Android][2]. On my laptop I'm using [Glowing Bear][5].
|
||||||
|
|
||||||
## Step 1: tmux
|
## step 1: tmux
|
||||||
|
|
||||||
To achieve this setup, first I install [tmux][1], which separates the terminal from the session. This means I can leave the weechat instance running in the background and detach my current session from it. The command for this is:
|
To achieve this setup, first I install [tmux][1], which separates the terminal from the session. This means I can leave the weechat instance running in the background and detach my current session from it. The command for this is:
|
||||||
|
|
||||||
|
@ -18,7 +20,7 @@ $ tmux new-session -s weechat
|
||||||
|
|
||||||
where the `-s` option just names the tmux session so it's not assigned some number.
|
where the `-s` option just names the tmux session so it's not assigned some number.
|
||||||
|
|
||||||
## Step 2: Add relay
|
## step 2: add relay
|
||||||
|
|
||||||
Now add a relay through weechat:
|
Now add a relay through weechat:
|
||||||
|
|
||||||
|
@ -37,7 +39,7 @@ where name is
|
||||||
|
|
||||||
according to the [documentation][3].
|
according to the [documentation][3].
|
||||||
|
|
||||||
## Step 2.5: SSL
|
## step 2.5: ssl
|
||||||
|
|
||||||
I'm using SSL on my relay endpoint, and I'd recommend anyone else to use it to. You could follow what the documentation says and generate a self-signed certificate, but getting a trusted certificate with [LetsEncrypt][4] is so easy there's almost no excuse not to do it.
|
I'm using SSL on my relay endpoint, and I'd recommend anyone else to use it to. You could follow what the documentation says and generate a self-signed certificate, but getting a trusted certificate with [LetsEncrypt][4] is so easy there's almost no excuse not to do it.
|
||||||
|
|
||||||
|
@ -62,7 +64,7 @@ Finally, just concatenate the important files, `privkey.pem` and `fullchain.pem`
|
||||||
|
|
||||||
If your private key file starts with `BEGIN CERTIFICATE`, just change that to `BEGIN PRIVATE KEY` (change the END one too) and it should be fine.
|
If your private key file starts with `BEGIN CERTIFICATE`, just change that to `BEGIN PRIVATE KEY` (change the END one too) and it should be fine.
|
||||||
|
|
||||||
## Step 3: Set password
|
## step 3: set password
|
||||||
|
|
||||||
Since weechat 1.6, the option to not use a password has been removed. So in order for clients to be able to connect to the server, you must set one using:
|
Since weechat 1.6, the option to not use a password has been removed. So in order for clients to be able to connect to the server, you must set one using:
|
||||||
|
|
||||||
|
@ -72,7 +74,7 @@ Since weechat 1.6, the option to not use a password has been removed. So in orde
|
||||||
|
|
||||||
The password should appear in asterisks in the weechat prompt box.
|
The password should appear in asterisks in the weechat prompt box.
|
||||||
|
|
||||||
## Step 4: Connect
|
## step 4: connect
|
||||||
|
|
||||||
This depends on your setup, but you must make sure that your setup is reachable from the outside. Make sure the port that you chose for the relay is accessible through firewalls.
|
This depends on your setup, but you must make sure that your setup is reachable from the outside. Make sure the port that you chose for the relay is accessible through firewalls.
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 332 KiB |
195
content/setup/2020-05-04-command-line-email-with-aerc/index.md
Normal file
195
content/setup/2020-05-04-command-line-email-with-aerc/index.md
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
+++
|
||||||
|
title = "command line email with aerc"
|
||||||
|
date = 2020-05-04
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["setup", "email", "protonmail"]
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
include_posts = true
|
||||||
|
toc = true
|
||||||
|
+++
|
||||||
|
|
||||||
|
I just set up command line email with my new work account and my [ProtonMail][1] account today! This article will be covering the full setup.
|
||||||
|
|
||||||
|
![aerc screenshot](aerc-mail.jpg)
|
||||||
|
|
||||||
|
## setting up proton-bridge
|
||||||
|
|
||||||
|
ProtonMail is an email service that end-to-end encrypts its users' emails with PGP. As a result, it doesn't speak SMTP or IMAP directly, since email clients wouldn't know how to undo the encryption anyway. That's why they've provided [proton-bridge][2], a ([now][3]) open-source SMTP/IMAP server that translates their own API calls into SMTP/IMAP for your email clients. For security, it's best to run this locally, so that emails aren't exposed over the network after they're decrypted.
|
||||||
|
|
||||||
|
For my setup, I have proton-bridge running as a systemd service. That means we have to deal with any interactive parts of the service so they're not popping up anymore.
|
||||||
|
|
||||||
|
Firstly, we want to build the bridge without support for the GUI. We won't be using it anyway, so this eliminates the Qt dependency.
|
||||||
|
|
||||||
|
Secondly, proton-bridge stores keys in an encrypted keyring, like [password-store][5]. My regular password-store is encrypted with my passphrase-protected GPG key, so I didn't want to use it since it'll be asking me for the passphrase again every time the timeout expires. We're going to make a separate GPG and password-store setup that will only be used for proton-bridge. Since it's all running locally anyway, we're _not_ to use a passphrase on this GPG key.
|
||||||
|
|
||||||
|
Authenticating only happens once, and the local SMTP/IMAP password doesn't change very often, so we won't really care about that. We'll bundle this up into a couple of nice scripts and then have it configured to start on startup!
|
||||||
|
|
||||||
|
### building proton-bridge
|
||||||
|
|
||||||
|
To build proton-bridge without the GUI, we'll need to grab a copy of the [source][2]. Clone the repo and then change directory to it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/protonmail/proton-bridge
|
||||||
|
cd proton-bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
As of now, there's no tagged releases, so let's just build straight from master. Peeking into the Makefile, there isn't a release build for the nogui option, so let's add it ourselves. Put this somewhere in the Makefile:
|
||||||
|
|
||||||
|
```make
|
||||||
|
build-nogui:
|
||||||
|
PROTONMAIL_ENV= go build ${BUILD_FLAGS_NOGUI} ./cmd/Desktop-Bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `make build-nogui` and you should get a binary called `Desktop-Bridge`. Don't authenticate just quite yet; we want to set up the keychain first so it stores it in the right place.
|
||||||
|
|
||||||
|
### isolating the keychain
|
||||||
|
|
||||||
|
proton-bridge needs a keychain to store the keys that it gets from authenticating. The bridge supports [password-store][5] and GNOME keyring, but I'll be setting up password-store here. The goal now is to create a password-store instance that's isolated from the default installation so it doesn't require any interactive prompts.
|
||||||
|
|
||||||
|
For this part, I created two directories: the directory for the new GPG homedir, and the directory for the new password-store. If you're copy-pasting commands out of this post, I'd recommend you add these variables right now:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PASSWORD_STORE_DIR=/path/to/password/store
|
||||||
|
export GNUPGHOME=/path/to/gpg/home
|
||||||
|
```
|
||||||
|
|
||||||
|
...obviously replacing the paths with paths that you choose. These variables are used by `pass` and `gpg` to overwrite the default directories they use, so it's important to set them up. The next step is to make sure they both exist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p $PASSWORD_STORE_DIR $GNUPGHOME
|
||||||
|
```
|
||||||
|
|
||||||
|
Now initialize the GPG key first. Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg --full-generate-key
|
||||||
|
```
|
||||||
|
|
||||||
|
There should be an interactive prompt. Go through and answer the questions however you see fit. I'd recommend you make the key 4096 bits, never expire, and have no passphrase. This key should never leave your local machine, and you will almost never use it directly, so there should be no problem.
|
||||||
|
|
||||||
|
Then, set up password-store. Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pass init [gpg-id]
|
||||||
|
```
|
||||||
|
|
||||||
|
where `[gpg-id]` is some identifier for the key you just created (name or email works).
|
||||||
|
|
||||||
|
At this point, you should test your configuration by running the `Desktop-Bridge` program with the `-c` option to open the prompt. Make sure you are able to log in and that rerunning the program should automatically run the bridge using the authenticated user without any interactive prompts.
|
||||||
|
|
||||||
|
```
|
||||||
|
Welcome to ProtonMail Bridge interactive shell
|
||||||
|
___....___
|
||||||
|
^^ __..-:'':__:..:__:'':-..__
|
||||||
|
_.-:__:.-:'': : : :'':-.:__:-._
|
||||||
|
.':.-: : : : : : : : : :._:'.
|
||||||
|
_ :.': : : : : : : : : : : :'.: _
|
||||||
|
[ ]: : : : : : : : : : : : : :[ ]
|
||||||
|
[ ]: : : : : : : : : : : : : :[ ]
|
||||||
|
:::::::::[ ]:__:__:__:__:__:__:__:__:__:__:__:__:__:[ ]:::::::::::
|
||||||
|
!!!!!!!!![ ]!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![ ]!!!!!!!!!!!
|
||||||
|
^^^^^^^^^[ ]^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[ ]^^^^^^^^^^^
|
||||||
|
[ ] [ ]
|
||||||
|
[ ] [ ]
|
||||||
|
jgs [ ] [ ]
|
||||||
|
~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^
|
||||||
|
>>> login
|
||||||
|
Username: iptq
|
||||||
|
Password:
|
||||||
|
Authenticating ...
|
||||||
|
Adding account ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### automating the whole process
|
||||||
|
|
||||||
|
Now that the bridge is able to run independently, let's make a tiny wrapper script called `run-proton-bridge.sh` that calls it using the appropriate environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# same as before
|
||||||
|
export PASSWORD_STORE_DIR=/path/to/password/store
|
||||||
|
export GNUPGHOME=/path/to/gpg/home
|
||||||
|
|
||||||
|
exec /path/to/Desktop-Bridge $@
|
||||||
|
```
|
||||||
|
|
||||||
|
Save this somewhere, give it executable permissions.
|
||||||
|
|
||||||
|
Finally, we can add a systemd service that runs this whole business whenever the network is available. I'd recommend adding this as a user service rather than a system service. You can put this somewhere like `$HOME/.config/systemd/user/proton-bridge.service`:
|
||||||
|
|
||||||
|
```systemd
|
||||||
|
[Unit]
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/path/to/run-proton-bridge.sh
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Run this service:
|
||||||
|
|
||||||
|
```
|
||||||
|
systemctl --user start proton-bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable it to have it auto-start:
|
||||||
|
|
||||||
|
```
|
||||||
|
systemctl --user enable proton-bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
Bridge configuration should be complete at this point, so let's move on to configuring our mail client to work with it.
|
||||||
|
|
||||||
|
## setting up aerc
|
||||||
|
|
||||||
|
[aerc][4] is a new (**work-in-progress**) command-line mail client by Drew Devault. It features a familiar set of default keybinds, calls out to your favorite editor for composition, and DWIM for many other things. I just started using it today and had no problem getting used to the interface.
|
||||||
|
|
||||||
|
The setup for basic SMTP/IMAP email accounts is actually pretty trivial. When you run aerc for the first time, or whenever you run the command `:new-account`, an interactive screen is brought up prompting you for the details about your mailbox. If your mail provider doesn't work immediately, jump into `#aerc` on freenode; the folks there are super helpful with different mail providers.
|
||||||
|
|
||||||
|
### setting up aerc.. _with_ protonmail
|
||||||
|
|
||||||
|
If you've been keeping up, then all the pieces should be in place for aerc to work with ProtonMail. There's just a tiny bit of glue we have to add to put it all together.
|
||||||
|
|
||||||
|
**aerc does not allow untrusted certificates**. Since proton-bridge generates a self-signed cert, we'll need to trust this cert before we can do anything. There's not really an easy way to pull the certificate out, so I'd recommend just firing up the bridge and then connecting to it using the openssl client and then copy-pasting the certificate part:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ openssl s_client -starttls imap -connect 127.0.0.1:1143 -showcerts
|
||||||
|
CONNECTED(00000003)
|
||||||
|
Can't use SSL_get_servername
|
||||||
|
depth=0 C = CH, O = Proton Technologies AG, OU = ProtonMail, CN = 127.0.0.1
|
||||||
|
verify return:1
|
||||||
|
---
|
||||||
|
Certificate chain
|
||||||
|
0 s:C = CH, O = Proton Technologies AG, OU = ProtonMail, CN = 127.0.0.1
|
||||||
|
i:C = CH, O = Proton Technologies AG, OU = ProtonMail, CN = 127.0.0.1
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
...
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
---
|
||||||
|
Server certificate
|
||||||
|
subject=C = CH, O = Proton Technologies AG, OU = ProtonMail, CN = 127.0.0.1
|
||||||
|
issuer=C = CH, O = Proton Technologies AG, OU = ProtonMail, CN = 127.0.0.1
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Take that huge chunk starting with the line `BEGIN CERTIFICATE` and ending with the line `END CERTIFICATE` and stick it into some file (ex. `protonmail.crt`). This is the self-signed cert we need to trust.
|
||||||
|
|
||||||
|
Now we'll need to tell our system to allow this cert. This varies from system to system; I'm on an Arch machine, so I ran:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo trust anchor --store protonmail.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
After this, fire up `aerc` again, and your emails should start showing up.
|
||||||
|
|
||||||
|
[1]: https://protonmail.com/
|
||||||
|
[2]: https://github.com/ProtonMail/proton-bridge
|
||||||
|
[3]: https://protonmail.com/blog/bridge-open-source/
|
||||||
|
[4]: https://aerc-mail.org/
|
11
content/setup/_index.md
Normal file
11
content/setup/_index.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
+++
|
||||||
|
template = "blog.html"
|
||||||
|
insert_anchor_links = "left"
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
include_posts = true
|
||||||
|
+++
|
||||||
|
|
||||||
|
# setup
|
||||||
|
|
||||||
|
These posts are tutorial-style articles for setting things up.
|
116
flake.lock
116
flake.lock
|
@ -1,116 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"agda": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-parts": "flake-parts",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1729253305,
|
|
||||||
"narHash": "sha256-S7/C8VGMrXeGhFeYXx1cVBjZlNVB5L1o/SkW0hFm0Jc=",
|
|
||||||
"owner": "agda",
|
|
||||||
"repo": "agda",
|
|
||||||
"rev": "3425ed5543d2e6f98094b834fedcdec3a2fb67a6",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "agda",
|
|
||||||
"repo": "agda",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-parts": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs-lib": "nixpkgs-lib"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1701473968,
|
|
||||||
"narHash": "sha256-YcVE5emp1qQ8ieHUnxt1wCZCC3ZfAS+SRRWZ2TMda7E=",
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "flake-parts",
|
|
||||||
"rev": "34fed993f1674c8d06d58b37ce1e0fe5eebcb9f5",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "flake-parts",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1726560853,
|
|
||||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "flake-utils",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1729265718,
|
|
||||||
"narHash": "sha256-4HQI+6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "ccc0c2126893dd20963580b6478d1a10a4512185",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "nixpkgs",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-lib": {
|
|
||||||
"locked": {
|
|
||||||
"dir": "lib",
|
|
||||||
"lastModified": 1701253981,
|
|
||||||
"narHash": "sha256-ztaDIyZ7HrTAfEEUt9AtTDNoCYxUdSd6NrRHaYOIxtk=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "e92039b55bcd58469325ded85d4f58dd5a4eaf58",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"dir": "lib",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"agda": "agda",
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
31
flake.nix
31
flake.nix
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
inputs.agda.url = "github:agda/agda";
|
|
||||||
inputs.agda.inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
outputs = { self, nixpkgs, flake-utils, agda, }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs { inherit system; overlays = [ agda.overlays.default ]; };
|
|
||||||
agda-pkg = agda.packages.x86_64-linux.default;
|
|
||||||
flakePkgs = rec {
|
|
||||||
agda-bin = pkgs.callPackage ./nix/agda-bin.nix { agda-pkg = pkgs.haskellPackages.Agda.bin; };
|
|
||||||
docker-builder =
|
|
||||||
pkgs.callPackage ./nix/docker-builder.nix { inherit agda-bin; };
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
packages = flake-utils.lib.flattenTree flakePkgs;
|
|
||||||
devShell = pkgs.mkShell {
|
|
||||||
ASTRO_TELEMETRY_DISABLED = 1;
|
|
||||||
|
|
||||||
packages = with pkgs;
|
|
||||||
with flakePkgs; [
|
|
||||||
bun
|
|
||||||
nixfmt-rfc-style
|
|
||||||
nix-tree
|
|
||||||
shellcheck
|
|
||||||
|
|
||||||
nodejs_20
|
|
||||||
corepack
|
|
||||||
];
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://frontmatter.codes/frontmatter.schema.json",
|
|
||||||
"frontMatter.framework.id": "astro",
|
|
||||||
"frontMatter.preview.host": "http://localhost:4321",
|
|
||||||
"frontMatter.content.publicFolder": "public",
|
|
||||||
"frontMatter.content.pageFolders": [
|
|
||||||
{
|
|
||||||
"title": "posts",
|
|
||||||
"path": "[[workspace]]/src/content/posts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"frontMatter.taxonomy.contentTypes": [
|
|
||||||
{
|
|
||||||
"name": "default",
|
|
||||||
"pageBundle": false,
|
|
||||||
"previewPath": "",
|
|
||||||
"filePrefix": null,
|
|
||||||
"clearEmpty": true,
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"title": "Title",
|
|
||||||
"name": "title",
|
|
||||||
"type": "string",
|
|
||||||
"single": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Description",
|
|
||||||
"name": "description",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Publishing date",
|
|
||||||
"name": "date",
|
|
||||||
"type": "datetime",
|
|
||||||
"default": "{{now}}",
|
|
||||||
"isPublishDate": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Content preview",
|
|
||||||
"name": "heroImage",
|
|
||||||
"type": "image",
|
|
||||||
"isPreviewImage": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
{ agda-pkg, runCommand, writeShellScriptBin, writeTextFile, agdaPackages }:
|
|
||||||
|
|
||||||
let
|
|
||||||
libraryFile =
|
|
||||||
with agdaPackages;
|
|
||||||
writeTextFile {
|
|
||||||
name = "agda-libraries";
|
|
||||||
text = ''
|
|
||||||
${agdaPackages.cubical.src}/cubical.agda-lib
|
|
||||||
${agdaPackages.standard-library.src}/standard-library.agda-lib
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Add an extra layer of indirection here to prevent all of GHC from being pulled in
|
|
||||||
wtf = runCommand "agda-bin" { } ''
|
|
||||||
cp ${agda-pkg}/bin/agda $out
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
|
|
||||||
writeShellScriptBin "agda" ''
|
|
||||||
set -euo pipefail
|
|
||||||
exec ${wtf} --library-file=${libraryFile} $@
|
|
||||||
''
|
|
|
@ -1,47 +0,0 @@
|
||||||
{ dockerTools
|
|
||||||
, agda-bin
|
|
||||||
, bash
|
|
||||||
, corepack
|
|
||||||
, coreutils
|
|
||||||
, gitMinimal
|
|
||||||
, gnused
|
|
||||||
, minio-client
|
|
||||||
, nodejs_20
|
|
||||||
, openssh
|
|
||||||
, pkgsLinux
|
|
||||||
, rsync
|
|
||||||
}:
|
|
||||||
|
|
||||||
dockerTools.buildLayeredImage {
|
|
||||||
name = "blog-docker-builder";
|
|
||||||
|
|
||||||
contents = with dockerTools; [
|
|
||||||
agda-bin
|
|
||||||
bash
|
|
||||||
caCertificates
|
|
||||||
corepack
|
|
||||||
coreutils
|
|
||||||
fakeNss
|
|
||||||
gitMinimal
|
|
||||||
gnused
|
|
||||||
minio-client
|
|
||||||
nodejs_20
|
|
||||||
openssh
|
|
||||||
rsync
|
|
||||||
usrBinEnv
|
|
||||||
];
|
|
||||||
|
|
||||||
# fakeRootCommands = ''
|
|
||||||
# #!${pkgsLinux.runtimeShell}
|
|
||||||
# ${pkgsLinux.dockerTools.shadowSetup}
|
|
||||||
# groupadd -r builder
|
|
||||||
# useradd -r -g builder builder
|
|
||||||
# '';
|
|
||||||
}
|
|
||||||
|
|
||||||
# copyToRoot = with dockerTools; buildEnv {
|
|
||||||
# name = "blog-docker-builder-image-root";
|
|
||||||
# paths = [
|
|
||||||
# ];
|
|
||||||
# };
|
|
||||||
|
|
57
package.json
57
package.json
|
@ -1,57 +0,0 @@
|
||||||
{
|
|
||||||
"name": "blog",
|
|
||||||
"type": "module",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"packageManager": "pnpm@9.4.0+sha256.b6fd0bfda555e7e584ad7e56b30c68b01d5a04f9ee93989f4b93ca8473c49c74",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "astro dev",
|
|
||||||
"start": "astro dev",
|
|
||||||
"build": "astro build",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"astro": "astro",
|
|
||||||
"format": "prettier -w ."
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/markdoc": "^0.11.1",
|
|
||||||
"@astrojs/markdown-remark": "^5.1.1",
|
|
||||||
"@astrojs/mdx": "^1.1.5",
|
|
||||||
"@astrojs/rss": "^3.0.0",
|
|
||||||
"@astrojs/sitemap": "^3.1.6",
|
|
||||||
"@justfork/rehype-autolink-headings": "^5.1.1",
|
|
||||||
"astro": "^3.6.5",
|
|
||||||
"astro-imagetools": "^0.9.0",
|
|
||||||
"astro-remark-description": "^1.1.2",
|
|
||||||
"classnames": "^2.5.1",
|
|
||||||
"fork-awesome": "^1.2.0",
|
|
||||||
"katex": "^0.16.10",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"mdast-util-to-string": "^4.0.0",
|
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"reading-time": "^1.5.0",
|
|
||||||
"rehype-accessible-emojis": "^0.3.2",
|
|
||||||
"rehype-katex": "^6.0.3",
|
|
||||||
"remark-emoji": "^4.0.1",
|
|
||||||
"remark-github-beta-blockquote-admonitions": "^2.2.1",
|
|
||||||
"remark-math": "^5.1.1",
|
|
||||||
"remark-parse": "^10.0.2",
|
|
||||||
"sanitize-html": "^2.13.1",
|
|
||||||
"sharp": "^0.33.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@biomejs/biome": "^1.8.2",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
|
||||||
"@types/sanitize-html": "^2.13.0",
|
|
||||||
"date-fns": "^2.30.0",
|
|
||||||
"hast-util-from-html": "^2.0.1",
|
|
||||||
"hast-util-to-html": "^9.0.1",
|
|
||||||
"mdast": "^3.0.0",
|
|
||||||
"mdast-util-from-markdown": "^2.0.1",
|
|
||||||
"prettier": "^3.3.2",
|
|
||||||
"prettier-plugin-astro": "^0.12.3",
|
|
||||||
"rehype-slug": "^6.0.0",
|
|
||||||
"sass": "^1.77.6",
|
|
||||||
"shiki": "^0.14.7",
|
|
||||||
"unified": "^11.0.5",
|
|
||||||
"unist-util-visit": "^5.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,155 +0,0 @@
|
||||||
// https://github.com/myl7/remark-github-beta-blockquote-admonitions
|
|
||||||
// License: Apache-2.0
|
|
||||||
|
|
||||||
import { visit } from "unist-util-visit";
|
|
||||||
import type { Data } from "unist";
|
|
||||||
import type { BuildVisitor } from "unist-util-visit";
|
|
||||||
import type { Blockquote, Paragraph, Text } from "mdast";
|
|
||||||
import type { RemarkPlugin } from "@astrojs/markdown-remark";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
const remarkAdmonitions: RemarkPlugin =
|
|
||||||
(providedConfig?: Partial<Config>) => (tree) => {
|
|
||||||
visit(tree, handleNode({ ...defaultConfig, ...providedConfig }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export default remarkAdmonitions;
|
|
||||||
|
|
||||||
const handleNode =
|
|
||||||
(config: Config): BuildVisitor =>
|
|
||||||
(node) => {
|
|
||||||
// Filter required elems
|
|
||||||
if (node.type !== "blockquote") return;
|
|
||||||
const blockquote = node as Blockquote;
|
|
||||||
|
|
||||||
if (blockquote.children[0]?.type !== "paragraph") return;
|
|
||||||
|
|
||||||
const paragraph = blockquote.children[0];
|
|
||||||
if (paragraph.children[0]?.type !== "text") return;
|
|
||||||
|
|
||||||
const text = paragraph.children[0];
|
|
||||||
|
|
||||||
// A link break after the title is explicitly required by GitHub
|
|
||||||
const titleEnd = text.value.indexOf("\n");
|
|
||||||
if (titleEnd < 0) return;
|
|
||||||
|
|
||||||
const textBody = text.value.substring(titleEnd + 1);
|
|
||||||
let title = text.value.substring(0, titleEnd);
|
|
||||||
// Handle whitespaces after the title.
|
|
||||||
// Whitespace characters are defined by GFM
|
|
||||||
const m = /[ \t\v\f\r]+$/.exec(title);
|
|
||||||
if (m && !config.titleKeepTrailingWhitespaces) {
|
|
||||||
title = title.substring(0, title.length - m[0].length);
|
|
||||||
}
|
|
||||||
if (!nameFilter(config.titleFilter)(title)) return;
|
|
||||||
const { displayTitle, checkedTitle } = config.titleTextMap(title);
|
|
||||||
|
|
||||||
// Update the text body
|
|
||||||
text.value = textBody;
|
|
||||||
|
|
||||||
// Insert the title element and add classes for the title
|
|
||||||
const paragraphTitleText: Text = { type: "text", value: displayTitle };
|
|
||||||
const paragraphTitle: Paragraph = {
|
|
||||||
type: "paragraph",
|
|
||||||
children: [paragraphTitleText],
|
|
||||||
data: config.dataMaps.title({
|
|
||||||
hProperties: {
|
|
||||||
className: classNameMap(config.classNameMaps.title)(checkedTitle),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
blockquote.children.unshift(paragraphTitle);
|
|
||||||
|
|
||||||
// Add classes for the block
|
|
||||||
blockquote.data = config.dataMaps.block({
|
|
||||||
...blockquote.data,
|
|
||||||
hProperties: {
|
|
||||||
className: classNameMap(config.classNameMaps.block)(checkedTitle),
|
|
||||||
},
|
|
||||||
// The blockquote should be rendered as a div, which is explicitly required by GitHub
|
|
||||||
hName: "div",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const TITLE_PATTERN =
|
|
||||||
/\[\!admonition: (attention|caution|danger|error|hint|important|note|tip|warning)\]/i;
|
|
||||||
|
|
||||||
export const mkdocsConfig: Partial<Config> = {
|
|
||||||
classNameMaps: {
|
|
||||||
block: (title) => [
|
|
||||||
"admonition",
|
|
||||||
...(title.startsWith("admonition: ")
|
|
||||||
? title.substring("admonition: ".length)
|
|
||||||
: title
|
|
||||||
).split(" "),
|
|
||||||
],
|
|
||||||
title: classNames("admonition-title"),
|
|
||||||
},
|
|
||||||
|
|
||||||
titleFilter: (title) => Boolean(title.match(TITLE_PATTERN)),
|
|
||||||
|
|
||||||
titleTextMap: (title: string) => {
|
|
||||||
const match = title.match(TITLE_PATTERN);
|
|
||||||
const displayTitle = match?.[1] ?? "";
|
|
||||||
const checkedTitle = displayTitle;
|
|
||||||
return { displayTitle, checkedTitle };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
classNameMaps: {
|
|
||||||
block: ClassNameMap;
|
|
||||||
title: ClassNameMap;
|
|
||||||
};
|
|
||||||
titleFilter: NameFilter;
|
|
||||||
titleTextMap: (title: string) => {
|
|
||||||
displayTitle: string;
|
|
||||||
checkedTitle: string;
|
|
||||||
};
|
|
||||||
dataMaps: {
|
|
||||||
block: (data: Data) => Data;
|
|
||||||
title: (data: Data) => Data;
|
|
||||||
};
|
|
||||||
titleKeepTrailingWhitespaces: boolean;
|
|
||||||
legacyTitle: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultConfig: Config = {
|
|
||||||
classNameMaps: {
|
|
||||||
block: "admonition",
|
|
||||||
title: "admonition-title",
|
|
||||||
},
|
|
||||||
|
|
||||||
titleFilter: ["[!NOTE]", "[!IMPORTANT]", "[!WARNING]"],
|
|
||||||
|
|
||||||
titleTextMap: (title) => ({
|
|
||||||
displayTitle: title.substring(2, title.length - 1),
|
|
||||||
checkedTitle: title.substring(2, title.length - 1),
|
|
||||||
}),
|
|
||||||
dataMaps: {
|
|
||||||
block: (data) => data,
|
|
||||||
title: (data) => data,
|
|
||||||
},
|
|
||||||
titleKeepTrailingWhitespaces: false,
|
|
||||||
legacyTitle: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
type ClassNames = string | string[];
|
|
||||||
type ClassNameMap = ClassNames | ((title: string) => ClassNames);
|
|
||||||
|
|
||||||
export function classNameMap(gen: ClassNameMap) {
|
|
||||||
return (title: string) => {
|
|
||||||
const classNames = typeof gen === "function" ? gen(title) : gen;
|
|
||||||
return typeof classNames === "object" ? classNames.join(" ") : classNames;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type NameFilter = ((title: string) => boolean) | string[];
|
|
||||||
|
|
||||||
export function nameFilter(filter: NameFilter) {
|
|
||||||
return (title: string) => {
|
|
||||||
return typeof filter === "function"
|
|
||||||
? filter(title)
|
|
||||||
: filter.includes(title);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,174 +0,0 @@
|
||||||
import type { RootContent } from "hast";
|
|
||||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
||||||
import { fromHtml } from "hast-util-from-html";
|
|
||||||
import { toHtml } from "hast-util-to-html";
|
|
||||||
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import {
|
|
||||||
mkdirSync,
|
|
||||||
mkdtempSync,
|
|
||||||
readFileSync,
|
|
||||||
existsSync,
|
|
||||||
readdirSync,
|
|
||||||
copyFileSync,
|
|
||||||
writeFileSync,
|
|
||||||
} from "node:fs";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join, parse, resolve, basename } from "node:path";
|
|
||||||
import { visit } from "unist-util-visit";
|
|
||||||
import type { RemarkPlugin } from "@astrojs/markdown-remark";
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
base: string;
|
|
||||||
outDir: string;
|
|
||||||
publicDir: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const remarkAgda: RemarkPlugin = ({ base, publicDir }: Options) => {
|
|
||||||
const destDir = join(publicDir, "generated", "agda");
|
|
||||||
mkdirSync(destDir, { recursive: true });
|
|
||||||
|
|
||||||
return (tree, { history }) => {
|
|
||||||
const path: string = history[history.length - 1]!;
|
|
||||||
if (!(path.endsWith(".lagda.md") || path.endsWith(".agda"))) return;
|
|
||||||
|
|
||||||
// console.log("AGDA:processing path", path);
|
|
||||||
|
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), "agdaRender."));
|
|
||||||
const agdaOutDir = join(tempDir, "output");
|
|
||||||
mkdirSync(agdaOutDir, { recursive: true });
|
|
||||||
|
|
||||||
const childOutput = spawnSync(
|
|
||||||
"agda",
|
|
||||||
[
|
|
||||||
"--html",
|
|
||||||
`--html-dir=${agdaOutDir}`,
|
|
||||||
"--highlight-occurrences",
|
|
||||||
"--html-highlight=code",
|
|
||||||
path,
|
|
||||||
],
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Locate output file
|
|
||||||
const directory = readdirSync(agdaOutDir);
|
|
||||||
const outputFilename = directory.find((name) => name.endsWith(".md"));
|
|
||||||
|
|
||||||
if (childOutput.status !== 0 || outputFilename === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
`Agda output:
|
|
||||||
|
|
||||||
Stdout:
|
|
||||||
${childOutput.stdout}
|
|
||||||
|
|
||||||
Stderr:
|
|
||||||
${childOutput.stderr}
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
cause: childOutput.error,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputFile = join(agdaOutDir, outputFilename);
|
|
||||||
|
|
||||||
// // TODO: Handle child output
|
|
||||||
// console.error("--AGDA OUTPUT--");
|
|
||||||
// console.error(childOutput);
|
|
||||||
// console.error(childOutput.stdout?.toString());
|
|
||||||
// console.error(childOutput.stderr?.toString());
|
|
||||||
// console.error("--AGDA OUTPUT--");
|
|
||||||
|
|
||||||
const referencedFiles = new Set();
|
|
||||||
for (const file of readdirSync(agdaOutDir)) {
|
|
||||||
referencedFiles.add(file);
|
|
||||||
|
|
||||||
const fullPath = join(agdaOutDir, file);
|
|
||||||
const fullDestPath = join(destDir, file);
|
|
||||||
|
|
||||||
if (file.endsWith(".html")) {
|
|
||||||
const src = readFileSync(fullPath);
|
|
||||||
writeFileSync(
|
|
||||||
fullDestPath,
|
|
||||||
`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="stylesheet" href="${base}generated/agda/Agda.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<pre class="Agda">
|
|
||||||
${src}
|
|
||||||
</pre>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
copyFileSync(fullPath, fullDestPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const htmlname = basename(resolve(outputFile)).replace(/(\.lagda)?\.md/, ".html");
|
|
||||||
|
|
||||||
const doc = readFileSync(outputFile);
|
|
||||||
|
|
||||||
// This is the post-processed markdown with HTML code blocks replacing the Agda code blocks
|
|
||||||
const tree2 = fromMarkdown(doc);
|
|
||||||
|
|
||||||
const collectedCodeBlocks: RootContent[] = [];
|
|
||||||
visit(tree2, "html", (node) => {
|
|
||||||
const html = fromHtml(node.value, { fragment: true });
|
|
||||||
|
|
||||||
const firstChild: RootContent = html.children[0]!;
|
|
||||||
|
|
||||||
visit(html, "element", (node) => {
|
|
||||||
if (node.tagName !== "a") return;
|
|
||||||
|
|
||||||
if (node.properties.href) {
|
|
||||||
// Trim off end
|
|
||||||
const [href, hash, ...rest] = node.properties.href.split("#");
|
|
||||||
if (rest.length > 0) throw new Error("come look at this");
|
|
||||||
|
|
||||||
if (href === htmlname) {
|
|
||||||
node.properties.href = `#${hash}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referencedFiles.has(href)) {
|
|
||||||
node.properties.href = `${base}generated/agda/${href}${hash ? `#${hash}` : ""}`;
|
|
||||||
node.properties.target = "_blank";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!firstChild?.properties?.className?.includes("Agda")) return;
|
|
||||||
|
|
||||||
const stringContents = toHtml(firstChild);
|
|
||||||
collectedCodeBlocks.push({
|
|
||||||
contents: stringContents,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let idx = 0;
|
|
||||||
try {
|
|
||||||
visit(tree, "code", (node) => {
|
|
||||||
if (!(node.lang === null || node.lang === "agda")) return;
|
|
||||||
|
|
||||||
if (idx > collectedCodeBlocks.length) {
|
|
||||||
throw new Error("failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
node.type = "html";
|
|
||||||
node.value = collectedCodeBlocks[idx].contents;
|
|
||||||
idx += 1;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// TODO: Figure out a way to handle this correctly
|
|
||||||
// Possibly by diffing?
|
|
||||||
console.log("Mismatch in number of args. Perhaps there was an empty block?");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default remarkAgda;
|
|
|
@ -1,16 +0,0 @@
|
||||||
import getReadingTime from "reading-time";
|
|
||||||
import { toString as mdastToString } from "mdast-util-to-string";
|
|
||||||
import type { RemarkPlugin } from "@astrojs/markdown-remark";
|
|
||||||
|
|
||||||
const remarkReadingTime: RemarkPlugin = () => {
|
|
||||||
return (tree, { data }) => {
|
|
||||||
const textOnPage = mdastToString(tree);
|
|
||||||
const readingTime = getReadingTime(textOnPage);
|
|
||||||
|
|
||||||
// readingTime.text will give us minutes read as a friendly string,
|
|
||||||
// i.e. "3 min read"
|
|
||||||
data.astro.frontmatter.minutesRead = readingTime.text;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default remarkReadingTime;
|
|
|
@ -1,42 +0,0 @@
|
||||||
import type { RemarkPlugin } from "@astrojs/markdown-remark";
|
|
||||||
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
||||||
import { visit } from "unist-util-visit";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
|
|
||||||
const remarkTypst: RemarkPlugin = () => {
|
|
||||||
const tmp = mkdtempSync(join(tmpdir(), "typst"));
|
|
||||||
let ctr = 0;
|
|
||||||
|
|
||||||
return (tree) => {
|
|
||||||
visit(
|
|
||||||
tree,
|
|
||||||
(node) => node.type === "code" && node.lang === "typst",
|
|
||||||
(node, index, parent) => {
|
|
||||||
const doc = join(tmp, `${ctr}.typ`);
|
|
||||||
const docOut = join(tmp, `${ctr}.svg`);
|
|
||||||
ctr += 1;
|
|
||||||
|
|
||||||
writeFileSync(doc, node.value);
|
|
||||||
const result = spawnSync(
|
|
||||||
"typst",
|
|
||||||
[
|
|
||||||
"compile",
|
|
||||||
"--format",
|
|
||||||
"svg",
|
|
||||||
doc,
|
|
||||||
],
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
console.log("OUTPUT", result.stderr.toString());
|
|
||||||
|
|
||||||
const svgOut = readFileSync(docOut);
|
|
||||||
node.type = "html";
|
|
||||||
node.value = svgOut.toString();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default remarkTypst;
|
|
7253
pnpm-lock.yaml
7253
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1 +0,0 @@
|
||||||
did:plc:zbg2asfcpyughqspwjjgyc2d
|
|
Binary file not shown.
Before Width: | Height: | Size: 136 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,93 +0,0 @@
|
||||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
|
||||||
|
|
||||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
||||||
This license is copied below, and is also available with a FAQ at:
|
|
||||||
http://scripts.sil.org/OFL
|
|
||||||
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
||||||
-----------------------------------------------------------
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
||||||
development of collaborative font projects, to support the font creation
|
|
||||||
efforts of academic and linguistic communities, and to provide a free and
|
|
||||||
open framework in which fonts may be shared and improved in partnership
|
|
||||||
with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and
|
|
||||||
redistributed freely as long as they are not sold by themselves. The
|
|
||||||
fonts, including any derivative works, can be bundled, embedded,
|
|
||||||
redistributed and/or sold with any software provided that any reserved
|
|
||||||
names are not used by derivative works. The fonts and derivatives,
|
|
||||||
however, cannot be released under any other type of license. The
|
|
||||||
requirement for fonts to remain under this license does not apply
|
|
||||||
to any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
"Font Software" refers to the set of files released by the Copyright
|
|
||||||
Holder(s) under this license and clearly marked as such. This may
|
|
||||||
include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the
|
|
||||||
copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software components as
|
|
||||||
distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
||||||
or substituting -- in part or in whole -- any of the components of the
|
|
||||||
Original Version, by changing formats or by porting the Font Software to a
|
|
||||||
new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical
|
|
||||||
writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
||||||
redistribute, and sell modified and unmodified copies of the Font
|
|
||||||
Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components,
|
|
||||||
in Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled,
|
|
||||||
redistributed and/or sold with any software, provided that each copy
|
|
||||||
contains the above copyright notice and this license. These can be
|
|
||||||
included either as stand-alone text files, human-readable headers or
|
|
||||||
in the appropriate machine-readable metadata fields within text or
|
|
||||||
binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font
|
|
||||||
Name(s) unless explicit written permission is granted by the corresponding
|
|
||||||
Copyright Holder. This restriction only applies to the primary font name as
|
|
||||||
presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
||||||
Software shall not be used to promote, endorse or advertise any
|
|
||||||
Modified Version, except to acknowledge the contribution(s) of the
|
|
||||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
||||||
permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole,
|
|
||||||
must be distributed entirely under this license, and must not be
|
|
||||||
distributed under any other license. The requirement for fonts to
|
|
||||||
remain under this license does not apply to any document created
|
|
||||||
using the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
This license becomes null and void if any of the above conditions are
|
|
||||||
not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
||||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
||||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
||||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
||||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 547 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,119 +0,0 @@
|
||||||
# [<img src="https://katex.org/img/katex-logo-black.svg" width="130" alt="KaTeX">](https://katex.org/)
|
|
||||||
[![npm](https://img.shields.io/npm/v/katex.svg)](https://www.npmjs.com/package/katex)
|
|
||||||
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
|
|
||||||
[![CI](https://github.com/KaTeX/KaTeX/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/KaTeX/KaTeX/actions?query=workflow%3ACI)
|
|
||||||
[![codecov](https://codecov.io/gh/KaTeX/KaTeX/branch/main/graph/badge.svg)](https://codecov.io/gh/KaTeX/KaTeX)
|
|
||||||
[![Discussions](https://img.shields.io/badge/Discussions-join-brightgreen)](https://github.com/KaTeX/KaTeX/discussions)
|
|
||||||
[![jsDelivr](https://data.jsdelivr.com/v1/package/npm/katex/badge?style=rounded)](https://www.jsdelivr.com/package/npm/katex)
|
|
||||||
![katex.min.js size](https://img.badgesize.io/https://unpkg.com/katex/dist/katex.min.js?compression=gzip)
|
|
||||||
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/KaTeX/KaTeX)
|
|
||||||
[![Financial Contributors on Open Collective](https://opencollective.com/katex/all/badge.svg?label=financial+contributors)](https://opencollective.com/katex)
|
|
||||||
|
|
||||||
KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web.
|
|
||||||
|
|
||||||
* **Fast:** KaTeX renders its math synchronously and doesn't need to reflow the page. See how it compares to a competitor in [this speed test](http://www.intmath.com/cg5/katex-mathjax-comparison.php).
|
|
||||||
* **Print quality:** KaTeX's layout is based on Donald Knuth's TeX, the gold standard for math typesetting.
|
|
||||||
* **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources.
|
|
||||||
* **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML.
|
|
||||||
|
|
||||||
KaTeX is compatible with all major browsers, including Chrome, Safari, Firefox, Opera, Edge, and IE 11.
|
|
||||||
|
|
||||||
KaTeX supports much (but not all) of LaTeX and many LaTeX packages. See the [list of supported functions](https://katex.org/docs/supported.html).
|
|
||||||
|
|
||||||
Try out KaTeX [on the demo page](https://katex.org/#demo)!
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
### Starter template
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<!-- KaTeX requires the use of the HTML5 doctype. Without it, KaTeX may not render properly -->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css" integrity="sha384-MlJdn/WNKDGXveldHDdyRP1R4CTHr3FeuDNfhsLPYrq2t0UBkUdK2jyTnXPEK1NQ" crossorigin="anonymous">
|
|
||||||
|
|
||||||
<!-- The loading of KaTeX is deferred to speed up page rendering -->
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.js" integrity="sha384-VQ8d8WVFw0yHhCk5E8I86oOhv48xLpnDZx5T9GogA/Y84DcCKWXDmSDfn13bzFZY" crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
<!-- To automatically render math in text elements, include the auto-render extension: -->
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/contrib/auto-render.min.js" integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR" crossorigin="anonymous"
|
|
||||||
onload="renderMathInElement(document.body);"></script>
|
|
||||||
</head>
|
|
||||||
...
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also [download KaTeX](https://github.com/KaTeX/KaTeX/releases) and host it yourself.
|
|
||||||
|
|
||||||
For details on how to configure auto-render extension, refer to [the documentation](https://katex.org/docs/autorender.html).
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
Call `katex.render` to render a TeX expression directly into a DOM element.
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```js
|
|
||||||
katex.render("c = \\pm\\sqrt{a^2 + b^2}", element, {
|
|
||||||
throwOnError: false
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Call `katex.renderToString` to generate an HTML string of the rendered math,
|
|
||||||
e.g., for server-side rendering. For example:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var html = katex.renderToString("c = \\pm\\sqrt{a^2 + b^2}", {
|
|
||||||
throwOnError: false
|
|
||||||
});
|
|
||||||
// '<span class="katex">...</span>'
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure to include the CSS and font files in both cases.
|
|
||||||
If you are doing all rendering on the server, there is no need to include the
|
|
||||||
JavaScript on the client.
|
|
||||||
|
|
||||||
The examples above use the `throwOnError: false` option, which renders invalid
|
|
||||||
inputs as the TeX source code in red (by default), with the error message as
|
|
||||||
hover text. For other available options, see the
|
|
||||||
[API documentation](https://katex.org/docs/api.html),
|
|
||||||
[options documentation](https://katex.org/docs/options.html), and
|
|
||||||
[handling errors documentation](https://katex.org/docs/error.html).
|
|
||||||
|
|
||||||
## Demo and Documentation
|
|
||||||
|
|
||||||
Learn more about using KaTeX [on the website](https://katex.org)!
|
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
### Code Contributors
|
|
||||||
|
|
||||||
This project exists thanks to all the people who contribute code. If you'd like to help, see [our guide to contributing code](CONTRIBUTING.md).
|
|
||||||
<a href="https://github.com/KaTeX/KaTeX/graphs/contributors"><img src="https://contributors-svg.opencollective.com/katex/contributors.svg?width=890&button=false" alt="Code contributors" /></a>
|
|
||||||
|
|
||||||
### Financial Contributors
|
|
||||||
|
|
||||||
Become a financial contributor and help us sustain our community.
|
|
||||||
|
|
||||||
#### Individuals
|
|
||||||
|
|
||||||
<a href="https://opencollective.com/katex"><img src="https://opencollective.com/katex/individuals.svg?width=890" alt="Contribute on Open Collective"></a>
|
|
||||||
|
|
||||||
#### Organizations
|
|
||||||
|
|
||||||
Support this project with your organization. Your logo will show up here with a link to your website.
|
|
||||||
|
|
||||||
<a href="https://opencollective.com/katex/organization/0/website"><img src="https://opencollective.com/katex/organization/0/avatar.svg" alt="Organization 1"></a>
|
|
||||||
<a href="https://opencollective.com/katex/organization/1/website"><img src="https://opencollective.com/katex/organization/1/avatar.svg" alt="Organization 2"></a>
|
|
||||||
<a href="https://opencollective.com/katex/organization/2/website"><img src="https://opencollective.com/katex/organization/2/avatar.svg" alt="Organization 3"></a>
|
|
||||||
<a href="https://opencollective.com/katex/organization/3/website"><img src="https://opencollective.com/katex/organization/3/avatar.svg" alt="Organization 4"></a>
|
|
||||||
<a href="https://opencollective.com/katex/organization/4/website"><img src="https://opencollective.com/katex/organization/4/avatar.svg" alt="Organization 5"></a>
|
|
||||||
<a href="https://opencollective.com/katex/organization/5/website"><img src="https://opencollective.com/katex/organization/5/avatar.svg" alt="Organization 6"></a>
|
|
||||||
<a href="https://opencollective.com/katex/organization/6/website"><img src="https://opencollective.com/katex/organization/6/avatar.svg" alt="Organization 7"></a>
|
|
||||||
<a href="https://opencollective.com/katex/organization/7/website"><img src="https://opencollective.com/katex/organization/7/avatar.svg" alt="Organization 8"></a>
|
|
||||||
<a href="https://opencollective.com/katex/organization/8/website"><img src="https://opencollective.com/katex/organization/8/avatar.svg" alt="Organization 9"></a>
|
|
||||||
<a href="https://opencollective.com/katex/organization/9/website"><img src="https://opencollective.com/katex/organization/9/avatar.svg" alt="Organization 10"></a>
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
KaTeX is licensed under the [MIT License](http://opensource.org/licenses/MIT).
|
|
|
@ -1,327 +0,0 @@
|
||||||
(function webpackUniversalModuleDefinition(root, factory) {
|
|
||||||
if(typeof exports === 'object' && typeof module === 'object')
|
|
||||||
module.exports = factory(require("katex"));
|
|
||||||
else if(typeof define === 'function' && define.amd)
|
|
||||||
define(["katex"], factory);
|
|
||||||
else if(typeof exports === 'object')
|
|
||||||
exports["renderMathInElement"] = factory(require("katex"));
|
|
||||||
else
|
|
||||||
root["renderMathInElement"] = factory(root["katex"]);
|
|
||||||
})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__771__) {
|
|
||||||
return /******/ (function() { // webpackBootstrap
|
|
||||||
/******/ "use strict";
|
|
||||||
/******/ var __webpack_modules__ = ({
|
|
||||||
|
|
||||||
/***/ 771:
|
|
||||||
/***/ (function(module) {
|
|
||||||
|
|
||||||
module.exports = __WEBPACK_EXTERNAL_MODULE__771__;
|
|
||||||
|
|
||||||
/***/ })
|
|
||||||
|
|
||||||
/******/ });
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ // The module cache
|
|
||||||
/******/ var __webpack_module_cache__ = {};
|
|
||||||
/******/
|
|
||||||
/******/ // The require function
|
|
||||||
/******/ function __webpack_require__(moduleId) {
|
|
||||||
/******/ // Check if module is in cache
|
|
||||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
||||||
/******/ if (cachedModule !== undefined) {
|
|
||||||
/******/ return cachedModule.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/ // Create a new module (and put it into the cache)
|
|
||||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
||||||
/******/ // no module.id needed
|
|
||||||
/******/ // no module.loaded needed
|
|
||||||
/******/ exports: {}
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // Execute the module function
|
|
||||||
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
||||||
/******/
|
|
||||||
/******/ // Return the exports of the module
|
|
||||||
/******/ return module.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ /* webpack/runtime/compat get default export */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
||||||
/******/ __webpack_require__.n = function(module) {
|
|
||||||
/******/ var getter = module && module.__esModule ?
|
|
||||||
/******/ function() { return module['default']; } :
|
|
||||||
/******/ function() { return module; };
|
|
||||||
/******/ __webpack_require__.d(getter, { a: getter });
|
|
||||||
/******/ return getter;
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/define property getters */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // define getter functions for harmony exports
|
|
||||||
/******/ __webpack_require__.d = function(exports, definition) {
|
|
||||||
/******/ for(var key in definition) {
|
|
||||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
||||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
var __webpack_exports__ = {};
|
|
||||||
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
|
|
||||||
!function() {
|
|
||||||
|
|
||||||
// EXPORTS
|
|
||||||
__webpack_require__.d(__webpack_exports__, {
|
|
||||||
"default": function() { return /* binding */ auto_render; }
|
|
||||||
});
|
|
||||||
|
|
||||||
// EXTERNAL MODULE: external "katex"
|
|
||||||
var external_katex_ = __webpack_require__(771);
|
|
||||||
var external_katex_default = /*#__PURE__*/__webpack_require__.n(external_katex_);
|
|
||||||
;// CONCATENATED MODULE: ./contrib/auto-render/splitAtDelimiters.js
|
|
||||||
/* eslint no-constant-condition:0 */
|
|
||||||
var findEndOfMath = function findEndOfMath(delimiter, text, startIndex) {
|
|
||||||
// Adapted from
|
|
||||||
// https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx
|
|
||||||
var index = startIndex;
|
|
||||||
var braceLevel = 0;
|
|
||||||
var delimLength = delimiter.length;
|
|
||||||
|
|
||||||
while (index < text.length) {
|
|
||||||
var character = text[index];
|
|
||||||
|
|
||||||
if (braceLevel <= 0 && text.slice(index, index + delimLength) === delimiter) {
|
|
||||||
return index;
|
|
||||||
} else if (character === "\\") {
|
|
||||||
index++;
|
|
||||||
} else if (character === "{") {
|
|
||||||
braceLevel++;
|
|
||||||
} else if (character === "}") {
|
|
||||||
braceLevel--;
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
var escapeRegex = function escapeRegex(string) {
|
|
||||||
return string.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
||||||
};
|
|
||||||
|
|
||||||
var amsRegex = /^\\begin{/;
|
|
||||||
|
|
||||||
var splitAtDelimiters = function splitAtDelimiters(text, delimiters) {
|
|
||||||
var index;
|
|
||||||
var data = [];
|
|
||||||
var regexLeft = new RegExp("(" + delimiters.map(function (x) {
|
|
||||||
return escapeRegex(x.left);
|
|
||||||
}).join("|") + ")");
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
index = text.search(regexLeft);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index > 0) {
|
|
||||||
data.push({
|
|
||||||
type: "text",
|
|
||||||
data: text.slice(0, index)
|
|
||||||
});
|
|
||||||
text = text.slice(index); // now text starts with delimiter
|
|
||||||
} // ... so this always succeeds:
|
|
||||||
|
|
||||||
|
|
||||||
var i = delimiters.findIndex(function (delim) {
|
|
||||||
return text.startsWith(delim.left);
|
|
||||||
});
|
|
||||||
index = findEndOfMath(delimiters[i].right, text, delimiters[i].left.length);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rawData = text.slice(0, index + delimiters[i].right.length);
|
|
||||||
var math = amsRegex.test(rawData) ? rawData : text.slice(delimiters[i].left.length, index);
|
|
||||||
data.push({
|
|
||||||
type: "math",
|
|
||||||
data: math,
|
|
||||||
rawData: rawData,
|
|
||||||
display: delimiters[i].display
|
|
||||||
});
|
|
||||||
text = text.slice(index + delimiters[i].right.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text !== "") {
|
|
||||||
data.push({
|
|
||||||
type: "text",
|
|
||||||
data: text
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* harmony default export */ var auto_render_splitAtDelimiters = (splitAtDelimiters);
|
|
||||||
;// CONCATENATED MODULE: ./contrib/auto-render/auto-render.js
|
|
||||||
/* eslint no-console:0 */
|
|
||||||
|
|
||||||
|
|
||||||
/* Note: optionsCopy is mutated by this method. If it is ever exposed in the
|
|
||||||
* API, we should copy it before mutating.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var renderMathInText = function renderMathInText(text, optionsCopy) {
|
|
||||||
var data = auto_render_splitAtDelimiters(text, optionsCopy.delimiters);
|
|
||||||
|
|
||||||
if (data.length === 1 && data[0].type === 'text') {
|
|
||||||
// There is no formula in the text.
|
|
||||||
// Let's return null which means there is no need to replace
|
|
||||||
// the current text node with a new one.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fragment = document.createDocumentFragment();
|
|
||||||
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
|
||||||
if (data[i].type === "text") {
|
|
||||||
fragment.appendChild(document.createTextNode(data[i].data));
|
|
||||||
} else {
|
|
||||||
var span = document.createElement("span");
|
|
||||||
var math = data[i].data; // Override any display mode defined in the settings with that
|
|
||||||
// defined by the text itself
|
|
||||||
|
|
||||||
optionsCopy.displayMode = data[i].display;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (optionsCopy.preProcess) {
|
|
||||||
math = optionsCopy.preProcess(math);
|
|
||||||
}
|
|
||||||
|
|
||||||
external_katex_default().render(math, span, optionsCopy);
|
|
||||||
} catch (e) {
|
|
||||||
if (!(e instanceof (external_katex_default()).ParseError)) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsCopy.errorCallback("KaTeX auto-render: Failed to parse `" + data[i].data + "` with ", e);
|
|
||||||
fragment.appendChild(document.createTextNode(data[i].rawData));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment.appendChild(span);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fragment;
|
|
||||||
};
|
|
||||||
|
|
||||||
var renderElem = function renderElem(elem, optionsCopy) {
|
|
||||||
for (var i = 0; i < elem.childNodes.length; i++) {
|
|
||||||
var childNode = elem.childNodes[i];
|
|
||||||
|
|
||||||
if (childNode.nodeType === 3) {
|
|
||||||
// Text node
|
|
||||||
var frag = renderMathInText(childNode.textContent, optionsCopy);
|
|
||||||
|
|
||||||
if (frag) {
|
|
||||||
i += frag.childNodes.length - 1;
|
|
||||||
elem.replaceChild(frag, childNode);
|
|
||||||
}
|
|
||||||
} else if (childNode.nodeType === 1) {
|
|
||||||
(function () {
|
|
||||||
// Element node
|
|
||||||
var className = ' ' + childNode.className + ' ';
|
|
||||||
var shouldRender = optionsCopy.ignoredTags.indexOf(childNode.nodeName.toLowerCase()) === -1 && optionsCopy.ignoredClasses.every(function (x) {
|
|
||||||
return className.indexOf(' ' + x + ' ') === -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shouldRender) {
|
|
||||||
renderElem(childNode, optionsCopy);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
} // Otherwise, it's something else, and ignore it.
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var renderMathInElement = function renderMathInElement(elem, options) {
|
|
||||||
if (!elem) {
|
|
||||||
throw new Error("No element provided to render");
|
|
||||||
}
|
|
||||||
|
|
||||||
var optionsCopy = {}; // Object.assign(optionsCopy, option)
|
|
||||||
|
|
||||||
for (var option in options) {
|
|
||||||
if (options.hasOwnProperty(option)) {
|
|
||||||
optionsCopy[option] = options[option];
|
|
||||||
}
|
|
||||||
} // default options
|
|
||||||
|
|
||||||
|
|
||||||
optionsCopy.delimiters = optionsCopy.delimiters || [{
|
|
||||||
left: "$$",
|
|
||||||
right: "$$",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\(",
|
|
||||||
right: "\\)",
|
|
||||||
display: false
|
|
||||||
}, // LaTeX uses $…$, but it ruins the display of normal `$` in text:
|
|
||||||
// {left: "$", right: "$", display: false},
|
|
||||||
// $ must come after $$
|
|
||||||
// Render AMS environments even if outside $$…$$ delimiters.
|
|
||||||
{
|
|
||||||
left: "\\begin{equation}",
|
|
||||||
right: "\\end{equation}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\begin{align}",
|
|
||||||
right: "\\end{align}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\begin{alignat}",
|
|
||||||
right: "\\end{alignat}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\begin{gather}",
|
|
||||||
right: "\\end{gather}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\begin{CD}",
|
|
||||||
right: "\\end{CD}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\[",
|
|
||||||
right: "\\]",
|
|
||||||
display: true
|
|
||||||
}];
|
|
||||||
optionsCopy.ignoredTags = optionsCopy.ignoredTags || ["script", "noscript", "style", "textarea", "pre", "code", "option"];
|
|
||||||
optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || [];
|
|
||||||
optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different
|
|
||||||
// math elements within a single call to `renderMathInElement`.
|
|
||||||
|
|
||||||
optionsCopy.macros = optionsCopy.macros || {};
|
|
||||||
renderElem(elem, optionsCopy);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* harmony default export */ var auto_render = (renderMathInElement);
|
|
||||||
}();
|
|
||||||
__webpack_exports__ = __webpack_exports__["default"];
|
|
||||||
/******/ return __webpack_exports__;
|
|
||||||
/******/ })()
|
|
||||||
;
|
|
||||||
});
|
|
1
public/katex/contrib/auto-render.min.js
vendored
1
public/katex/contrib/auto-render.min.js
vendored
|
@ -1 +0,0 @@
|
||||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},r={};function n(e){var a=r[e];if(void 0!==a)return a.exports;var i=r[e]={exports:{}};return t[e](i,i.exports,n),i.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var a={};return function(){n.d(a,{default:function(){return s}});var e=n(771),t=n.n(e),r=function(e,t,r){for(var n=r,a=0,i=e.length;n<t.length;){var o=t[n];if(a<=0&&t.slice(n,n+i)===e)return n;"\\"===o?n++:"{"===o?a++:"}"===o&&a--,n++}return-1},i=/^\\begin{/,o=function(e,t){for(var n,a=[],o=new RegExp("("+t.map((function(e){return e.left.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")})).join("|")+")");-1!==(n=e.search(o));){n>0&&(a.push({type:"text",data:e.slice(0,n)}),e=e.slice(n));var l=t.findIndex((function(t){return e.startsWith(t.left)}));if(-1===(n=r(t[l].right,e,t[l].left.length)))break;var d=e.slice(0,n+t[l].right.length),s=i.test(d)?d:e.slice(t[l].left.length,n);a.push({type:"math",data:s,rawData:d,display:t[l].display}),e=e.slice(n+t[l].right.length)}return""!==e&&a.push({type:"text",data:e}),a},l=function(e,r){var n=o(e,r.delimiters);if(1===n.length&&"text"===n[0].type)return null;for(var a=document.createDocumentFragment(),i=0;i<n.length;i++)if("text"===n[i].type)a.appendChild(document.createTextNode(n[i].data));else{var l=document.createElement("span"),d=n[i].data;r.displayMode=n[i].display;try{r.preProcess&&(d=r.preProcess(d)),t().render(d,l,r)}catch(e){if(!(e instanceof t().ParseError))throw e;r.errorCallback("KaTeX auto-render: Failed to parse `"+n[i].data+"` with ",e),a.appendChild(document.createTextNode(n[i].rawData));continue}a.appendChild(l)}return a},d=function e(t,r){for(var n=0;n<t.childNodes.length;n++){var a=t.childNodes[n];if(3===a.nodeType){var i=l(a.textContent,r);i&&(n+=i.childNodes.length-1,t.replaceChild(i,a))}else 1===a.nodeType&&function(){var t=" "+a.className+" ";-1===r.ignoredTags.indexOf(a.nodeName.toLowerCase())&&r.ignoredClasses.every((function(e){return-1===t.indexOf(" "+e+" ")}))&&e(a,r)}()}},s=function(e,t){if(!e)throw new Error("No element provided to render");var r={};for(var n in t)t.hasOwnProperty(n)&&(r[n]=t[n]);r.delimiters=r.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\begin{equation}",right:"\\end{equation}",display:!0},{left:"\\begin{align}",right:"\\end{align}",display:!0},{left:"\\begin{alignat}",right:"\\end{alignat}",display:!0},{left:"\\begin{gather}",right:"\\end{gather}",display:!0},{left:"\\begin{CD}",right:"\\end{CD}",display:!0},{left:"\\[",right:"\\]",display:!0}],r.ignoredTags=r.ignoredTags||["script","noscript","style","textarea","pre","code","option"],r.ignoredClasses=r.ignoredClasses||[],r.errorCallback=r.errorCallback||console.error,r.macros=r.macros||{},d(e,r)}}(),a=a.default}()}));
|
|
|
@ -1,222 +0,0 @@
|
||||||
import katex from '../katex.mjs';
|
|
||||||
|
|
||||||
/* eslint no-constant-condition:0 */
|
|
||||||
var findEndOfMath = function findEndOfMath(delimiter, text, startIndex) {
|
|
||||||
// Adapted from
|
|
||||||
// https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx
|
|
||||||
var index = startIndex;
|
|
||||||
var braceLevel = 0;
|
|
||||||
var delimLength = delimiter.length;
|
|
||||||
|
|
||||||
while (index < text.length) {
|
|
||||||
var character = text[index];
|
|
||||||
|
|
||||||
if (braceLevel <= 0 && text.slice(index, index + delimLength) === delimiter) {
|
|
||||||
return index;
|
|
||||||
} else if (character === "\\") {
|
|
||||||
index++;
|
|
||||||
} else if (character === "{") {
|
|
||||||
braceLevel++;
|
|
||||||
} else if (character === "}") {
|
|
||||||
braceLevel--;
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
var escapeRegex = function escapeRegex(string) {
|
|
||||||
return string.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
||||||
};
|
|
||||||
|
|
||||||
var amsRegex = /^\\begin{/;
|
|
||||||
|
|
||||||
var splitAtDelimiters = function splitAtDelimiters(text, delimiters) {
|
|
||||||
var index;
|
|
||||||
var data = [];
|
|
||||||
var regexLeft = new RegExp("(" + delimiters.map(x => escapeRegex(x.left)).join("|") + ")");
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
index = text.search(regexLeft);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index > 0) {
|
|
||||||
data.push({
|
|
||||||
type: "text",
|
|
||||||
data: text.slice(0, index)
|
|
||||||
});
|
|
||||||
text = text.slice(index); // now text starts with delimiter
|
|
||||||
} // ... so this always succeeds:
|
|
||||||
|
|
||||||
|
|
||||||
var i = delimiters.findIndex(delim => text.startsWith(delim.left));
|
|
||||||
index = findEndOfMath(delimiters[i].right, text, delimiters[i].left.length);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rawData = text.slice(0, index + delimiters[i].right.length);
|
|
||||||
var math = amsRegex.test(rawData) ? rawData : text.slice(delimiters[i].left.length, index);
|
|
||||||
data.push({
|
|
||||||
type: "math",
|
|
||||||
data: math,
|
|
||||||
rawData,
|
|
||||||
display: delimiters[i].display
|
|
||||||
});
|
|
||||||
text = text.slice(index + delimiters[i].right.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text !== "") {
|
|
||||||
data.push({
|
|
||||||
type: "text",
|
|
||||||
data: text
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eslint no-console:0 */
|
|
||||||
/* Note: optionsCopy is mutated by this method. If it is ever exposed in the
|
|
||||||
* API, we should copy it before mutating.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var renderMathInText = function renderMathInText(text, optionsCopy) {
|
|
||||||
var data = splitAtDelimiters(text, optionsCopy.delimiters);
|
|
||||||
|
|
||||||
if (data.length === 1 && data[0].type === 'text') {
|
|
||||||
// There is no formula in the text.
|
|
||||||
// Let's return null which means there is no need to replace
|
|
||||||
// the current text node with a new one.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fragment = document.createDocumentFragment();
|
|
||||||
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
|
||||||
if (data[i].type === "text") {
|
|
||||||
fragment.appendChild(document.createTextNode(data[i].data));
|
|
||||||
} else {
|
|
||||||
var span = document.createElement("span");
|
|
||||||
var math = data[i].data; // Override any display mode defined in the settings with that
|
|
||||||
// defined by the text itself
|
|
||||||
|
|
||||||
optionsCopy.displayMode = data[i].display;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (optionsCopy.preProcess) {
|
|
||||||
math = optionsCopy.preProcess(math);
|
|
||||||
}
|
|
||||||
|
|
||||||
katex.render(math, span, optionsCopy);
|
|
||||||
} catch (e) {
|
|
||||||
if (!(e instanceof katex.ParseError)) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsCopy.errorCallback("KaTeX auto-render: Failed to parse `" + data[i].data + "` with ", e);
|
|
||||||
fragment.appendChild(document.createTextNode(data[i].rawData));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment.appendChild(span);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fragment;
|
|
||||||
};
|
|
||||||
|
|
||||||
var renderElem = function renderElem(elem, optionsCopy) {
|
|
||||||
for (var i = 0; i < elem.childNodes.length; i++) {
|
|
||||||
var childNode = elem.childNodes[i];
|
|
||||||
|
|
||||||
if (childNode.nodeType === 3) {
|
|
||||||
// Text node
|
|
||||||
var frag = renderMathInText(childNode.textContent, optionsCopy);
|
|
||||||
|
|
||||||
if (frag) {
|
|
||||||
i += frag.childNodes.length - 1;
|
|
||||||
elem.replaceChild(frag, childNode);
|
|
||||||
}
|
|
||||||
} else if (childNode.nodeType === 1) {
|
|
||||||
(function () {
|
|
||||||
// Element node
|
|
||||||
var className = ' ' + childNode.className + ' ';
|
|
||||||
var shouldRender = optionsCopy.ignoredTags.indexOf(childNode.nodeName.toLowerCase()) === -1 && optionsCopy.ignoredClasses.every(x => className.indexOf(' ' + x + ' ') === -1);
|
|
||||||
|
|
||||||
if (shouldRender) {
|
|
||||||
renderElem(childNode, optionsCopy);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
} // Otherwise, it's something else, and ignore it.
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var renderMathInElement = function renderMathInElement(elem, options) {
|
|
||||||
if (!elem) {
|
|
||||||
throw new Error("No element provided to render");
|
|
||||||
}
|
|
||||||
|
|
||||||
var optionsCopy = {}; // Object.assign(optionsCopy, option)
|
|
||||||
|
|
||||||
for (var option in options) {
|
|
||||||
if (options.hasOwnProperty(option)) {
|
|
||||||
optionsCopy[option] = options[option];
|
|
||||||
}
|
|
||||||
} // default options
|
|
||||||
|
|
||||||
|
|
||||||
optionsCopy.delimiters = optionsCopy.delimiters || [{
|
|
||||||
left: "$$",
|
|
||||||
right: "$$",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\(",
|
|
||||||
right: "\\)",
|
|
||||||
display: false
|
|
||||||
}, // LaTeX uses $…$, but it ruins the display of normal `$` in text:
|
|
||||||
// {left: "$", right: "$", display: false},
|
|
||||||
// $ must come after $$
|
|
||||||
// Render AMS environments even if outside $$…$$ delimiters.
|
|
||||||
{
|
|
||||||
left: "\\begin{equation}",
|
|
||||||
right: "\\end{equation}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\begin{align}",
|
|
||||||
right: "\\end{align}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\begin{alignat}",
|
|
||||||
right: "\\end{alignat}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\begin{gather}",
|
|
||||||
right: "\\end{gather}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\begin{CD}",
|
|
||||||
right: "\\end{CD}",
|
|
||||||
display: true
|
|
||||||
}, {
|
|
||||||
left: "\\[",
|
|
||||||
right: "\\]",
|
|
||||||
display: true
|
|
||||||
}];
|
|
||||||
optionsCopy.ignoredTags = optionsCopy.ignoredTags || ["script", "noscript", "style", "textarea", "pre", "code", "option"];
|
|
||||||
optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || [];
|
|
||||||
optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different
|
|
||||||
// math elements within a single call to `renderMathInElement`.
|
|
||||||
|
|
||||||
optionsCopy.macros = optionsCopy.macros || {};
|
|
||||||
renderElem(elem, optionsCopy);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { renderMathInElement as default };
|
|
|
@ -1,14 +0,0 @@
|
||||||
/* Force selection of entire .katex/.katex-display blocks, so that we can
|
|
||||||
* copy/paste the entire source code. If you omit this CSS, partial
|
|
||||||
* selections of a formula will work, but will copy the ugly HTML
|
|
||||||
* representation instead of the LaTeX source code. (Full selections will
|
|
||||||
* still produce the LaTeX source code.)
|
|
||||||
*/
|
|
||||||
.katex,
|
|
||||||
.katex-display {
|
|
||||||
-webkit-user-select: all;
|
|
||||||
-moz-user-select: all;
|
|
||||||
-ms-user-select: all;
|
|
||||||
user-select: all;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
(function webpackUniversalModuleDefinition(root, factory) {
|
|
||||||
if(typeof exports === 'object' && typeof module === 'object')
|
|
||||||
module.exports = factory();
|
|
||||||
else if(typeof define === 'function' && define.amd)
|
|
||||||
define([], factory);
|
|
||||||
else {
|
|
||||||
var a = factory();
|
|
||||||
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
|
|
||||||
}
|
|
||||||
})((typeof self !== 'undefined' ? self : this), function() {
|
|
||||||
return /******/ (function() { // webpackBootstrap
|
|
||||||
/******/ "use strict";
|
|
||||||
var __webpack_exports__ = {};
|
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./contrib/copy-tex/katex2tex.js
|
|
||||||
// Set these to how you want inline and display math to be delimited.
|
|
||||||
var defaultCopyDelimiters = {
|
|
||||||
inline: ['$', '$'],
|
|
||||||
// alternative: ['\(', '\)']
|
|
||||||
display: ['$$', '$$'] // alternative: ['\[', '\]']
|
|
||||||
|
|
||||||
}; // Replace .katex elements with their TeX source (<annotation> element).
|
|
||||||
// Modifies fragment in-place. Useful for writing your own 'copy' handler,
|
|
||||||
// as in copy-tex.js.
|
|
||||||
|
|
||||||
var katexReplaceWithTex = function katexReplaceWithTex(fragment, copyDelimiters) {
|
|
||||||
if (copyDelimiters === void 0) {
|
|
||||||
copyDelimiters = defaultCopyDelimiters;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove .katex-html blocks that are preceded by .katex-mathml blocks
|
|
||||||
// (which will get replaced below).
|
|
||||||
var katexHtml = fragment.querySelectorAll('.katex-mathml + .katex-html');
|
|
||||||
|
|
||||||
for (var i = 0; i < katexHtml.length; i++) {
|
|
||||||
var element = katexHtml[i];
|
|
||||||
|
|
||||||
if (element.remove) {
|
|
||||||
element.remove(null);
|
|
||||||
} else {
|
|
||||||
element.parentNode.removeChild(element);
|
|
||||||
}
|
|
||||||
} // Replace .katex-mathml elements with their annotation (TeX source)
|
|
||||||
// descendant, with inline delimiters.
|
|
||||||
|
|
||||||
|
|
||||||
var katexMathml = fragment.querySelectorAll('.katex-mathml');
|
|
||||||
|
|
||||||
for (var _i = 0; _i < katexMathml.length; _i++) {
|
|
||||||
var _element = katexMathml[_i];
|
|
||||||
|
|
||||||
var texSource = _element.querySelector('annotation');
|
|
||||||
|
|
||||||
if (texSource) {
|
|
||||||
if (_element.replaceWith) {
|
|
||||||
_element.replaceWith(texSource);
|
|
||||||
} else {
|
|
||||||
_element.parentNode.replaceChild(texSource, _element);
|
|
||||||
}
|
|
||||||
|
|
||||||
texSource.innerHTML = copyDelimiters.inline[0] + texSource.innerHTML + copyDelimiters.inline[1];
|
|
||||||
}
|
|
||||||
} // Switch display math to display delimiters.
|
|
||||||
|
|
||||||
|
|
||||||
var displays = fragment.querySelectorAll('.katex-display annotation');
|
|
||||||
|
|
||||||
for (var _i2 = 0; _i2 < displays.length; _i2++) {
|
|
||||||
var _element2 = displays[_i2];
|
|
||||||
_element2.innerHTML = copyDelimiters.display[0] + _element2.innerHTML.substr(copyDelimiters.inline[0].length, _element2.innerHTML.length - copyDelimiters.inline[0].length - copyDelimiters.inline[1].length) + copyDelimiters.display[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return fragment;
|
|
||||||
};
|
|
||||||
/* harmony default export */ var katex2tex = (katexReplaceWithTex);
|
|
||||||
;// CONCATENATED MODULE: ./contrib/copy-tex/copy-tex.js
|
|
||||||
// Global copy handler to modify behavior on .katex elements.
|
|
||||||
|
|
||||||
document.addEventListener('copy', function (event) {
|
|
||||||
var selection = window.getSelection();
|
|
||||||
|
|
||||||
if (selection.isCollapsed) {
|
|
||||||
return; // default action OK if selection is empty
|
|
||||||
}
|
|
||||||
|
|
||||||
var fragment = selection.getRangeAt(0).cloneContents();
|
|
||||||
|
|
||||||
if (!fragment.querySelector('.katex-mathml')) {
|
|
||||||
return; // default action OK if no .katex-mathml elements
|
|
||||||
} // Preserve usual HTML copy/paste behavior.
|
|
||||||
|
|
||||||
|
|
||||||
var html = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < fragment.childNodes.length; i++) {
|
|
||||||
html.push(fragment.childNodes[i].outerHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.clipboardData.setData('text/html', html.join('')); // Rewrite plain-text version.
|
|
||||||
|
|
||||||
event.clipboardData.setData('text/plain', katex2tex(fragment).textContent); // Prevent normal copy handling.
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
;// CONCATENATED MODULE: ./contrib/copy-tex/copy-tex.webpack.js
|
|
||||||
/**
|
|
||||||
* This is the webpack entry point for KaTeX. As ECMAScript doesn't support
|
|
||||||
* CSS modules natively, a separate entry point is used.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
__webpack_exports__ = __webpack_exports__["default"];
|
|
||||||
/******/ return __webpack_exports__;
|
|
||||||
/******/ })()
|
|
||||||
;
|
|
||||||
});
|
|
1
public/katex/contrib/copy-tex.min.css
vendored
1
public/katex/contrib/copy-tex.min.css
vendored
|
@ -1 +0,0 @@
|
||||||
.katex,.katex-display{-webkit-user-select:all;-moz-user-select:all;-ms-user-select:all;user-select:all}
|
|
1
public/katex/contrib/copy-tex.min.js
vendored
1
public/katex/contrib/copy-tex.min.js
vendored
|
@ -1 +0,0 @@
|
||||||
!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n=t();for(var l in n)("object"==typeof exports?exports:e)[l]=n[l]}}("undefined"!=typeof self?self:this,(function(){return function(){"use strict";var e={},t={inline:["$","$"],display:["$$","$$"]},n=function(e,n){void 0===n&&(n=t);for(var l=e.querySelectorAll(".katex-mathml + .katex-html"),r=0;r<l.length;r++){var i=l[r];i.remove?i.remove(null):i.parentNode.removeChild(i)}for(var o=e.querySelectorAll(".katex-mathml"),a=0;a<o.length;a++){var d=o[a],f=d.querySelector("annotation");f&&(d.replaceWith?d.replaceWith(f):d.parentNode.replaceChild(f,d),f.innerHTML=n.inline[0]+f.innerHTML+n.inline[1])}for(var c=e.querySelectorAll(".katex-display annotation"),s=0;s<c.length;s++){var p=c[s];p.innerHTML=n.display[0]+p.innerHTML.substr(n.inline[0].length,p.innerHTML.length-n.inline[0].length-n.inline[1].length)+n.display[1]}return e};return document.addEventListener("copy",(function(e){var t=window.getSelection();if(!t.isCollapsed){var l=t.getRangeAt(0).cloneContents();if(l.querySelector(".katex-mathml")){for(var r=[],i=0;i<l.childNodes.length;i++)r.push(l.childNodes[i].outerHTML);e.clipboardData.setData("text/html",r.join("")),e.clipboardData.setData("text/plain",n(l).textContent),e.preventDefault()}}})),e=e.default}()}));
|
|
|
@ -1,86 +0,0 @@
|
||||||
// Set these to how you want inline and display math to be delimited.
|
|
||||||
var defaultCopyDelimiters = {
|
|
||||||
inline: ['$', '$'],
|
|
||||||
// alternative: ['\(', '\)']
|
|
||||||
display: ['$$', '$$'] // alternative: ['\[', '\]']
|
|
||||||
|
|
||||||
}; // Replace .katex elements with their TeX source (<annotation> element).
|
|
||||||
// Modifies fragment in-place. Useful for writing your own 'copy' handler,
|
|
||||||
// as in copy-tex.js.
|
|
||||||
|
|
||||||
var katexReplaceWithTex = function katexReplaceWithTex(fragment, copyDelimiters) {
|
|
||||||
if (copyDelimiters === void 0) {
|
|
||||||
copyDelimiters = defaultCopyDelimiters;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove .katex-html blocks that are preceded by .katex-mathml blocks
|
|
||||||
// (which will get replaced below).
|
|
||||||
var katexHtml = fragment.querySelectorAll('.katex-mathml + .katex-html');
|
|
||||||
|
|
||||||
for (var i = 0; i < katexHtml.length; i++) {
|
|
||||||
var element = katexHtml[i];
|
|
||||||
|
|
||||||
if (element.remove) {
|
|
||||||
element.remove(null);
|
|
||||||
} else {
|
|
||||||
element.parentNode.removeChild(element);
|
|
||||||
}
|
|
||||||
} // Replace .katex-mathml elements with their annotation (TeX source)
|
|
||||||
// descendant, with inline delimiters.
|
|
||||||
|
|
||||||
|
|
||||||
var katexMathml = fragment.querySelectorAll('.katex-mathml');
|
|
||||||
|
|
||||||
for (var _i = 0; _i < katexMathml.length; _i++) {
|
|
||||||
var _element = katexMathml[_i];
|
|
||||||
|
|
||||||
var texSource = _element.querySelector('annotation');
|
|
||||||
|
|
||||||
if (texSource) {
|
|
||||||
if (_element.replaceWith) {
|
|
||||||
_element.replaceWith(texSource);
|
|
||||||
} else {
|
|
||||||
_element.parentNode.replaceChild(texSource, _element);
|
|
||||||
}
|
|
||||||
|
|
||||||
texSource.innerHTML = copyDelimiters.inline[0] + texSource.innerHTML + copyDelimiters.inline[1];
|
|
||||||
}
|
|
||||||
} // Switch display math to display delimiters.
|
|
||||||
|
|
||||||
|
|
||||||
var displays = fragment.querySelectorAll('.katex-display annotation');
|
|
||||||
|
|
||||||
for (var _i2 = 0; _i2 < displays.length; _i2++) {
|
|
||||||
var _element2 = displays[_i2];
|
|
||||||
_element2.innerHTML = copyDelimiters.display[0] + _element2.innerHTML.substr(copyDelimiters.inline[0].length, _element2.innerHTML.length - copyDelimiters.inline[0].length - copyDelimiters.inline[1].length) + copyDelimiters.display[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return fragment;
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('copy', function (event) {
|
|
||||||
var selection = window.getSelection();
|
|
||||||
|
|
||||||
if (selection.isCollapsed) {
|
|
||||||
return; // default action OK if selection is empty
|
|
||||||
}
|
|
||||||
|
|
||||||
var fragment = selection.getRangeAt(0).cloneContents();
|
|
||||||
|
|
||||||
if (!fragment.querySelector('.katex-mathml')) {
|
|
||||||
return; // default action OK if no .katex-mathml elements
|
|
||||||
} // Preserve usual HTML copy/paste behavior.
|
|
||||||
|
|
||||||
|
|
||||||
var html = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < fragment.childNodes.length; i++) {
|
|
||||||
html.push(fragment.childNodes[i].outerHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.clipboardData.setData('text/html', html.join('')); // Rewrite plain-text version.
|
|
||||||
|
|
||||||
event.clipboardData.setData('text/plain', katexReplaceWithTex(fragment).textContent); // Prevent normal copy handling.
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
|
@ -1,112 +0,0 @@
|
||||||
(function webpackUniversalModuleDefinition(root, factory) {
|
|
||||||
if(typeof exports === 'object' && typeof module === 'object')
|
|
||||||
module.exports = factory(require("katex"));
|
|
||||||
else if(typeof define === 'function' && define.amd)
|
|
||||||
define(["katex"], factory);
|
|
||||||
else {
|
|
||||||
var a = typeof exports === 'object' ? factory(require("katex")) : factory(root["katex"]);
|
|
||||||
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
|
|
||||||
}
|
|
||||||
})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__771__) {
|
|
||||||
return /******/ (function() { // webpackBootstrap
|
|
||||||
/******/ "use strict";
|
|
||||||
/******/ var __webpack_modules__ = ({
|
|
||||||
|
|
||||||
/***/ 771:
|
|
||||||
/***/ (function(module) {
|
|
||||||
|
|
||||||
module.exports = __WEBPACK_EXTERNAL_MODULE__771__;
|
|
||||||
|
|
||||||
/***/ })
|
|
||||||
|
|
||||||
/******/ });
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ // The module cache
|
|
||||||
/******/ var __webpack_module_cache__ = {};
|
|
||||||
/******/
|
|
||||||
/******/ // The require function
|
|
||||||
/******/ function __webpack_require__(moduleId) {
|
|
||||||
/******/ // Check if module is in cache
|
|
||||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
||||||
/******/ if (cachedModule !== undefined) {
|
|
||||||
/******/ return cachedModule.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/ // Create a new module (and put it into the cache)
|
|
||||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
||||||
/******/ // no module.id needed
|
|
||||||
/******/ // no module.loaded needed
|
|
||||||
/******/ exports: {}
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // Execute the module function
|
|
||||||
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
||||||
/******/
|
|
||||||
/******/ // Return the exports of the module
|
|
||||||
/******/ return module.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ /* webpack/runtime/compat get default export */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
||||||
/******/ __webpack_require__.n = function(module) {
|
|
||||||
/******/ var getter = module && module.__esModule ?
|
|
||||||
/******/ function() { return module['default']; } :
|
|
||||||
/******/ function() { return module; };
|
|
||||||
/******/ __webpack_require__.d(getter, { a: getter });
|
|
||||||
/******/ return getter;
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/define property getters */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // define getter functions for harmony exports
|
|
||||||
/******/ __webpack_require__.d = function(exports, definition) {
|
|
||||||
/******/ for(var key in definition) {
|
|
||||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
||||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
var __webpack_exports__ = {};
|
|
||||||
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
|
|
||||||
!function() {
|
|
||||||
/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(771);
|
|
||||||
/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(katex__WEBPACK_IMPORTED_MODULE_0__);
|
|
||||||
|
|
||||||
var scripts = document.body.getElementsByTagName("script");
|
|
||||||
scripts = Array.prototype.slice.call(scripts);
|
|
||||||
scripts.forEach(function (script) {
|
|
||||||
if (!script.type || !script.type.match(/math\/tex/i)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var display = script.type.match(/mode\s*=\s*display(;|\s|\n|$)/) != null;
|
|
||||||
var katexElement = document.createElement(display ? "div" : "span");
|
|
||||||
katexElement.setAttribute("class", display ? "equation" : "inline-equation");
|
|
||||||
|
|
||||||
try {
|
|
||||||
katex__WEBPACK_IMPORTED_MODULE_0___default().render(script.text, katexElement, {
|
|
||||||
displayMode: display
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
//console.error(err); linter doesn't like this
|
|
||||||
katexElement.textContent = script.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
script.parentNode.replaceChild(katexElement, script);
|
|
||||||
});
|
|
||||||
}();
|
|
||||||
__webpack_exports__ = __webpack_exports__["default"];
|
|
||||||
/******/ return __webpack_exports__;
|
|
||||||
/******/ })()
|
|
||||||
;
|
|
||||||
});
|
|
|
@ -1 +0,0 @@
|
||||||
!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t(require("katex"));else if("function"==typeof define&&define.amd)define(["katex"],t);else{var r="object"==typeof exports?t(require("katex")):t(e.katex);for(var n in r)("object"==typeof exports?exports:e)[n]=r[n]}}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},r={};function n(e){var o=r[e];if(void 0!==o)return o.exports;var i=r[e]={exports:{}};return t[e](i,i.exports,n),i.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var o,i,a,u={};return o=n(771),i=n.n(o),a=document.body.getElementsByTagName("script"),(a=Array.prototype.slice.call(a)).forEach((function(e){if(!e.type||!e.type.match(/math\/tex/i))return-1;var t=null!=e.type.match(/mode\s*=\s*display(;|\s|\n|$)/),r=document.createElement(t?"div":"span");r.setAttribute("class",t?"equation":"inline-equation");try{i().render(e.text,r,{displayMode:t})}catch(t){r.textContent=e.text}e.parentNode.replaceChild(r,e)})),u=u.default}()}));
|
|
|
@ -1,24 +0,0 @@
|
||||||
import katex from '../katex.mjs';
|
|
||||||
|
|
||||||
var scripts = document.body.getElementsByTagName("script");
|
|
||||||
scripts = Array.prototype.slice.call(scripts);
|
|
||||||
scripts.forEach(function (script) {
|
|
||||||
if (!script.type || !script.type.match(/math\/tex/i)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var display = script.type.match(/mode\s*=\s*display(;|\s|\n|$)/) != null;
|
|
||||||
var katexElement = document.createElement(display ? "div" : "span");
|
|
||||||
katexElement.setAttribute("class", display ? "equation" : "inline-equation");
|
|
||||||
|
|
||||||
try {
|
|
||||||
katex.render(script.text, katexElement, {
|
|
||||||
displayMode: display
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
//console.error(err); linter doesn't like this
|
|
||||||
katexElement.textContent = script.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
script.parentNode.replaceChild(katexElement, script);
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load diff
1
public/katex/contrib/mhchem.min.js
vendored
1
public/katex/contrib/mhchem.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
@ -1,875 +0,0 @@
|
||||||
(function webpackUniversalModuleDefinition(root, factory) {
|
|
||||||
if(typeof exports === 'object' && typeof module === 'object')
|
|
||||||
module.exports = factory(require("katex"));
|
|
||||||
else if(typeof define === 'function' && define.amd)
|
|
||||||
define(["katex"], factory);
|
|
||||||
else {
|
|
||||||
var a = typeof exports === 'object' ? factory(require("katex")) : factory(root["katex"]);
|
|
||||||
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
|
|
||||||
}
|
|
||||||
})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__771__) {
|
|
||||||
return /******/ (function() { // webpackBootstrap
|
|
||||||
/******/ "use strict";
|
|
||||||
/******/ var __webpack_modules__ = ({
|
|
||||||
|
|
||||||
/***/ 771:
|
|
||||||
/***/ (function(module) {
|
|
||||||
|
|
||||||
module.exports = __WEBPACK_EXTERNAL_MODULE__771__;
|
|
||||||
|
|
||||||
/***/ })
|
|
||||||
|
|
||||||
/******/ });
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ // The module cache
|
|
||||||
/******/ var __webpack_module_cache__ = {};
|
|
||||||
/******/
|
|
||||||
/******/ // The require function
|
|
||||||
/******/ function __webpack_require__(moduleId) {
|
|
||||||
/******/ // Check if module is in cache
|
|
||||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
||||||
/******/ if (cachedModule !== undefined) {
|
|
||||||
/******/ return cachedModule.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/ // Create a new module (and put it into the cache)
|
|
||||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
||||||
/******/ // no module.id needed
|
|
||||||
/******/ // no module.loaded needed
|
|
||||||
/******/ exports: {}
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // Execute the module function
|
|
||||||
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
||||||
/******/
|
|
||||||
/******/ // Return the exports of the module
|
|
||||||
/******/ return module.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ /* webpack/runtime/compat get default export */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
||||||
/******/ __webpack_require__.n = function(module) {
|
|
||||||
/******/ var getter = module && module.__esModule ?
|
|
||||||
/******/ function() { return module['default']; } :
|
|
||||||
/******/ function() { return module; };
|
|
||||||
/******/ __webpack_require__.d(getter, { a: getter });
|
|
||||||
/******/ return getter;
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/define property getters */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // define getter functions for harmony exports
|
|
||||||
/******/ __webpack_require__.d = function(exports, definition) {
|
|
||||||
/******/ for(var key in definition) {
|
|
||||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
||||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
var __webpack_exports__ = {};
|
|
||||||
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
|
|
||||||
!function() {
|
|
||||||
/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(771);
|
|
||||||
/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(katex__WEBPACK_IMPORTED_MODULE_0__);
|
|
||||||
/**
|
|
||||||
* renderA11yString returns a readable string.
|
|
||||||
*
|
|
||||||
* In some cases the string will have the proper semantic math
|
|
||||||
* meaning,:
|
|
||||||
* renderA11yString("\\frac{1}{2}"")
|
|
||||||
* -> "start fraction, 1, divided by, 2, end fraction"
|
|
||||||
*
|
|
||||||
* However, other cases do not:
|
|
||||||
* renderA11yString("f(x) = x^2")
|
|
||||||
* -> "f, left parenthesis, x, right parenthesis, equals, x, squared"
|
|
||||||
*
|
|
||||||
* The commas in the string aim to increase ease of understanding
|
|
||||||
* when read by a screenreader.
|
|
||||||
*/
|
|
||||||
// NOTE: since we're importing types here these files won't actually be
|
|
||||||
// included in the build.
|
|
||||||
// $FlowIgnore: we import the types directly anyways
|
|
||||||
|
|
||||||
var stringMap = {
|
|
||||||
"(": "left parenthesis",
|
|
||||||
")": "right parenthesis",
|
|
||||||
"[": "open bracket",
|
|
||||||
"]": "close bracket",
|
|
||||||
"\\{": "left brace",
|
|
||||||
"\\}": "right brace",
|
|
||||||
"\\lvert": "open vertical bar",
|
|
||||||
"\\rvert": "close vertical bar",
|
|
||||||
"|": "vertical bar",
|
|
||||||
"\\uparrow": "up arrow",
|
|
||||||
"\\Uparrow": "up arrow",
|
|
||||||
"\\downarrow": "down arrow",
|
|
||||||
"\\Downarrow": "down arrow",
|
|
||||||
"\\updownarrow": "up down arrow",
|
|
||||||
"\\leftarrow": "left arrow",
|
|
||||||
"\\Leftarrow": "left arrow",
|
|
||||||
"\\rightarrow": "right arrow",
|
|
||||||
"\\Rightarrow": "right arrow",
|
|
||||||
"\\langle": "open angle",
|
|
||||||
"\\rangle": "close angle",
|
|
||||||
"\\lfloor": "open floor",
|
|
||||||
"\\rfloor": "close floor",
|
|
||||||
"\\int": "integral",
|
|
||||||
"\\intop": "integral",
|
|
||||||
"\\lim": "limit",
|
|
||||||
"\\ln": "natural log",
|
|
||||||
"\\log": "log",
|
|
||||||
"\\sin": "sine",
|
|
||||||
"\\cos": "cosine",
|
|
||||||
"\\tan": "tangent",
|
|
||||||
"\\cot": "cotangent",
|
|
||||||
"\\sum": "sum",
|
|
||||||
"/": "slash",
|
|
||||||
",": "comma",
|
|
||||||
".": "point",
|
|
||||||
"-": "negative",
|
|
||||||
"+": "plus",
|
|
||||||
"~": "tilde",
|
|
||||||
":": "colon",
|
|
||||||
"?": "question mark",
|
|
||||||
"'": "apostrophe",
|
|
||||||
"\\%": "percent",
|
|
||||||
" ": "space",
|
|
||||||
"\\ ": "space",
|
|
||||||
"\\$": "dollar sign",
|
|
||||||
"\\angle": "angle",
|
|
||||||
"\\degree": "degree",
|
|
||||||
"\\circ": "circle",
|
|
||||||
"\\vec": "vector",
|
|
||||||
"\\triangle": "triangle",
|
|
||||||
"\\pi": "pi",
|
|
||||||
"\\prime": "prime",
|
|
||||||
"\\infty": "infinity",
|
|
||||||
"\\alpha": "alpha",
|
|
||||||
"\\beta": "beta",
|
|
||||||
"\\gamma": "gamma",
|
|
||||||
"\\omega": "omega",
|
|
||||||
"\\theta": "theta",
|
|
||||||
"\\sigma": "sigma",
|
|
||||||
"\\lambda": "lambda",
|
|
||||||
"\\tau": "tau",
|
|
||||||
"\\Delta": "delta",
|
|
||||||
"\\delta": "delta",
|
|
||||||
"\\mu": "mu",
|
|
||||||
"\\rho": "rho",
|
|
||||||
"\\nabla": "del",
|
|
||||||
"\\ell": "ell",
|
|
||||||
"\\ldots": "dots",
|
|
||||||
// TODO: add entries for all accents
|
|
||||||
"\\hat": "hat",
|
|
||||||
"\\acute": "acute"
|
|
||||||
};
|
|
||||||
var powerMap = {
|
|
||||||
"prime": "prime",
|
|
||||||
"degree": "degrees",
|
|
||||||
"circle": "degrees",
|
|
||||||
"2": "squared",
|
|
||||||
"3": "cubed"
|
|
||||||
};
|
|
||||||
var openMap = {
|
|
||||||
"|": "open vertical bar",
|
|
||||||
".": ""
|
|
||||||
};
|
|
||||||
var closeMap = {
|
|
||||||
"|": "close vertical bar",
|
|
||||||
".": ""
|
|
||||||
};
|
|
||||||
var binMap = {
|
|
||||||
"+": "plus",
|
|
||||||
"-": "minus",
|
|
||||||
"\\pm": "plus minus",
|
|
||||||
"\\cdot": "dot",
|
|
||||||
"*": "times",
|
|
||||||
"/": "divided by",
|
|
||||||
"\\times": "times",
|
|
||||||
"\\div": "divided by",
|
|
||||||
"\\circ": "circle",
|
|
||||||
"\\bullet": "bullet"
|
|
||||||
};
|
|
||||||
var relMap = {
|
|
||||||
"=": "equals",
|
|
||||||
"\\approx": "approximately equals",
|
|
||||||
"≠": "does not equal",
|
|
||||||
"\\geq": "is greater than or equal to",
|
|
||||||
"\\ge": "is greater than or equal to",
|
|
||||||
"\\leq": "is less than or equal to",
|
|
||||||
"\\le": "is less than or equal to",
|
|
||||||
">": "is greater than",
|
|
||||||
"<": "is less than",
|
|
||||||
"\\leftarrow": "left arrow",
|
|
||||||
"\\Leftarrow": "left arrow",
|
|
||||||
"\\rightarrow": "right arrow",
|
|
||||||
"\\Rightarrow": "right arrow",
|
|
||||||
":": "colon"
|
|
||||||
};
|
|
||||||
var accentUnderMap = {
|
|
||||||
"\\underleftarrow": "left arrow",
|
|
||||||
"\\underrightarrow": "right arrow",
|
|
||||||
"\\underleftrightarrow": "left-right arrow",
|
|
||||||
"\\undergroup": "group",
|
|
||||||
"\\underlinesegment": "line segment",
|
|
||||||
"\\utilde": "tilde"
|
|
||||||
};
|
|
||||||
|
|
||||||
var buildString = function buildString(str, type, a11yStrings) {
|
|
||||||
if (!str) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret;
|
|
||||||
|
|
||||||
if (type === "open") {
|
|
||||||
ret = str in openMap ? openMap[str] : stringMap[str] || str;
|
|
||||||
} else if (type === "close") {
|
|
||||||
ret = str in closeMap ? closeMap[str] : stringMap[str] || str;
|
|
||||||
} else if (type === "bin") {
|
|
||||||
ret = binMap[str] || str;
|
|
||||||
} else if (type === "rel") {
|
|
||||||
ret = relMap[str] || str;
|
|
||||||
} else {
|
|
||||||
ret = stringMap[str] || str;
|
|
||||||
} // If the text to add is a number and there is already a string
|
|
||||||
// in the list and the last string is a number then we should
|
|
||||||
// combine them into a single number
|
|
||||||
|
|
||||||
|
|
||||||
if (/^\d+$/.test(ret) && a11yStrings.length > 0 && // TODO(kevinb): check that the last item in a11yStrings is a string
|
|
||||||
// I think we might be able to drop the nested arrays, which would make
|
|
||||||
// this easier to type
|
|
||||||
// $FlowFixMe
|
|
||||||
/^\d+$/.test(a11yStrings[a11yStrings.length - 1])) {
|
|
||||||
a11yStrings[a11yStrings.length - 1] += ret;
|
|
||||||
} else if (ret) {
|
|
||||||
a11yStrings.push(ret);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var buildRegion = function buildRegion(a11yStrings, callback) {
|
|
||||||
var regionStrings = [];
|
|
||||||
a11yStrings.push(regionStrings);
|
|
||||||
callback(regionStrings);
|
|
||||||
};
|
|
||||||
|
|
||||||
var handleObject = function handleObject(tree, a11yStrings, atomType) {
|
|
||||||
// Everything else is assumed to be an object...
|
|
||||||
switch (tree.type) {
|
|
||||||
case "accent":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, function (a11yStrings) {
|
|
||||||
buildA11yStrings(tree.base, a11yStrings, atomType);
|
|
||||||
a11yStrings.push("with");
|
|
||||||
buildString(tree.label, "normal", a11yStrings);
|
|
||||||
a11yStrings.push("on top");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "accentUnder":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, function (a11yStrings) {
|
|
||||||
buildA11yStrings(tree.base, a11yStrings, atomType);
|
|
||||||
a11yStrings.push("with");
|
|
||||||
buildString(accentUnderMap[tree.label], "normal", a11yStrings);
|
|
||||||
a11yStrings.push("underneath");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "accent-token":
|
|
||||||
{
|
|
||||||
// Used internally by accent symbols.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "atom":
|
|
||||||
{
|
|
||||||
var text = tree.text;
|
|
||||||
|
|
||||||
switch (tree.family) {
|
|
||||||
case "bin":
|
|
||||||
{
|
|
||||||
buildString(text, "bin", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "close":
|
|
||||||
{
|
|
||||||
buildString(text, "close", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// TODO(kevinb): figure out what should be done for inner
|
|
||||||
|
|
||||||
case "inner":
|
|
||||||
{
|
|
||||||
buildString(tree.text, "inner", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "open":
|
|
||||||
{
|
|
||||||
buildString(text, "open", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "punct":
|
|
||||||
{
|
|
||||||
buildString(text, "punct", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "rel":
|
|
||||||
{
|
|
||||||
buildString(text, "rel", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
tree.family;
|
|
||||||
throw new Error("\"" + tree.family + "\" is not a valid atom type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "color":
|
|
||||||
{
|
|
||||||
var color = tree.color.replace(/katex-/, "");
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start color " + color);
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end color " + color);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "color-token":
|
|
||||||
{
|
|
||||||
// Used by \color, \colorbox, and \fcolorbox but not directly rendered.
|
|
||||||
// It's a leaf node and has no children so just break.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "delimsizing":
|
|
||||||
{
|
|
||||||
if (tree.delim && tree.delim !== ".") {
|
|
||||||
buildString(tree.delim, "normal", a11yStrings);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "genfrac":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
// genfrac can have unbalanced delimiters
|
|
||||||
var leftDelim = tree.leftDelim,
|
|
||||||
rightDelim = tree.rightDelim; // NOTE: Not sure if this is a safe assumption
|
|
||||||
// hasBarLine true -> fraction, false -> binomial
|
|
||||||
|
|
||||||
if (tree.hasBarLine) {
|
|
||||||
regionStrings.push("start fraction");
|
|
||||||
leftDelim && buildString(leftDelim, "open", regionStrings);
|
|
||||||
buildA11yStrings(tree.numer, regionStrings, atomType);
|
|
||||||
regionStrings.push("divided by");
|
|
||||||
buildA11yStrings(tree.denom, regionStrings, atomType);
|
|
||||||
rightDelim && buildString(rightDelim, "close", regionStrings);
|
|
||||||
regionStrings.push("end fraction");
|
|
||||||
} else {
|
|
||||||
regionStrings.push("start binomial");
|
|
||||||
leftDelim && buildString(leftDelim, "open", regionStrings);
|
|
||||||
buildA11yStrings(tree.numer, regionStrings, atomType);
|
|
||||||
regionStrings.push("over");
|
|
||||||
buildA11yStrings(tree.denom, regionStrings, atomType);
|
|
||||||
rightDelim && buildString(rightDelim, "close", regionStrings);
|
|
||||||
regionStrings.push("end binomial");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "hbox":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "kern":
|
|
||||||
{
|
|
||||||
// No op: we don't attempt to present kerning information
|
|
||||||
// to the screen reader.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "leftright":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
buildString(tree.left, "open", regionStrings);
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
buildString(tree.right, "close", regionStrings);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "leftright-right":
|
|
||||||
{
|
|
||||||
// TODO: double check that this is a no-op
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "lap":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "mathord":
|
|
||||||
{
|
|
||||||
buildString(tree.text, "normal", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "op":
|
|
||||||
{
|
|
||||||
var body = tree.body,
|
|
||||||
name = tree.name;
|
|
||||||
|
|
||||||
if (body) {
|
|
||||||
buildA11yStrings(body, a11yStrings, atomType);
|
|
||||||
} else if (name) {
|
|
||||||
buildString(name, "normal", a11yStrings);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "op-token":
|
|
||||||
{
|
|
||||||
// Used internally by operator symbols.
|
|
||||||
buildString(tree.text, atomType, a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ordgroup":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "overline":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, function (a11yStrings) {
|
|
||||||
a11yStrings.push("start overline");
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
a11yStrings.push("end overline");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "phantom":
|
|
||||||
{
|
|
||||||
a11yStrings.push("empty space");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "raisebox":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "rule":
|
|
||||||
{
|
|
||||||
a11yStrings.push("rectangle");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "sizing":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "spacing":
|
|
||||||
{
|
|
||||||
a11yStrings.push("space");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "styling":
|
|
||||||
{
|
|
||||||
// We ignore the styling and just pass through the contents
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "sqrt":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
var body = tree.body,
|
|
||||||
index = tree.index;
|
|
||||||
|
|
||||||
if (index) {
|
|
||||||
var indexString = flatten(buildA11yStrings(index, [], atomType)).join(",");
|
|
||||||
|
|
||||||
if (indexString === "3") {
|
|
||||||
regionStrings.push("cube root of");
|
|
||||||
buildA11yStrings(body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end cube root");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
regionStrings.push("root");
|
|
||||||
regionStrings.push("start index");
|
|
||||||
buildA11yStrings(index, regionStrings, atomType);
|
|
||||||
regionStrings.push("end index");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
regionStrings.push("square root of");
|
|
||||||
buildA11yStrings(body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end square root");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "supsub":
|
|
||||||
{
|
|
||||||
var base = tree.base,
|
|
||||||
sub = tree.sub,
|
|
||||||
sup = tree.sup;
|
|
||||||
var isLog = false;
|
|
||||||
|
|
||||||
if (base) {
|
|
||||||
buildA11yStrings(base, a11yStrings, atomType);
|
|
||||||
isLog = base.type === "op" && base.name === "\\log";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub) {
|
|
||||||
var regionName = isLog ? "base" : "subscript";
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start " + regionName);
|
|
||||||
buildA11yStrings(sub, regionStrings, atomType);
|
|
||||||
regionStrings.push("end " + regionName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sup) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
var supString = flatten(buildA11yStrings(sup, [], atomType)).join(",");
|
|
||||||
|
|
||||||
if (supString in powerMap) {
|
|
||||||
regionStrings.push(powerMap[supString]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
regionStrings.push("start superscript");
|
|
||||||
buildA11yStrings(sup, regionStrings, atomType);
|
|
||||||
regionStrings.push("end superscript");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "text":
|
|
||||||
{
|
|
||||||
// TODO: handle other fonts
|
|
||||||
if (tree.font === "\\textbf") {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start bold text");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end bold text");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start text");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end text");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "textord":
|
|
||||||
{
|
|
||||||
buildString(tree.text, atomType, a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "smash":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "enclose":
|
|
||||||
{
|
|
||||||
// TODO: create a map for these.
|
|
||||||
// TODO: differentiate between a body with a single atom, e.g.
|
|
||||||
// "cancel a" instead of "start cancel, a, end cancel"
|
|
||||||
if (/cancel/.test(tree.label)) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start cancel");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end cancel");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
} else if (/box/.test(tree.label)) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start box");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end box");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
} else if (/sout/.test(tree.label)) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start strikeout");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end strikeout");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
} else if (/phase/.test(tree.label)) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start phase angle");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end phase angle");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("KaTeX-a11y: enclose node with " + tree.label + " not supported yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "vcenter":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "vphantom":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: vphantom not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "hphantom":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: hphantom not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "operatorname":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "array":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: array not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "raw":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: raw not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "size":
|
|
||||||
{
|
|
||||||
// Although there are nodes of type "size" in the parse tree, they have
|
|
||||||
// no semantic meaning and should be ignored.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "url":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: url not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "tag":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: tag not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "verb":
|
|
||||||
{
|
|
||||||
buildString("start verbatim", "normal", a11yStrings);
|
|
||||||
buildString(tree.body, "normal", a11yStrings);
|
|
||||||
buildString("end verbatim", "normal", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "environment":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: environment not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "horizBrace":
|
|
||||||
{
|
|
||||||
buildString("start " + tree.label.slice(1), "normal", a11yStrings);
|
|
||||||
buildA11yStrings(tree.base, a11yStrings, atomType);
|
|
||||||
buildString("end " + tree.label.slice(1), "normal", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "infix":
|
|
||||||
{
|
|
||||||
// All infix nodes are replace with other nodes.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "includegraphics":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: includegraphics not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "font":
|
|
||||||
{
|
|
||||||
// TODO: callout the start/end of specific fonts
|
|
||||||
// TODO: map \BBb{N} to "the naturals" or something like that
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "href":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: href not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "cr":
|
|
||||||
{
|
|
||||||
// This is used by environments.
|
|
||||||
throw new Error("KaTeX-a11y: cr not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "underline":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, function (a11yStrings) {
|
|
||||||
a11yStrings.push("start underline");
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
a11yStrings.push("end underline");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "xArrow":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: xArrow not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "cdlabel":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: cdlabel not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "cdlabelparent":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: cdlabelparent not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "mclass":
|
|
||||||
{
|
|
||||||
// \neq and \ne are macros so we let "htmlmathml" render the mathmal
|
|
||||||
// side of things and extract the text from that.
|
|
||||||
var _atomType = tree.mclass.slice(1); // $FlowFixMe: drop the leading "m" from the values in mclass
|
|
||||||
|
|
||||||
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, _atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "mathchoice":
|
|
||||||
{
|
|
||||||
// TODO: track which which style we're using, e.g. dispaly, text, etc.
|
|
||||||
// default to text style if even that may not be the correct style
|
|
||||||
buildA11yStrings(tree.text, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "htmlmathml":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.mathml, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "middle":
|
|
||||||
{
|
|
||||||
buildString(tree.delim, atomType, a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "internal":
|
|
||||||
{
|
|
||||||
// internal nodes are never included in the parse tree
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "html":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
tree.type;
|
|
||||||
throw new Error("KaTeX a11y un-recognized type: " + tree.type);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) {
|
|
||||||
if (a11yStrings === void 0) {
|
|
||||||
a11yStrings = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree instanceof Array) {
|
|
||||||
for (var i = 0; i < tree.length; i++) {
|
|
||||||
buildA11yStrings(tree[i], a11yStrings, atomType);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handleObject(tree, a11yStrings, atomType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return a11yStrings;
|
|
||||||
};
|
|
||||||
|
|
||||||
var flatten = function flatten(array) {
|
|
||||||
var result = [];
|
|
||||||
array.forEach(function (item) {
|
|
||||||
if (item instanceof Array) {
|
|
||||||
result = result.concat(flatten(item));
|
|
||||||
} else {
|
|
||||||
result.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
var renderA11yString = function renderA11yString(text, settings) {
|
|
||||||
var tree = katex__WEBPACK_IMPORTED_MODULE_0___default().__parse(text, settings);
|
|
||||||
|
|
||||||
var a11yStrings = buildA11yStrings(tree, [], "normal");
|
|
||||||
return flatten(a11yStrings).join(", ");
|
|
||||||
};
|
|
||||||
|
|
||||||
/* harmony default export */ __webpack_exports__["default"] = (renderA11yString);
|
|
||||||
}();
|
|
||||||
__webpack_exports__ = __webpack_exports__["default"];
|
|
||||||
/******/ return __webpack_exports__;
|
|
||||||
/******/ })()
|
|
||||||
;
|
|
||||||
});
|
|
File diff suppressed because one or more lines are too long
|
@ -1,794 +0,0 @@
|
||||||
import katex from '../katex.mjs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* renderA11yString returns a readable string.
|
|
||||||
*
|
|
||||||
* In some cases the string will have the proper semantic math
|
|
||||||
* meaning,:
|
|
||||||
* renderA11yString("\\frac{1}{2}"")
|
|
||||||
* -> "start fraction, 1, divided by, 2, end fraction"
|
|
||||||
*
|
|
||||||
* However, other cases do not:
|
|
||||||
* renderA11yString("f(x) = x^2")
|
|
||||||
* -> "f, left parenthesis, x, right parenthesis, equals, x, squared"
|
|
||||||
*
|
|
||||||
* The commas in the string aim to increase ease of understanding
|
|
||||||
* when read by a screenreader.
|
|
||||||
*/
|
|
||||||
var stringMap = {
|
|
||||||
"(": "left parenthesis",
|
|
||||||
")": "right parenthesis",
|
|
||||||
"[": "open bracket",
|
|
||||||
"]": "close bracket",
|
|
||||||
"\\{": "left brace",
|
|
||||||
"\\}": "right brace",
|
|
||||||
"\\lvert": "open vertical bar",
|
|
||||||
"\\rvert": "close vertical bar",
|
|
||||||
"|": "vertical bar",
|
|
||||||
"\\uparrow": "up arrow",
|
|
||||||
"\\Uparrow": "up arrow",
|
|
||||||
"\\downarrow": "down arrow",
|
|
||||||
"\\Downarrow": "down arrow",
|
|
||||||
"\\updownarrow": "up down arrow",
|
|
||||||
"\\leftarrow": "left arrow",
|
|
||||||
"\\Leftarrow": "left arrow",
|
|
||||||
"\\rightarrow": "right arrow",
|
|
||||||
"\\Rightarrow": "right arrow",
|
|
||||||
"\\langle": "open angle",
|
|
||||||
"\\rangle": "close angle",
|
|
||||||
"\\lfloor": "open floor",
|
|
||||||
"\\rfloor": "close floor",
|
|
||||||
"\\int": "integral",
|
|
||||||
"\\intop": "integral",
|
|
||||||
"\\lim": "limit",
|
|
||||||
"\\ln": "natural log",
|
|
||||||
"\\log": "log",
|
|
||||||
"\\sin": "sine",
|
|
||||||
"\\cos": "cosine",
|
|
||||||
"\\tan": "tangent",
|
|
||||||
"\\cot": "cotangent",
|
|
||||||
"\\sum": "sum",
|
|
||||||
"/": "slash",
|
|
||||||
",": "comma",
|
|
||||||
".": "point",
|
|
||||||
"-": "negative",
|
|
||||||
"+": "plus",
|
|
||||||
"~": "tilde",
|
|
||||||
":": "colon",
|
|
||||||
"?": "question mark",
|
|
||||||
"'": "apostrophe",
|
|
||||||
"\\%": "percent",
|
|
||||||
" ": "space",
|
|
||||||
"\\ ": "space",
|
|
||||||
"\\$": "dollar sign",
|
|
||||||
"\\angle": "angle",
|
|
||||||
"\\degree": "degree",
|
|
||||||
"\\circ": "circle",
|
|
||||||
"\\vec": "vector",
|
|
||||||
"\\triangle": "triangle",
|
|
||||||
"\\pi": "pi",
|
|
||||||
"\\prime": "prime",
|
|
||||||
"\\infty": "infinity",
|
|
||||||
"\\alpha": "alpha",
|
|
||||||
"\\beta": "beta",
|
|
||||||
"\\gamma": "gamma",
|
|
||||||
"\\omega": "omega",
|
|
||||||
"\\theta": "theta",
|
|
||||||
"\\sigma": "sigma",
|
|
||||||
"\\lambda": "lambda",
|
|
||||||
"\\tau": "tau",
|
|
||||||
"\\Delta": "delta",
|
|
||||||
"\\delta": "delta",
|
|
||||||
"\\mu": "mu",
|
|
||||||
"\\rho": "rho",
|
|
||||||
"\\nabla": "del",
|
|
||||||
"\\ell": "ell",
|
|
||||||
"\\ldots": "dots",
|
|
||||||
// TODO: add entries for all accents
|
|
||||||
"\\hat": "hat",
|
|
||||||
"\\acute": "acute"
|
|
||||||
};
|
|
||||||
var powerMap = {
|
|
||||||
"prime": "prime",
|
|
||||||
"degree": "degrees",
|
|
||||||
"circle": "degrees",
|
|
||||||
"2": "squared",
|
|
||||||
"3": "cubed"
|
|
||||||
};
|
|
||||||
var openMap = {
|
|
||||||
"|": "open vertical bar",
|
|
||||||
".": ""
|
|
||||||
};
|
|
||||||
var closeMap = {
|
|
||||||
"|": "close vertical bar",
|
|
||||||
".": ""
|
|
||||||
};
|
|
||||||
var binMap = {
|
|
||||||
"+": "plus",
|
|
||||||
"-": "minus",
|
|
||||||
"\\pm": "plus minus",
|
|
||||||
"\\cdot": "dot",
|
|
||||||
"*": "times",
|
|
||||||
"/": "divided by",
|
|
||||||
"\\times": "times",
|
|
||||||
"\\div": "divided by",
|
|
||||||
"\\circ": "circle",
|
|
||||||
"\\bullet": "bullet"
|
|
||||||
};
|
|
||||||
var relMap = {
|
|
||||||
"=": "equals",
|
|
||||||
"\\approx": "approximately equals",
|
|
||||||
"≠": "does not equal",
|
|
||||||
"\\geq": "is greater than or equal to",
|
|
||||||
"\\ge": "is greater than or equal to",
|
|
||||||
"\\leq": "is less than or equal to",
|
|
||||||
"\\le": "is less than or equal to",
|
|
||||||
">": "is greater than",
|
|
||||||
"<": "is less than",
|
|
||||||
"\\leftarrow": "left arrow",
|
|
||||||
"\\Leftarrow": "left arrow",
|
|
||||||
"\\rightarrow": "right arrow",
|
|
||||||
"\\Rightarrow": "right arrow",
|
|
||||||
":": "colon"
|
|
||||||
};
|
|
||||||
var accentUnderMap = {
|
|
||||||
"\\underleftarrow": "left arrow",
|
|
||||||
"\\underrightarrow": "right arrow",
|
|
||||||
"\\underleftrightarrow": "left-right arrow",
|
|
||||||
"\\undergroup": "group",
|
|
||||||
"\\underlinesegment": "line segment",
|
|
||||||
"\\utilde": "tilde"
|
|
||||||
};
|
|
||||||
|
|
||||||
var buildString = (str, type, a11yStrings) => {
|
|
||||||
if (!str) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret;
|
|
||||||
|
|
||||||
if (type === "open") {
|
|
||||||
ret = str in openMap ? openMap[str] : stringMap[str] || str;
|
|
||||||
} else if (type === "close") {
|
|
||||||
ret = str in closeMap ? closeMap[str] : stringMap[str] || str;
|
|
||||||
} else if (type === "bin") {
|
|
||||||
ret = binMap[str] || str;
|
|
||||||
} else if (type === "rel") {
|
|
||||||
ret = relMap[str] || str;
|
|
||||||
} else {
|
|
||||||
ret = stringMap[str] || str;
|
|
||||||
} // If the text to add is a number and there is already a string
|
|
||||||
// in the list and the last string is a number then we should
|
|
||||||
// combine them into a single number
|
|
||||||
|
|
||||||
|
|
||||||
if (/^\d+$/.test(ret) && a11yStrings.length > 0 && // TODO(kevinb): check that the last item in a11yStrings is a string
|
|
||||||
// I think we might be able to drop the nested arrays, which would make
|
|
||||||
// this easier to type
|
|
||||||
// $FlowFixMe
|
|
||||||
/^\d+$/.test(a11yStrings[a11yStrings.length - 1])) {
|
|
||||||
a11yStrings[a11yStrings.length - 1] += ret;
|
|
||||||
} else if (ret) {
|
|
||||||
a11yStrings.push(ret);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var buildRegion = (a11yStrings, callback) => {
|
|
||||||
var regionStrings = [];
|
|
||||||
a11yStrings.push(regionStrings);
|
|
||||||
callback(regionStrings);
|
|
||||||
};
|
|
||||||
|
|
||||||
var handleObject = (tree, a11yStrings, atomType) => {
|
|
||||||
// Everything else is assumed to be an object...
|
|
||||||
switch (tree.type) {
|
|
||||||
case "accent":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, a11yStrings => {
|
|
||||||
buildA11yStrings(tree.base, a11yStrings, atomType);
|
|
||||||
a11yStrings.push("with");
|
|
||||||
buildString(tree.label, "normal", a11yStrings);
|
|
||||||
a11yStrings.push("on top");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "accentUnder":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, a11yStrings => {
|
|
||||||
buildA11yStrings(tree.base, a11yStrings, atomType);
|
|
||||||
a11yStrings.push("with");
|
|
||||||
buildString(accentUnderMap[tree.label], "normal", a11yStrings);
|
|
||||||
a11yStrings.push("underneath");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "accent-token":
|
|
||||||
{
|
|
||||||
// Used internally by accent symbols.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "atom":
|
|
||||||
{
|
|
||||||
var {
|
|
||||||
text
|
|
||||||
} = tree;
|
|
||||||
|
|
||||||
switch (tree.family) {
|
|
||||||
case "bin":
|
|
||||||
{
|
|
||||||
buildString(text, "bin", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "close":
|
|
||||||
{
|
|
||||||
buildString(text, "close", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// TODO(kevinb): figure out what should be done for inner
|
|
||||||
|
|
||||||
case "inner":
|
|
||||||
{
|
|
||||||
buildString(tree.text, "inner", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "open":
|
|
||||||
{
|
|
||||||
buildString(text, "open", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "punct":
|
|
||||||
{
|
|
||||||
buildString(text, "punct", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "rel":
|
|
||||||
{
|
|
||||||
buildString(text, "rel", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
tree.family;
|
|
||||||
throw new Error("\"" + tree.family + "\" is not a valid atom type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "color":
|
|
||||||
{
|
|
||||||
var color = tree.color.replace(/katex-/, "");
|
|
||||||
buildRegion(a11yStrings, regionStrings => {
|
|
||||||
regionStrings.push("start color " + color);
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end color " + color);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "color-token":
|
|
||||||
{
|
|
||||||
// Used by \color, \colorbox, and \fcolorbox but not directly rendered.
|
|
||||||
// It's a leaf node and has no children so just break.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "delimsizing":
|
|
||||||
{
|
|
||||||
if (tree.delim && tree.delim !== ".") {
|
|
||||||
buildString(tree.delim, "normal", a11yStrings);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "genfrac":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, regionStrings => {
|
|
||||||
// genfrac can have unbalanced delimiters
|
|
||||||
var {
|
|
||||||
leftDelim,
|
|
||||||
rightDelim
|
|
||||||
} = tree; // NOTE: Not sure if this is a safe assumption
|
|
||||||
// hasBarLine true -> fraction, false -> binomial
|
|
||||||
|
|
||||||
if (tree.hasBarLine) {
|
|
||||||
regionStrings.push("start fraction");
|
|
||||||
leftDelim && buildString(leftDelim, "open", regionStrings);
|
|
||||||
buildA11yStrings(tree.numer, regionStrings, atomType);
|
|
||||||
regionStrings.push("divided by");
|
|
||||||
buildA11yStrings(tree.denom, regionStrings, atomType);
|
|
||||||
rightDelim && buildString(rightDelim, "close", regionStrings);
|
|
||||||
regionStrings.push("end fraction");
|
|
||||||
} else {
|
|
||||||
regionStrings.push("start binomial");
|
|
||||||
leftDelim && buildString(leftDelim, "open", regionStrings);
|
|
||||||
buildA11yStrings(tree.numer, regionStrings, atomType);
|
|
||||||
regionStrings.push("over");
|
|
||||||
buildA11yStrings(tree.denom, regionStrings, atomType);
|
|
||||||
rightDelim && buildString(rightDelim, "close", regionStrings);
|
|
||||||
regionStrings.push("end binomial");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "hbox":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "kern":
|
|
||||||
{
|
|
||||||
// No op: we don't attempt to present kerning information
|
|
||||||
// to the screen reader.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "leftright":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, regionStrings => {
|
|
||||||
buildString(tree.left, "open", regionStrings);
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
buildString(tree.right, "close", regionStrings);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "leftright-right":
|
|
||||||
{
|
|
||||||
// TODO: double check that this is a no-op
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "lap":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "mathord":
|
|
||||||
{
|
|
||||||
buildString(tree.text, "normal", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "op":
|
|
||||||
{
|
|
||||||
var {
|
|
||||||
body,
|
|
||||||
name
|
|
||||||
} = tree;
|
|
||||||
|
|
||||||
if (body) {
|
|
||||||
buildA11yStrings(body, a11yStrings, atomType);
|
|
||||||
} else if (name) {
|
|
||||||
buildString(name, "normal", a11yStrings);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "op-token":
|
|
||||||
{
|
|
||||||
// Used internally by operator symbols.
|
|
||||||
buildString(tree.text, atomType, a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ordgroup":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "overline":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, function (a11yStrings) {
|
|
||||||
a11yStrings.push("start overline");
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
a11yStrings.push("end overline");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "phantom":
|
|
||||||
{
|
|
||||||
a11yStrings.push("empty space");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "raisebox":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "rule":
|
|
||||||
{
|
|
||||||
a11yStrings.push("rectangle");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "sizing":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "spacing":
|
|
||||||
{
|
|
||||||
a11yStrings.push("space");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "styling":
|
|
||||||
{
|
|
||||||
// We ignore the styling and just pass through the contents
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "sqrt":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, regionStrings => {
|
|
||||||
var {
|
|
||||||
body,
|
|
||||||
index
|
|
||||||
} = tree;
|
|
||||||
|
|
||||||
if (index) {
|
|
||||||
var indexString = flatten(buildA11yStrings(index, [], atomType)).join(",");
|
|
||||||
|
|
||||||
if (indexString === "3") {
|
|
||||||
regionStrings.push("cube root of");
|
|
||||||
buildA11yStrings(body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end cube root");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
regionStrings.push("root");
|
|
||||||
regionStrings.push("start index");
|
|
||||||
buildA11yStrings(index, regionStrings, atomType);
|
|
||||||
regionStrings.push("end index");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
regionStrings.push("square root of");
|
|
||||||
buildA11yStrings(body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end square root");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "supsub":
|
|
||||||
{
|
|
||||||
var {
|
|
||||||
base,
|
|
||||||
sub,
|
|
||||||
sup
|
|
||||||
} = tree;
|
|
||||||
var isLog = false;
|
|
||||||
|
|
||||||
if (base) {
|
|
||||||
buildA11yStrings(base, a11yStrings, atomType);
|
|
||||||
isLog = base.type === "op" && base.name === "\\log";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub) {
|
|
||||||
var regionName = isLog ? "base" : "subscript";
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start " + regionName);
|
|
||||||
buildA11yStrings(sub, regionStrings, atomType);
|
|
||||||
regionStrings.push("end " + regionName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sup) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
var supString = flatten(buildA11yStrings(sup, [], atomType)).join(",");
|
|
||||||
|
|
||||||
if (supString in powerMap) {
|
|
||||||
regionStrings.push(powerMap[supString]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
regionStrings.push("start superscript");
|
|
||||||
buildA11yStrings(sup, regionStrings, atomType);
|
|
||||||
regionStrings.push("end superscript");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "text":
|
|
||||||
{
|
|
||||||
// TODO: handle other fonts
|
|
||||||
if (tree.font === "\\textbf") {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start bold text");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end bold text");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start text");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end text");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "textord":
|
|
||||||
{
|
|
||||||
buildString(tree.text, atomType, a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "smash":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "enclose":
|
|
||||||
{
|
|
||||||
// TODO: create a map for these.
|
|
||||||
// TODO: differentiate between a body with a single atom, e.g.
|
|
||||||
// "cancel a" instead of "start cancel, a, end cancel"
|
|
||||||
if (/cancel/.test(tree.label)) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start cancel");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end cancel");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
} else if (/box/.test(tree.label)) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start box");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end box");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
} else if (/sout/.test(tree.label)) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start strikeout");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end strikeout");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
} else if (/phase/.test(tree.label)) {
|
|
||||||
buildRegion(a11yStrings, function (regionStrings) {
|
|
||||||
regionStrings.push("start phase angle");
|
|
||||||
buildA11yStrings(tree.body, regionStrings, atomType);
|
|
||||||
regionStrings.push("end phase angle");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("KaTeX-a11y: enclose node with " + tree.label + " not supported yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "vcenter":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "vphantom":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: vphantom not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "hphantom":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: hphantom not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "operatorname":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "array":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: array not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "raw":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: raw not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "size":
|
|
||||||
{
|
|
||||||
// Although there are nodes of type "size" in the parse tree, they have
|
|
||||||
// no semantic meaning and should be ignored.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "url":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: url not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "tag":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: tag not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "verb":
|
|
||||||
{
|
|
||||||
buildString("start verbatim", "normal", a11yStrings);
|
|
||||||
buildString(tree.body, "normal", a11yStrings);
|
|
||||||
buildString("end verbatim", "normal", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "environment":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: environment not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "horizBrace":
|
|
||||||
{
|
|
||||||
buildString("start " + tree.label.slice(1), "normal", a11yStrings);
|
|
||||||
buildA11yStrings(tree.base, a11yStrings, atomType);
|
|
||||||
buildString("end " + tree.label.slice(1), "normal", a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "infix":
|
|
||||||
{
|
|
||||||
// All infix nodes are replace with other nodes.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "includegraphics":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: includegraphics not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "font":
|
|
||||||
{
|
|
||||||
// TODO: callout the start/end of specific fonts
|
|
||||||
// TODO: map \BBb{N} to "the naturals" or something like that
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "href":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: href not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "cr":
|
|
||||||
{
|
|
||||||
// This is used by environments.
|
|
||||||
throw new Error("KaTeX-a11y: cr not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "underline":
|
|
||||||
{
|
|
||||||
buildRegion(a11yStrings, function (a11yStrings) {
|
|
||||||
a11yStrings.push("start underline");
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
a11yStrings.push("end underline");
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "xArrow":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: xArrow not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "cdlabel":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: cdlabel not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "cdlabelparent":
|
|
||||||
{
|
|
||||||
throw new Error("KaTeX-a11y: cdlabelparent not implemented yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
case "mclass":
|
|
||||||
{
|
|
||||||
// \neq and \ne are macros so we let "htmlmathml" render the mathmal
|
|
||||||
// side of things and extract the text from that.
|
|
||||||
var _atomType = tree.mclass.slice(1); // $FlowFixMe: drop the leading "m" from the values in mclass
|
|
||||||
|
|
||||||
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, _atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "mathchoice":
|
|
||||||
{
|
|
||||||
// TODO: track which which style we're using, e.g. dispaly, text, etc.
|
|
||||||
// default to text style if even that may not be the correct style
|
|
||||||
buildA11yStrings(tree.text, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "htmlmathml":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.mathml, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "middle":
|
|
||||||
{
|
|
||||||
buildString(tree.delim, atomType, a11yStrings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "internal":
|
|
||||||
{
|
|
||||||
// internal nodes are never included in the parse tree
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "html":
|
|
||||||
{
|
|
||||||
buildA11yStrings(tree.body, a11yStrings, atomType);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
tree.type;
|
|
||||||
throw new Error("KaTeX a11y un-recognized type: " + tree.type);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) {
|
|
||||||
if (a11yStrings === void 0) {
|
|
||||||
a11yStrings = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree instanceof Array) {
|
|
||||||
for (var i = 0; i < tree.length; i++) {
|
|
||||||
buildA11yStrings(tree[i], a11yStrings, atomType);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handleObject(tree, a11yStrings, atomType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return a11yStrings;
|
|
||||||
};
|
|
||||||
|
|
||||||
var flatten = function flatten(array) {
|
|
||||||
var result = [];
|
|
||||||
array.forEach(function (item) {
|
|
||||||
if (item instanceof Array) {
|
|
||||||
result = result.concat(flatten(item));
|
|
||||||
} else {
|
|
||||||
result.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
var renderA11yString = function renderA11yString(text, settings) {
|
|
||||||
var tree = katex.__parse(text, settings);
|
|
||||||
|
|
||||||
var a11yStrings = buildA11yStrings(tree, [], "normal");
|
|
||||||
return flatten(a11yStrings).join(", ");
|
|
||||||
};
|
|
||||||
|
|
||||||
export { renderA11yString as default };
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue