fast-reflexes / better-react-mathjax

MIT License
124 stars 16 forks source link

MathJax with A Markdown Editor like in this Example inside #35

Closed tanmayaBiswalOdiware closed 1 year ago

tanmayaBiswalOdiware commented 1 year ago

Link to the Stackblitz example

As you can see from the example, I have an EasyMDE instantiated. Following which I initialized better-react-mathjax package and wrapped it around the Editor.

Now that is obviously not the way to go, since I just want to render the Math in the preview mode of the Editor (you can see the 👁‍🗨 icon in the toolbar) but I just wanted to see what happens. Well nothing happened haha.

So any clues as to what I could do from here? Here is a link I found while kicking around on the internet - https://gist.github.com/chooco13/c280c1cc6584c97af85307028ecaebb1 It has SimpleMDE (an origin branch of EasyMDE) configured with MathJax package. But these are not in React.

Any ideas would be helpful here.

fast-reflexes commented 1 year ago

Hi there! Yeah, what is done in that piece of code in the second link should basically happen automatically in the first link so I'm not sure what's going on... I will check it during the weekend.. I'm positive there is a slick way to accomplish it :)

tanmayaBiswalOdiware commented 1 year ago

Ayo, thanks for the heads up. I will keep looking into it but I am not sure if I would find out much. Will be waiting to hear from you still. Thanks for the good work!

fast-reflexes commented 1 year ago

Ok I got a solution :) It's a little bit hacky but not too much :)

Context

All of these packages are kind of building on old-style Javascript where you import global variables and use them. By that I mean that both MathJax, EasyMDE and SimpleMDE work in this old-fashioned way and it seems that we're quite limited as to how we can interact with EasyMDE. Nonetheless, it's possible via the previewRender hook that you showed in one of your links.

Initial problem

Normally, better-react-mathjax typesets automatically any content it finds. The current usecase is non-standard in many ways:

Solution

We CAN hook into the process using the previewRender like you show in your second link. However, there are quite a few things that differ given the use of React instead of raw Javascript. For once, we can't access the EasyMDE / SimpleMDE instance using this.parent and the problem is that if we replace the previewRender option with our own custom renderer, then the markdown will not be rendered as it should which we ALSO want, besides the MathJax typesetting. However, there is the option in SimpleMdeReact to access the EasyMDE instance which we then can use. The result is a bit verbose because:

previewRender: (text) => {
    // first process the markdown
    const markdownHtml = easyMde.current.markdown(text);

    // typeset with MathJax
    const container = document.createElement('div');
    container.innerHTML = markdownHtml;
    props.mathJax.current.typesetClear([container]);
    props.mathJax.current.typeset([container]);
    return container.innerHTML;
}

Initially both props.mathJax.current and easyMde.current will be null but by the time you hit preview, they will both be set. Otherwise you could add some conditional to handle if they, against all odds, should not be set.

Here is a sandbox that works: https://codesandbox.io/s/better-react-mathjax-35-v8fg5z

Note, I don't have the sandbox system (Stackblitz) that you use and I wasn't too keen to create a new account so I just anonymously used a forked sandbox from your example and then I ported it to CodeSandbox instead. However, there seems to be some problem there using react-simplemde-editor version 5.2.0 so I had to downgrade it. Should work in any real scenario with the updated version though (and it worked in the anonymous sandbox I used at Stackblitz). Also, I added the MathJaxContext component to the parent of SimpleEditor since the MathJaxContext component shouldn't rerender all the time, it should ideally just render once.

Let me know if this is ok and close the ticket if so.

tanmayaBiswalOdiware commented 1 year ago

First - I award you the MVP of the year. This is a lot of work you did. I appreciate the effort.

Second - No, I don't care which sandbox you use if you were willing to help. I actually thought you will prefer codesandbox, but like you mentioned, there is some problem with codesandbox in the latest react-simplemde-editor so I switched to stackblitz.

And I dont find it hacky at all! I mean sure, these component don't use the React logic anymore, but there is only so much React that could go around hahaha. I really debated that I should just download the MathJax from the CDN but I guess I wanted the 'better' package! Hah, get it. lol anyway...

