stacker.news icon indicating copy to clipboard operation
stacker.news copied to clipboard

Markdown editor with Rich Text preview via MDAST

Open Soxasora opened this issue 1 month ago • 17 comments

Status

Ready to QA

Pending:

  • README
  • PR update

Description

Based on #2501 This is the first stage of the transition to a new Lexical-based editor.

Introduces a Lexical plain text editor with a rich text preview, using MDAST to map and transform Lexical nodes to markdown, aiming to provide lossless transformations.

This PR focuses on providing the same editor experience as before, while transitioning to Lexical.

Screenshots

Refreshed reply editor style, same background as comments

image

Refreshed top-level editor style

image

Editor workings

Supported nodes

Nodes are a core concept in Lexical. Not only do they form the visual editor view, as part of the EditorState, but they also represent the underlying data model for what is stored in the editor at any given time. [src]

These are the nodes we support at the moment, the scope for stage 1 is to support everything our old MarkdownInput text editor supported.

node what does it do
SNHeadingNode enriches Lexical's HeadingNode with anchors/slugs for ToCs
TableOfContentsNode collects headings and auto-generates a collapsible table of contents
MathNode LaTeX math rendering via KaTeX
(User/Territory/Item)Node a set of nodes for user/territory/item mentions
MediaNode stateful node for images and videos, supports media type checks (disabled) and wraps MediaOrLink. support for captions and resize is disabled.
EmbedNode general node for embeds, wraps the Embed component

Plugins

A plugin, to all intents and purposes, is a React components that take (either param or via context) a LexicalEditor instance and does something with it. The following are plugins that change the editor's behavior or complete nodes' functionalities.

plugin what does it do commands
FormikBridgePlugin extracts text from Lexical and places it in Formik values
LocalDraftPlugin saves and restores drafts to/from local storage
MaxLengthPlugin enforces character limits, configurable via lengthOptions
MentionsPlugin handles @user and ~territory dual autocomplete
FileUploadPlugin file upload with paste and drag/drop support SN_UPLOAD_FILES_COMMAND: triggers browser file upload
PreviewPlugin toggle between write/preview modes and renders a Lexical Reader TOGGLE_PREVIEW_COMMAND
ToolbarPlugin editor toolbar
HistoryPlugin undo/redo

Extensions

Extensions are a new paradigm, introduced just a couple of major versions ago. Many plugins are essentially just useEffects, they don't render nor effectively need to use React in some useful way.

Extensions are lightweight, meant to be used for stuff that doesn't need React. Let's say we just want to capture the creation of a specific node to do something special, we can just create an extension that registers a MutationListener and that's it. Less boilerplate, less React dependency.

extensions what does it do commands
CodeShikiSN custom Shiki registration that re-register itself on theme changes (light/dark mode) UPDATE_CODE_THEME_COMMAND
Shortcuts barebones metaKey + key shortcuts handler
MDCommands barebones link/bold/italic insert on selection MD_INSERT_LINK_COMMAND MD_INSERT_BOLD_COMMAND MD_INSERT_ITALIC_COMMAND

Feature parity

parity list
  • write/preview: Y
  • bold shortcut: Y
  • italic shortcut: Y
  • link shortcut: Y
  • upload shortcut: Y
  • tab insert shortcut: NO
  • submit shortcut: Y
  • paste images: Y
  • file DnD: Y
  • upload button: Y
  • upload fees: Y
  • user/territory/item mentions and autocomplete: Y
  • local drafts: Y
  • formik: Y
  • max length: Y
  • undo/redo: Y (by Lexical HistoryPlugin)
  • markdown help link: NO
  • textarea autosize: Y (by Lexical)
  • rich preview: Y (LexicalReader)
  • footnotes: NO even though MDAST supports it, we need a Lexical plugin to handle this.
new features
  • preview toggle shortcut (meta+P)
  • undo/redo via Lexical
  • Shiki syntax highlighting
  • KaTeX math support
  • MDAST-based markdown transformations

MDAST

