Compare commits

..

No commits in common. "master" and "zola-version" have entirely different histories.

326 changed files with 1796 additions and 71089 deletions

View file

@ -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

View file

@ -1 +0,0 @@
*

6
.drone.yml Normal file
View file

@ -0,0 +1,6 @@
pipeline:
build:
image: j1mc/docker-zola
commands:
- zola build

View file

@ -6,4 +6,4 @@ insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 2
indent_size = 4

1
.envrc
View file

@ -1 +0,0 @@
use flake

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
public/katex/**/* linguist-vendored
src/**/*.md linguist-documentation=false

35
.gitignore vendored
View file

@ -1,34 +1 @@
# build output
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
public

View file

@ -1,3 +0,0 @@
public/katex
pnpm-lock.yaml
src/styles/fork-awesome

View file

@ -1,17 +0,0 @@
{
"plugins": ["prettier-plugin-astro"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
],
"useTabs": false,
"tabWidth": 2,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100
}

View file

@ -1,4 +0,0 @@
public
package-lock.json
src/styles/fork-awesome
pnpm-lock.yaml

View file

@ -1,7 +0,0 @@
{
"recommendations": [
"astro-build.astro-vscode",
"eliostruyf.vscode-front-matter"
],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored
View file

@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View file

@ -1,3 +0,0 @@
{
"npm.packageManager": "pnpm"
}

View file

@ -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

View file

@ -1,5 +0,0 @@
# Michael's Blog
https://mzhang.io
License: GPL-3.0 code / CC-BY-SA-4.0 contents

View file

@ -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" }],
],
},
});

View file

@ -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
}
}
}

View file

@ -1,2 +0,0 @@
include: src/content/posts src
depend: standard-library cubical

BIN
bun.lockb

Binary file not shown.

22
config.toml Normal file
View 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" },
]

View file

@ -1,16 +1,18 @@
---
title: My new life stack
date: 2018-02-01
tags: ["arch", "linux", "setup", "computers"]
---
+++
title = "my new life stack"
date = 2018-02-01
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
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]
$ 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.

View file

@ -1,11 +1,12 @@
---
title: "Cleaning up your shell"
date: 2018-02-25
tags: ["computers", "linux", "terminal"]
languages: ["bash"]
---
+++
title = "cleaning up your shell"
date = 2018-02-25
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.

View file

@ -1,8 +1,10 @@
---
title: "Fixing tmux colors"
date: 2018-04-23
tags: ["computers"]
---
+++
title = "fixing tmux colors"
date = 2018-04-23
[taxonomies]
tags = ["computers", "terminal"]
+++
Put this in your `~/.tmux.conf`.

View file

@ -1,13 +1,14 @@
---
title: "Web apps"
date: 2018-05-28
tags: ["computers", "web", "things-that-are-bad"]
languages: ["javascript"]
---
+++
title = "web apps"
date = 2018-05-28
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.
@ -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?
## 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?
@ -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?
## 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:
@ -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?
- 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.

View file