I don't know if you opened my sandbox again, because I did end up in the same boat as you - use the previewRender property. But accessing previewRender meant I need to get that MathJax instance. As you pointed out, there is a way to get the instance with MathJaxBaseContext but sadly I couldn't understand how to initialize it in my context. Smh. But you also figured out why the wrapping did not work. Its because of useEffect. Makes sense. And you actually used the easymde's previewer. Because I straight up cut off its default renderer to just import marked package myself to try and render the markdown with my own config. but again, I did not have the Mathjax instance, so that's where I stopped.

So bonus points for not using more package than needed! 🎉 I will close the ticket. Let me just kick around the code a bit and try to see how it works.

Hope this thread also helps others with integrating this awesome package with other Editors.

tanmayaBiswalOdiware commented 1 year ago

Yep, it works with multiple instances of easymde too. Sweet. Now my next objective is to add Math-quill to the toolbar for quick/easy input of math expressions. But I wont be ruining your weekend anymore. Thanks again for the huge help! Happy holidays!

fast-reflexes commented 1 year ago

Super glad we got it to work and thanks for the kind words too!

I did use the MathJaxBaseContext like this first:

const mathJax = useRef(null)
const mathJaxContext = useContext(MathJaxBaseContext)

useEffect(() => {
    if(mathJaxContext)
        mathJaxContext.promise.then(mathJaxObject => {  
            mathJax.current = mathJaxObject
        })
}, [mathJaxContext])

and it worked too but it was more verbose than the solution using the onStartup callback so preferred that one :)

Thanks for checking in and good luck with the project! Think this thread might come in handy for others too ;)

tanmayaBiswalOdiware commented 1 year ago

Yea, I missed the part where I had to wait for the promise to be fulfilled. Just tried to shove the context and get the result.

That would be a good pointer for anyone who follows the thread. I also did not know I should be using the typeset property. Probably because I don't understand the complete package too well.

tanmayaBiswalOdiware commented 1 year ago

Hey, I am back. I do not want to re-open the issue yet, but just wanted a clarification regarding this context approach we took.

<MathJaxContext
    onStartup={(mathJax) => {
        mj.current = mathJax;
        console.log("entering");
    }}
    config={config}
>

As you can see, I tried to log when onStartup is getting triggered, and it seems like its not 'starting up' when I redirect from another component. It starts up fine when I directly go into the page, but if I use react-router to navigate to the page where MathJaxContext is wrapped, it doesn't start. Meaning there is no typeset property for the children to access -> hence app crash.

The useEffect technique did not work either. 🤷‍♂️

Should I just wrap the MathJaxContext at the base of the app, like the place where I wrap the BrowserRouter?

fast-reflexes commented 1 year ago

You can do it in two ways:


    // you can use state instead of ref under some circumstances, it all depends on how the rest renders
    const [mj, setMj] = useState()

    <MathJaxContext .... onStartup={ setMj }>
        <MyMathJaxContext.Provider value={ mj }
            <App>
        </..>
    </..>

Then use the context from all your components.

No matter how you do it, if you include the MathJaxContext, then at SOME point, the onStartup callback should fire, but it will never fire AGAIN once it has fired once so if you include your MathJaxContext in some place without the onStartup callback and then include it WITH the callback, then the callback won't fire because MathJax has already been downloaded (it only downloads once during the lifetime of the app). That's why you should really only keep one MathJaxContext wrapping your entire app (like BrowserRouter and many other similar components).

Given your usecase, I would use the MathJaxBaseContext solution because it requires less changes outside the editor component even though it's slightly more verbose.

tanmayaBiswalOdiware commented 1 year ago

Then use the context from all your components.

OR.. use the other method with MathJaxBaseContext. Here is a sandbox showing that method: https://codesandbox.io/s/better-react-mathjax-35-b-02qz7e

That's the thing, for some reason MathJaxBaseContext inside useEffect did not work like I expected.

...callback should fire, but it will never fire AGAIN once it has fired once so if you include your MathJaxContext in some place without the onStartup callback and then include it WITH the callback, then the callback won't fire because MathJax has already been downloaded (it only downloads once during the lifetime of the app). That's why you should really only keep one MathJaxContext wrapping your entire app (like BrowserRouter and many other similar components).

As I suspected. So right now, I just drilled some props from the top of the App. This old outdated project is not helping my case either, with class components written all over so I can't even use React hooks (like useRef) so just going to do the job with states.

Yea I should be using useContext Hook more, but drilling the props is just so trivial lol. It seems to work okay now, but I may holler at you once again. Hopefully not though.