Lexical already supports markdown transformations via @lexical/markdown, the problem with Lexical's native approach is that we're forced to do workarounds for proper markdown bi-directional transformations.

By treating the Lexical tree as an MDAST tree we can be confident in creating lossless bi-directional markdown transformations. More informations can be found in lib/lexical/mdast/README.md

Lines

This is undoubtedly a big PR but it's smaller than the lines being reported, for example let's take a look at the lib/lexical package:

  • mdast: ~1700 lines README has about 400 lines of infos, then we have the logic and the visitors (~800 lines! but they cover every node)
  • nodes: ~1700 lines there's a lot of boilerplate in Lexical nodes
  • theme: ~700 lines of CSS ...

Probably around 4k lines are true code.

Additional Context

Persisting and handling the tree in the database is still something that's being conceptualized, but like the original PR, we'll persist the lexical JSON state and its HTML variant.


lib/lexical/server/headless.js Creating a fake DOM for Lexical operations server-side pollutes the global environment

global.window = newWindow
global.document = newDocument

It is safely cleaned at the end of an operation with the finally block, but it's still something we should keep an eye on.

Checklist

Are your changes backward compatible? Please answer below: Yes, with a distributed migration approach we're going to migrate legacy content while still keeping its original version right in the text field.

On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below: 5 - in dev with iterative QA

For frontend changes: Tested on mobile, light and dark mode? Please answer below: In QA

Did you introduce any new environment variables? If so, call them out explicitly here: n/a

Did you use AI for this? If so, how much did it assist you? Lexical: Ask mode to bridge the gaps in the documentation MDAST: Light usage of Agent mode for refactoring and ensuring every transformer is well-represented


