| ... | ... | @@ -16,7 +16,8 @@ static const repo = "${repo}"; |
| 16 | 16 | [ts lie detector]: https://git.paperclover.net/clo/ts-lie-detector |
| 17 | 17 | [evil inc]: https://evil.inc/ |
| 18 | 18 | [history of japan reanimated]: /history-of-japan-reanimated |
| 19 | | [react mutation]: /history-of-japan-reanimated |
| 19 | [react mutation]: http://jsr.io/@clo/react-mutation |
| 20 | [react markdown]: http://jsr.io/@clo/react-markdown |
| 20 | 21 | [HOTEWIG reanimated]: /hotewig-reanimated |
| 21 | 22 | [clover creative control]: https://git.paperclover.net/clo/creative-control |
| 22 | 23 | [markodown]: https://git.paperclover.net/clo/markodown |
| ... | ... | @@ -29,6 +30,83 @@ static const repo = "${repo}"; |
| 29 | 30 | |
| 30 | 31 | these are like mini blog posts, but more in generally just what the heck i'm up to. |
| 31 | 32 | |
| 33 | ## 2026-03-20 |
| 34 | |
| 35 | tags: [react markdown] |
| 36 | |
| 37 | i published a new library named `@clo/react-markdown`. originally, i wanted to |
| 38 | have my own version of the `streamdown` package from vercel. but i didn't |
| 39 | realize how much of a loser company they all are. i wasn't even trying and i |
| 40 | made a library like a hundred times better and simpler than theres. because of |
| 41 | the awesome success here, instead of calling mine "memo markdown", i just said |
| 42 | "yea, this covers every markdown use case for react" and called it React Markdown. |
| 43 | |
| 44 | you can install it from the JSR: |
| 45 | |
| 46 | ```sh |
| 47 | npx jsr add @clo/react-markdown |
| 48 | pnpm add jsr:@clo/react-markdown |
| 49 | ``` |
| 50 | |
| 51 | there are two main features that i deliver on: |
| 52 | |
| 53 | - predicting close tokens for sequences like `hello **world`, appending `**` |
| 54 | - streamdown has this too, but many many cases are not considered. while |
| 55 | theirs is extensible and mine isn't, i don't think you'll need to extend |
| 56 | my markdown predictor. |
| 57 | - component memoization. all block and inline components will preserve their |
| 58 | state, even as adjacent content changes. this is done to preserve remounts |
| 59 | for things like custom `<a>` tags or other components. (for example, if a |
| 60 | custom `<a>` fetches previewing data and provides a hover card, that card |
| 61 | won't flicker). |
| 62 | |
| 63 | copying some architecture notes from the readme, the memoizer is performant |
| 64 | from the following tricks: |
| 65 | |
| 66 | - Using proper `React.memo()` calls. Obviously. |
| 67 | - Prediction is implemented in a stateful way that only parses the tail end of |
| 68 | the document, marking how much of the document is stable and where possible |
| 69 | incomplete syntax may live. Since prediction only applies at the end, changing |
| 70 | text midway through can invalidate the whole predictor. |
| 71 | - Separate the parsed document into "blocks", noting the source location of |
| 72 | where each block lives. |
| 73 | - Only start parsing after the first changed character, rounded to the nearest |
| 74 | block. In the append-only stream, this essentially means the last two blocks |
| 75 | are the only things being re-parsed. |
| 76 | - Similarly, run AST transforms only on the changed data. This step has a couple |
| 77 | of slow paths for when reference link definitions are added or edited, since |
| 78 | it means any places that might have used a reference link may now have to |
| 79 | reflect it. |
| 80 | - After all that, a special AST -> React node transform is used that diffs the |
| 81 | new ast with the last ast, reusing React nodes whenever possible. It supports |
| 82 | nested children as well as re-ordering top level blocks. This is what prevents |
| 83 | most rerenders and is the "secret sauce". |
| 84 | |
| 85 | ## 2026-03-18 |
| 86 | |
| 87 | tags: [progress.ts] |
| 88 | |
| 89 | laser hair removal is awesome btw. organizing things at home slowly. more work |
| 90 | on the progress library, trying to handle every edge case possible for the log |
| 91 | widget system. |
| 92 | |
| 93 | when it is done, a code snippet like this will work. |
| 94 | |
| 95 | ```ts |
| 96 | using node = progress.start("some action"); |
| 97 | for await (const token of stream) { |
| 98 | // correctly interweave progress TUI with partial log lines |
| 99 | process.stderr.write(token); |
| 100 | } |
| 101 | ``` |
| 102 | |
| 103 | the log system injects into `process` to ensure it plays nice, but there is |
| 104 | only a fast path on stdout (since the main log messages uses stdout). stderr |
| 105 | gets to use the crazy `getDrawLock` API i'm cooking up internally. |
| 106 | |
| 107 | after all of this works and is reliable, there are some more things i have to |
| 108 | tidy, but then the progress blog post can be written and reviewed for realsies. |
| 109 | |
| 32 | 110 | ## 2026-03-16 |
| 33 | 111 | |
| 34 | 112 | just chilling. at work i replaced `bun install` with `pnpm`. pretty peak. in the |