| ... | @@ -16,7 +16,8 @@ static const repo = "${repo}"; | ... | @@ -16,7 +16,8 @@ static const repo = "${repo}"; |
| 16 | [ts lie detector]: https://git.paperclover.net/clo/ts-lie-detector | 16 | [ts lie detector]: https://git.paperclover.net/clo/ts-lie-detector |
| 17 | [evil inc]: https://evil.inc/ | 17 | [evil inc]: https://evil.inc/ |
| 18 | [history of japan reanimated]: /history-of-japan-reanimated | 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 | [HOTEWIG reanimated]: /hotewig-reanimated | 21 | [HOTEWIG reanimated]: /hotewig-reanimated |
| 21 | [clover creative control]: https://git.paperclover.net/clo/creative-control | 22 | [clover creative control]: https://git.paperclover.net/clo/creative-control |
| 22 | [markodown]: https://git.paperclover.net/clo/markodown | 23 | [markodown]: https://git.paperclover.net/clo/markodown |
| ... | @@ -29,6 +30,83 @@ static const repo = "${repo}"; | ... | @@ -29,6 +30,83 @@ static const repo = "${repo}"; |
| 29 | | 30 | |
| 30 | these are like mini blog posts, but more in generally just what the heck i'm up to. | 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 | ## 2026-03-16 | 110 | ## 2026-03-16 |
| 33 | | 111 | |
| 34 | just chilling. at work i replaced `bun install` with `pnpm`. pretty peak. in the | 112 | just chilling. at work i replaced `bun install` with `pnpm`. pretty peak. in the |