[!NOTE] Replaces the Markdown editor with a Lexical-based editor/reader and SSR pipeline, adds lexicalState/html GraphQL fields, and updates forms/rendering to use the new system.

  • Editor/Reader (Lexical):
    • Add Lexical plain-text editor (SNEditor) with toolbar, preview, uploads, mentions, max-length, local drafts, shortcuts, and Shiki/Katex support.
    • Add client SNReader and dynamic reader with link interception, ToC, galleries, media/math nodes, embeds.
    • Replace MarkdownInput with SNInput across post/comment/bio/job/link/poll/territory forms.
    • Update Text component to render Lexical (Text) with fallback (LegacyText).
    • New styles for editor/text; refreshed tooltips and containers.
  • GraphQL/SSR:
    • Add lexicalState and html fields to Item and Sub types and resolvers.
    • Introduce lexicalStateLoader (DataLoader) and SSR HTML generation (lexicalHTMLGenerator); wire into API/Apollo SSR context.
  • Server libs:
    • Implement MDAST import/export/transform pipeline and numerous Lexical nodes/extensions.
    • Add server headless DOM utilities (LinkeDOM/DOMPurify) and item-context/media handling.
  • Rendering/Components:
    • Update comments, item pages, bios, notifications, TOC and media components to use new reader/state.
    • Minor UX tweaks (action tooltip delays/noWrapper, gallery/media refactor).
  • Config/Deps:
    • Add lexical, dompurify, linkedom, dataloader, katex, @lexical/*, shiki, etc.; alias canvas off in Next config.

Written by Cursor Bugbot for commit 8727372e77c4c582c9bdee5f17b3a3aade5fcfc5. This will update automatically on new commits. Configure here.

Soxasora avatar Nov 26 '25 12:11 Soxasora

I just gave it a little spin - didn't encounter any bugs yet. It feels great and looks great. I'm very excited knowing we took the time to build something we can be confident in. Nice job!

One thing I noticed: I think media loading may be a bit inconsistent with prod behavior

http://localhost:3000/items/458188 vs https://stacker.news/items/737272

I'll give it a deeper look tomorrow.

Edit: the media loading could be caused by me not running imgproxy in dev.

Screenshot 2025-12-11 at 8 59 32 PM Screenshot 2025-12-11 at 8 59 38 PM

huumn avatar Dec 12 '25 02:12 huumn

to build something we can be confident in

Agree, the new architecture allows us to do pretty much whatever we want to do with Markdown, and that's a really big power.

One thing I noticed: I think media loading may be a bit inconsistent with prod behavior

It is inconsistent towards measurements because the MediaNode was made for rich text resize/caption behavior. But, in terms of loading, we're still working with the same MediaOrLink component as prod, and you're viewing the image in the preview tab... so that's weird.

edit: unless you didn't run the capture container, in that case I think we don't have a fallback.

Soxasora avatar Dec 12 '25 11:12 Soxasora

Yes, I wasn't running capture. My bad. I'll do a better job QAing tonight.

The other difference is that youtube link renders an embed. I don't think I ever documented in anywhere but the behavior prod follows is: if a media/embed link is in a paragraph, render it as a link.

huumn avatar Dec 12 '25 16:12 huumn

Oh okay! It makes sense to allow media/embeds only if they're in their own paragraph. I explicitly did the opposite!

Soxasora avatar Dec 12 '25 16:12 Soxasora

I spent most of the time I set aside for this QA troubleshooting weird bugs (like someone disabling all their notification types and causing their notifications to throw errors).

I'll do a serious QA tomorrow and begin review-review!

huumn avatar Dec 13 '25 05:12 huumn

Just getting started but I'm seeing a bit of weird media flickering

https://github.com/user-attachments/assets/240a4e11-435a-4ace-9c20-b3fa6b46f1e5

huumn avatar Dec 13 '25 23:12 huumn

Yeah that is something that we have in prod too. Edit mode unmounts and re-mounts the Text component, causing media checks to run each time.

We can likely simply hide Text in edit mode instead of conditionally rendering it. 👀

edit: also the HTML placeholder contributes to this, it renders images as links


I suspect that the code block theme flickerings are due to Shiki not respecting the github-dark-theme-default default theme setting. It's next on the cleanup so I'll address it right away!

Soxasora avatar Dec 13 '25 23:12 Soxasora

I'm just going to keep editing this comment for little things I notice

  • triple-click behavior in the markdown editor. actual: selects all text for some reason. expected: selects the paragraph.
  • adjacent image/video media doesn't tile into a grid anymore (not a huge deal/priority but for anyone who styled things in the past they might be upset)
  • show full text appears in preview but is just fixed and doesn't clamp (in this example I copy and pasted the FAQ) Screenshot 2025-12-13 at 7 53 05 PM
  • headings in posts are no longer links/clickable (useful for linking to a section)
  • more of question: curious about the reason for keeping LegacyText
  • minor nit: preview toggle shortcut does not toggle back to edit mode (it does go to preview successfully though)

btw I'm really impressed by how well the code is organized. it's a lot, as editors are naturally, but everything feels like it's in a good spot.

huumn avatar Dec 14 '25 00:12 huumn

triple-click behavior in the markdown editor. actual: selects all text for some reason. expected: selects the paragraph.

editor.registerCommand(lexical.KEY_ENTER_COMMAND, event =>
  editor.dispatchCommand(lexical.INSERT_LINE_BREAK_COMMAND, false));

I was weirded out too, Lexical Plain Text stores the entire text in a single paragraph node, and intercepts the Enter key to insert a line break rather than a paragraph. We can restore paragraph on enter key though.


btw I'm really impressed by how well the code is organized.

hey thanks but I do honestly reconsider the structure every 2 minutes

Soxasora avatar Dec 14 '25 01:12 Soxasora

I've done a full initial pass through the code and, dude, it's very nice. You should feel very proud. Lexical is kind of like LDK - barebones for lightning protocol stuff, customizable but useless on its own - and you made SN a custom editor with it. It's amazing. It's crazy to think about what we can and will do with it.

I was trying to keep a mental list of all the cool stuff you did, but I especially like the choice to make the write/preview tabs and the attachment button into a toolbar. That wouldn't have occurred to me.

I'll do a harder pass tomorrow. It's been hard to find faults in it so far.

I also think we'll want READMEs for some of the other directories, or maybe just one README for lib/lexical and components/editor that summarizes what the things in the non-mdast directories are for - even if they mostly just link to the appropriate Lexical docs. I haven't wrapped my head around lib/lexical/html/customs yet, nor lib/lexical/nodes, for instance, and everything they do.

huumn avatar Dec 14 '25 05:12 huumn

more of question: curious about the reason for keeping LegacyText

We don't actually need it! Especially now that lexical states are computed on-request. It was really useful for live migrations, but we don't need it anymore.

I haven't wrapped my head around lib/lexical/html/customs yet, nor lib/lexical/nodes, for instance, and everything they do.

The HTML customizations are the most fragile piece in this PR. Probably not really fragile, but the fact that I don't feel confident about them, makes them fragile.

They're a nice-enough workaround for imgproxy and outlawed state management, and only for the split second HTML fallback. The need for an HTML fallback, honestly, introduced a lot of head-scratches.


Thank you for the nice words, the fact that we already have a full hybrid editor ready shows that we can confidently build pretty much whatever we want. Lexical is a really good choice.

Soxasora avatar Dec 15 '25 15:12 Soxasora

New nit list:

  • media ends up with both the old mediaContainer css classes and the new classes
    • we should probably do one or the other
  • only curious: useCallbackRef what's it being used for?
`{:toc}` renders with details collapsed, then pops open causing layout shift https://github.com/user-attachments/assets/a175ce64-a48b-4aa5-b6ff-5ff1fda94a06
low priority but something affecting heights of the media containers didn't port over well afaict IIRC this was very hard to get working well initially - lots of trial-and-error was required and then for videos we kind of had to undue some of what prevented layout shift Screenshot 2025-12-15 at 8 34 35 PM
links to headings referencing the same item (self-created) open in a new tab I had Chat create a markdown torture test for gfm (it has some stuff we don't support). Everything else worked perfectly afaict.
<!--
  GFM Torture Test Document
  Goal: hit as many edge-cases as possible (CommonMark + GFM extensions).
-->

# GFM Torture Test: “Everything Bagel” 🥯

> **Intent:** This document is intentionally dense, weird, and occasionally “ugly” to stress parsers.
>
> If your renderer survives this, it’s doing pretty well.
> (That line above ends with *two spaces* for a hard line break.)

---

## Table of contents

* [Headings & breaks](#headings--breaks)
* [Emphasis, escaping, and punctuation](#emphasis-escaping-and-punctuation)
* [Links, references, autolinks](#links-references-autolinks)
* [Lists (nested, mixed, loose/tight)](#lists-nested-mixed-loosetight)
* [Blockquotes (nested) + lists + code](#blockquotes-nested--lists--code)
* [Tables (alignment, pipes, inline markdown)](#tables-alignment-pipes-inline-markdown)
* [Code (inline, fenced, indented, nested fences)](#code-inline-fenced-indented-nested-fences)
* [HTML blocks & mixed parsing](#html-blocks--mixed-parsing)
* [Footnotes](#footnotes)
* [Oddities & fuzz section](#oddities--fuzz-section)

---

## Headings & breaks

# Setext heading level 1

## Setext heading level 2

### Heading with escapes: # not a heading, *not emphasis*

#### Heading with inline code: `const x = 1;` and emoji :rocket: 🚀

##### Heading with “smart” punctuation… quotes “like this” and dashes — like this

###### Heading 6

---

Thematic breaks variants:

---

---

---

Ambiguous-ish: (should be a list, not a break)

* * * not a break because it’s a list item
* --- also a list item

---

## Emphasis, escaping, and punctuation

Plain: *italic* **bold** ***bold italic*** ~~strikethrough~~.

Nested: **bold *italic ~~strike~~ italic* bold**.

Edge-ish underscore cases:

* a_word_with_underscores (should usually **not** emphasize inside words)
* *leading underscore*
* trailing underscore_
* **double** and ***triple***
* **bold with *italic inside* and `code`**.

Escapes (these should render literally):

* _ ~ ` # [ ] ( ) { } - + . !

Backslash at end of line for a hard break (CommonMark style):
This line should be on a new line.

HTML entities: © & < > ©

---

## Links, references, autolinks

Inline links:

* [Simple link](https://example.com)
* [Link with title](https://example.com "a title (with parens) and \"quotes\"")
* [Angle-bracket destination](https://example.com/a path/with spaces?q=hello world)
* [Paren-heavy URL](https://example.com/a_%28b%29_c?x=1&y=2)
* [Link with nested brackets [like this]](https://example.com/nested "title")

Reference links (case-insensitive labels):

* [Reference A][ref-a]
* [reference a][REF-A]
* [Collapsed reference][]
* [Shortcut reference]

Autolinks:

* [https://example.com/autolink?x=1&y=2](https://example.com/autolink?x=1&y=2)
* [mailto:[email protected]](mailto:[email protected])

Bare URL (some renderers auto-link this, some don’t—good to test):
[https://example.com/bare-url](https://example.com/bare-url)

Images:

* ![Alt text](https://via.placeholder.com/120x40?text=img "image title")
* ![Reference image][img-ref]

GitHub-ish autolink patterns (not strictly “GFM spec” but common on GitHub):

* Mention: @octocat
* Issue-ish: #123
* SHA-ish: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef

[ref-a]: https://example.com/ref "ref title"
[collapsed reference]: https://example.com/collapsed
[shortcut reference]: https://example.com/shortcut
[img-ref]: https://via.placeholder.com/80?text=refimg "ref image title"

---

## Lists (nested, mixed, loose/tight)

Tight list:

* one
* two
* three

Loose list (paragraphs inside items):

* one

  still item one after a blank line

* two

  * nested bullet A
  * nested bullet B

    1. nested ordered 1
    2. nested ordered 2

       * deeper bullet with `inline code`
       * deeper bullet with a fenced code block:

         ```js
         // code fence inside deeply nested list
         const arr = [1, 2, 3].map(x => x * 2);
         console.log(arr);
         ```

* three with a task list:

  * [ ] unchecked task
  * [x] checked task
  * [x] checked task (capital X)
  * [ ] task with **formatting**, ~~strike~~, and a [link](https://example.com)
  * [ ] task with nested tasks

    * [ ] subtask 1
    * [x] subtask 2

Ordered list starting at 7:

7. item seven
8. item eight
9. item nine

   * mixed nested bullet
   * mixed nested bullet
10. item ten with two paragraphs

    second paragraph in item ten

Weird numbering that should still be an ordered list:

1. first
2. second (still “2.” logically)
3. third

---

## Blockquotes (nested) + lists + code

> Blockquote level 1
>
> > Blockquote level 2
> >
> > * list item in nested quote
> > * another item
> >
> > ```python
> > def quoted_code():
> >     return "code inside blockquote"
> > ```
>
> Back to level 1 with `inline code`.

> A blockquote with a table (some parsers get quirky):
>
> | a | b |
> | - | - |
> | 1 | 2 |

---

## Tables (alignment, pipes, inline markdown)

Alignment + inline markdown:

| left                       |         center         |  right | tricky              |
| :------------------------- | :--------------------: | -----: | :------------------ |
| *it*                       |         **bd**         | ~~st~~ | `code \| span`      |
| [lnk](https://example.com) |       :smile: 😄       |  12345 | pipe | escaped      |
| multi<br>line              | cell<br>with<br>breaks |      0 | `<span>html</span>` |

Table with leading/trailing pipes:

| col1 | col2 |
| ---- | ---- |
| a    | b    |

No leading pipe:

| col1 | col2 | col3 |
| ---- | :--: | ---: |
| x    |   y  |    z |

---

## Code (inline, fenced, indented, nested fences)

Inline code edge cases:

* ``code with a ` backtick inside``
* `code with **markdown** inside should stay literal`

Indented code block (4 spaces):

line 1 (indented) line 2 (still code) line 3


Fenced code blocks:

```txt
Plain fenced text.
- This is not a list inside a code block.
- removed line
+ added line
@@ hunk header @@
{
  "string": "quotes: \"\", backslash: \\",
  "arr": [1, 2, 3],
  "nested": { "a": true, "b": null }
}

Tilde fence:

echo "hello from ~~~ fence"
printf '%s\n' "pipes | in | output"

Nested fence demonstration (fence containing fences):

Here is a fenced block *that itself contains* a fenced block:

```js
console.log("inner fence");
```

And a tilde fence:

~~~txt
inner tilde fence
~~~

HTML blocks & mixed parsing

Inline HTML in a paragraph: This is bold via HTML and this is emphasis.

HTML block with nested tags.

Some renderers stop parsing markdown inside HTML blocks; others don’t.

Expandable section (details/summary)

Inside details: markdown should usually work on GitHub.

  • [ ] task in details
  • [x] another task
type Foo = { a: number; b?: string };
const foo: Foo = { a: 1 };

Raw HTML that might be sanitized (good to test your sanitizer rules):


Footnotes

Footnote reference in text: Here’s a claim that needs a source.[^1] Another footnote, same label reused later.[^1]

Multiple footnotes: alpha[^a] beta[^b] gamma[^c]

[^1]: Footnote one with a link: https://example.com and code.

[^a]: Footnote A.

[^b]: Footnote B with emphasis.

[^c]: Footnote C with a list:

  • item 1
  • item 2

Oddities & fuzz section

Bracket soup:

Quotes and punctuation adjacency:

  • bold...then text
  • ~~strike~~,bold,code,italic;end.

Unicode + bidi-ish content (rendering quirks):

  • café naïve coöperate
  • emojis: 🧠⚡️🍕
  • combining: é (e + ◌́) vs é (single char)
  • RTL snippet: مرحبا بالعالم

Long-ish line (wrapping/perf):

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa


Final sanity checks

  1. A list item ending with two spaces should hard-break. This should be the next line in the same list item.

  2. A paragraph after a list:

This should not be part of the list above.


EOF

</details>
 
Defunct:

- ~~for images, `var(--aspect-ratio)` is not defined (this is what prevents layout shift)~~
   - edit: this might be a problem in dev because I wasn't using imgproxy's paid version ... need to double check that
   - confirmed that it's a paid version thing
- ~~image grids when width and height are set have strange spacing~~
   - ~~I think width and height might be getting switched somewhere - the portrait oriented images have a width > height for some reason~~
   - the problem was my browser cache


huumn avatar Dec 15 '25 22:12 huumn

Adding to the nits:

  • nested checklists turn into abstract art (lexical vs prod)
    • too much spacing image

Addressing nits:

  • something affecting heights of the media containers didn't port over well afaict
image

This is kind of hard to QA, I'm trying to reproduce but the behavior is 1:1 with prod, mhh...

Soxasora avatar Dec 16 '25 10:12 Soxasora

only curious: useCallbackRef what's it being used for?

The useCallbackRef is needed for Lexical because we dynamically load the Reader, which means that its ref isn't available immediately on mount. It's just an helper for a pattern we would have instead reproduced in many places!

a plan to remove or actual removal of LegacyText

We can remove it, but I'd like to do it in a child PR so that we can rollback safely in the ultra-remote case something happens.


Changes:

  • media ends up with both the old mediaContainer css classes and the new classes
    • now MediaNode handles styling
  • {:toc} renders with details collapsed, then pops open causing layout shift
    • At first I wanted to make the table of contents open by default... but it can be big, so now they're closed by default
  • links to headings referencing the same item (self-created) open in a new tab
    • there are now checks for internal/hash links in MDAST
    • Lexical doesn't support next/link which means that the show full button wasn't being triggered on hash route changes. So now there's a LinksPlugin that captures clicks to links and uses next/router to navigate instead of the default browser behavior.
  • replacing html transformation step with context aware SSR rendering
    • ItemContextExtension now receives outlawed, rel, imgproxyUrls via the lexicalStateLoader
      • replaces links, media, embeds with plain text
      • gets dimensions and sources from imgproxyUrls
        • these dimensions are used by MediaNode to set width, height
          • NOTE: I couldn't test this in dev as imgproxy only returns the srcSet
        • sources are used by MediaNode to set srcSet, bestResSrc

Remaining steps:

  • READMEs
  • removal of dead code

Soxasora avatar Dec 16 '25 16:12 Soxasora

lmk when you feel like this is ready to be merged and I'll give it a final QA/review.


This is kind of hard to QA, I'm trying to reproduce but the behavior is 1:1 with prod, mhh...

Then that regressed at some point. Not on the menu for this PR then!

The useCallbackRef is needed for Lexical because we dynamically load the Reader, which means that its ref isn't available immediately on mount.

That makes sense. I mostly wanted my suspicions confirmed.

We can remove it, but I'd like to do it in a child PR so that we can rollback safely in the ultra-remote case something happens.

Sounds good to me!

huumn avatar Dec 16 '25 17:12 huumn

[!WARNING] Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm linkedom is 92.0% likely obfuscated

Confidence: 0.92

Location: Package overview

From: package-lock.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

socket-security[bot] avatar Dec 17 '25 11:12 socket-security[bot]

I'm missing some READMEs (components and server), but other than that, this PR feels ready to be reviewed. I’ll finish writing them ASAP.

Soxasora avatar Dec 18 '25 17:12 Soxasora

fyi i'm seeing a regression on triple click

huumn avatar Dec 18 '25 19:12 huumn

Ah, forgot to do something about it! This will probably involve creating our own PlainTextExtension.

Soxasora avatar Dec 18 '25 19:12 Soxasora

Oh I thought you fixed it in some commit. I must've not tried to recreate it hard enough

huumn avatar Dec 18 '25 19:12 huumn

I'm seeing what looks like a placeholder in srcset

Screenshot 2025-12-18 at 1 36 25 PM

huumn avatar Dec 18 '25 19:12 huumn

I'm seeing what looks like a placeholder in srcset

Forgot to destructure format alongside dimensions, video to exclude it from the srcSetObj. Regressed because of a cleanup (was indeed asking myself why format was there unused)


Okay, didn't end up creating our own Mode: we just had to restore the paragraph commands overridden by PlainTextExtension. Then on the import side, since we can't use Lexical's full markdown normalizer in plain text, I just implemented \n -> linebreak and \n\n -> paragraph

Soxasora avatar Dec 18 '25 23:12 Soxasora

Okay, didn't end up creating our own Mode: we just had to restore the paragraph commands overridden by PlainTextExtension.

Works for me now. (Also there's a debug console.log in utils that you missed.)

Forgot to destructure format alongside dimensions, video

Cool, not getting warnings in console anymore.


I've done another QA/review. My plan is to merge/deploy this early tomorrow (my) morning.

Should I also plan to merge removing LegacyText?

huumn avatar Dec 18 '25 23:12 huumn

I finally got to review the SSR fallback to understand what cursorbot was saying, and it did seem like Lexical was being recreated on every render. It wasn't a noticeable thing, because the module is cached, but it was an oversight.

My plan is to merge/deploy this early tomorrow (my) morning.

I'll be ready 🫡

Should I also plan to merge removing LegacyText?

It does make up for the extra bundle, so I would also merge the cleanup PR. I'll continue to QA the cleanup to ensure that I didn't break anything while removing libraries.

Soxasora avatar Dec 19 '25 00:12 Soxasora

btw I think the newline/paragraph stuff broke the gallery node thing

and it looks like because media nodes are wrapped in paragraphs, they appear to have more padding because of line-height

at least we know we're at the stage where everything left is some tiny bug fix creating new ones lol

huumn avatar Dec 19 '25 00:12 huumn