@ -1,31 +1,25 @@
---
title: "Twenty years of attacks on rsa with examples"
date: 2018-10-26
toc: true
tags: ["ctf", "crypto", "rsa"]
languages: ["python"]
math: true
---
+++
title = "twenty years of attacks on rsa.. with examples!"
date = 2018-10-26
There's [a great paper][1] I found by Dan Boneh from 1998 highlighting the
weaknesses of the RSA cryptosystem. I found this paper to be a particularly
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-->
[taxonomies]
tags = ["math", "crypto", "python"]
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 that.
[extra]
toc = true
+++
## Factoring large integers
# 1. introduction
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.
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)
Let's choose some small numbers to demonstrate this one (you can follow along in
a Python REPL if you want):
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.
## 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
>>> N = 881653369
@ -33,9 +27,7 @@ a Python REPL if you want):
>>> c = 875978376
```
$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 \times 42557$, we can find the totient of $N$:
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:
```py
>>> p = 20717
@ -44,8 +36,7 @@ now that $N = 20717 \times 42557$, we can find the totient of $N$:
881590096
```
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])
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])
```py
>>> 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...
## Elementary attacks
# 2. elementary attacks
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)
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)
### Common modulus
## 2.1 common modulus
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.
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.
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.
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.
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).
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).
```py
>>> 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>
```
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):
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):
```py
>>> 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>
```
Ok, now we have two keys, $k_1$ and $k_2$. Now I'll show how using only the public
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$.
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`.
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 $$
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}} $$
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)`.
```py
>>> from decimal import Decimal, getcontext
@ -143,7 +96,7 @@ $$ \phi(N) = \frac{ed - 1}{\frac{ed}{N}} $$
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
>>> B = Decimal(N) - phi + 1
@ -151,8 +104,7 @@ Decimal('34642192582980793929380236893725052051755685627449634068179923908929124
>>> C = Decimal(N)
```
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:
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:
```py
>>> p = (B + (B * B - 4 * C).sqrt()) / Decimal(2)
@ -163,18 +115,13 @@ Decimal('16823427526200025299629360726790945249096067335633200400252946099787748
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
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'$.
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'.
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:
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:
```py
>>> 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"
```
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^eM \mod N$ where r is an integer that's
coprime to $N$:
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:
```py
>>> 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~'
```
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
$$
\begin{aligned}
S' &= (M'^d) \\\
&= (r^e * M)^d \\\
&= r^{ed} * M^d \\\
&= r * M^d \mod N
\end{aligned}
$$
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`.
```py
>>> Sp, = bob.sign(Mp, 0)
4222298342813922437811434251340999736739055616654488323193778229765071846717137952694561809398626068283668428796351354154566771597532278827070832905206221261994843265685464173739776886856384806238418884247949451413559988796455422271296883338455956330421559319009950760931899199217936823999874162064553735563087382870564193673989865778229832918474778963380170967676966373703157629615331081637805594392084045827925764529711433584853942576464491576212176547485726609891593617931393545058401472883178443786988683045423150809606471425615670582973274971087459634959553685559458456237617436410759134193279063427911112115134
```
Now, all Marvin has to do is multiply by the modular inverse of $r$, to obtain
$M^d$, the signature of the original message:
Now, all Marvin has to do is multiply by the modular inverse of r, to obtain `M^d`, the signature of the original message:
```py
>>> S = (Sp * modinv(r, N)) % N
6137678992536399703654836416525985142902780822513172949427421060785532284955531529418529725602418902796840570634560123808769013384654624916503940938715718120521434666716675795201896105310462331838807171312705686415521871046533303776516500490921892398440988515777575520183847518597482163414665355222659603386541869176930658730416118799866012276767364050134126722746224706026850062367243018313483359694686773566231956425606553198607719740067340776177716443517567144901614253170719278035838849363127850910135864099535083004590180745762100334268408681888925040382341592080592207557742366581814701422371311084081150092871
```
Sure enough, if you try to verify the "original" signature against the original
message, it checks out.
Sure enough, if you try to verify the "original" signature against the original message, it checks out.
```py
>>> bob.verify(M, (S,))

View file

@ -1,13 +1,12 @@
---
title: "Magic forms with proc macros: Ideas"
date: 2019-02-01
tags: ["computers", "web"]
languages: ["rust"]
---
+++
title = "magic forms with proc macros: ideas"
date = 2019-02-01
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
#[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.
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:
@ -60,12 +57,12 @@ name: String,
This should generate the following 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?
## 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):
@ -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.
## 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.
## Conclusion
## conclusion
This project is a work in progress! You can see how far I am [on Github](https://github.com/mzhang28/wtforms).
[1]: https://docs.rs/serde
This project is a work in progress! You can see how far I am [on Github](https://github.com/iptq/wtforms).

View file

@ -1,10 +1,12 @@
---
title: "Accept server analogy"
date: 2019-03-04
tags: ["computers"]
---
+++
title = "accept server analogy"
date = 2019-03-04
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.

View file

@ -1,20 +1,24 @@
---
title: "Password managers"
date: 2020-04-01
tags: ["computers", "things-that-are-good", "privacy"]
---
+++
title = "password managers"
date = 2020-04-01
[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.
<!-- 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.
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:
@ -23,7 +27,8 @@ If you're willing to put sensitive passwords into your password manager, it shou
- Your car's license plate number
- 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):
@ -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.
## 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.

View 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
View 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
+++

View 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;
}
};
}
));
}));

View 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

View 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

View file

@ -0,0 +1,7 @@
+++
template = "blog.html"
insert_anchor_links = "left"
[extra]
include_posts = true
+++

9
content/pages/_index.md Normal file
View 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
View 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
View 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)

View file

@ -1,14 +1,16 @@
---
title: "Setting up IRC with Weechat"
date: 2018-10-18
tags: ["irc"]
---
+++
title = "setting up irc with weechat"
date = 2018-10-18
[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.
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:
@ -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.
## Step 2: Add relay
## step 2: add relay
Now add a relay through weechat:
@ -37,7 +39,7 @@ where name is
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.
@ -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.
## 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:
@ -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.
## 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

View 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
View 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.

View file

@ -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
}

View file

@ -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
];
};
});
}

View file

@ -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
}
]
}
]
}

View file

@ -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} $@
''

View file

@ -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 = [
# ];
# };

View file

@ -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"
}
}

View file

@ -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);
};
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

File diff suppressed because it is too large Load diff

View file

@ -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.

View file

@ -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.

View file

@ -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).

View file

@ -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__;
/******/ })()
;
});

View file

@ -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}()}));

View file

@ -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 };

View file

@ -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;
}

View file

@ -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__;
/******/ })()
;
});

View file

@ -1 +0,0 @@
.katex,.katex-display{-webkit-user-select:all;-moz-user-select:all;-ms-user-select:all;user-select:all}

View file

@ -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}()}));

View file

@ -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();
});

View file

@ -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__;
/******/ })()
;
});

View file

@ -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}()}));

View file

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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 };

Some files were not shown because too many files have changed in this diff Show more