Centre-align feature is not working
Noticed at v6.0.0
After some investigating, it seems that the issue stems from how text-center is injected by markdown-it-center-text.js. Markdown-it seems to wrap inline rules automatically with <p>. And markdown-it-center-text.js centers the text by wrapping the content with <div class="text-center">.
This leads to the result being <p><div class="text-center"> {content} </div></p>, which is considered as invalid HTML since <div> cannot be put inside <p>.
~~It seems like in Vue2, the final rendered page resolves this to <p></p><div class="text-center"> {content} </div>, i.e. the <div> is pushed out of the <p>. But in Vue3, it instead deletes the <div>, resolving to just <p></p>.~~
EDIT: Refer to https://github.com/MarkBind/markbind/issues/2716#issuecomment-2975343570 for more updated explanation
There's also some strange behaviour regarding center-align and the 404 page. Our default 404 page uses center-align, so it also has the issue of its content being deleted (that's why https://markbind.org/no_page looks blank). But for some reason, if we go one directory deeper, the content renders correctly (e.g. https://markbind.org/subdir/no_page).
Thanks for looking into this, @AgentHagu
I've found a possible fix for this (ebcc339). If we update the center-text to be a block rule rather than an inline rule, the <div> would no longer be wrapped by a <p>, fixing the issue of missing <div> content.
For example, the following code:
Before
-> Test text <-
After
Now renders as expected:
The 404 page seems to render properly too
However, there also seems to be some regression with this fix. Particularly, it looks like there needs to be an empty line before the center-text now for it to work. So:
Hello world
-> Should be centered <-
will not work because there isn't an empty line before it. Looking at our user guide, it seems that this wasn't the case previously. This fix also doesn't seem to directly address the strange behavior mentioned previously or how it cropped up after the Vue migration.
Particularly, it looks like there needs to be an empty line before the center-text now for it to work.
@AgentHagu The parser we use (and IIRC, the Markdown spec) expects a blank line above a markdown block for the block to be parsed as markdown. Otherwise it will be parsed as plain text.
This will not work:
<div>
some text
**abc**
</div>
But this works:
<div>
some text
**abc**
</div>
I see, seems like an unfortunate regression from using the block rule for an inline rule 😢
I'll try to look further into the Vue side of things.
I've looked further into this and it seems to be closely tied to Vue's SSR and client-side hydration.
In the server-rendered HTML files, the center-align feature creates the following invalid HTML structure:
<p><div class="text-center"> {content} </div></p>
Because this is invalid HTML, browsers automatically correct this by closing the <p> before the <div> and reopening a new <p> to match the closing </p> at the end, resulting in:
<p></p><div class="text-center">{content}</div><p></p>
This corrected structure is reflected in the browser's DOM before hydration. We can see this by disabling Javascript in the browser temporarily, stopping the hydration script. When Javascript is re-enabled and Vue attempts to hydrate the DOM, it uses the original SSR markup, which expects a <p> with a <div> child:
<p><div class="text-center">{content}</div></p>
Vue detects that the actual DOM structure doesn't match the virtual DOM. It incorrectly detects the sibling <div> and <p> as unexpected nodes. As a way to fix this hydration mismatch, it removes these unexpected nodes, causing the <div> with the centered content to be deleted. That's why after re-enabling Javascript, we see that we're left with only a single <p></p>.
There's also some strange behaviour regarding center-align and the 404 page. Our default 404 page uses center-align, so it also has the issue of its content being deleted (that's why https://markbind.org/no_page looks blank). But for some reason, if we go one directory deeper, the content renders correctly (e.g. https://markbind.org/subdir/no_page).
Regarding this behavior, looking at the console seems to suggest that client-side hydration failed due to undefined render function. This in turn prevents the <div> from being deleted by hydration.
Another fix I've thought of that doesn't introduce any obvious regressions is to modify markdown-it-center-text.js to wrap the current <div> with </p><p> to quickly close the <p></p> that inline-rules automatically attaches. This will result in the following valid HTML:
<p></p><div class="text-center">{content}</div><p></p>
This way, there is no invalid HTML that the browsers will have to adjust and there will not be any hydration mismatch. It also keeps the center-align feature as an inline-rule.
@gerteck, since you're familiar with Vue3's SSR and hydration, do you know if its possible to adjust the hydration process to not delete these extra nodes, or any other possible fixes for this issue?
Also, we may need to update the developer guide's explanation of SSR as it seems to refer to Vue2's hydration mismatch behavior (i.e. Vue2 bailing out of hydration, relying on Client-side rendering vs Vue3 patching the DOM to match vDOM)
@AgentHagu Thank you for the detailed explanation!
I had also did some preliminary investigation a while back and realized it was something to do with div and p and HTML specifications. Unfortunately I got a bit busy and haven't had time to further look into this. Thanks for the thorough investigation!
Unfortunately, my understanding of Vue's hydration behavior does not go far beyond the documentation:
https://vuejs.org/guide/scaling-up/ssr.html#hydration-mismatch
When Vue encounters a hydration mismatch, it will attempt to automatically recover and adjust the pre-rendered DOM to match the client-side state. This will lead to some rendering performance loss due to incorrect nodes being discarded and new nodes being mounted, but in most cases, the app should continue to work as expected. That said, it is still best to eliminate hydration mismatches during development.
- In fact, the first example div & p issue is listed as the first gotcha example in the vue hydration mismatch docs, which supports your findings.
To confirm and reiterate the cause of the issue:
It seems that the different behavior under the hood is that Vue 3 hydration tries to recover, causing this new issue of the div being deleted. (If i understand it correctly). Previously, since the browser corrects it, it would just bail and hence show the corrected version.
- Vue 3 doesn’t "bail out" like Vue 2.
- Instead, it tries to patch the DOM to match the virtual DOM.
- Vue sees the < div > is not a child of < p > , so it considers the < div > "unexpected".
- Vue removes the unexpected nodes (i.e. < div class="text-center">).
Is it also right to say that fixing this would also fix the missing 404 page?
Possible solns?
To answer your question
- I don't think we can easily adjust the hydration process, since the hydration process is quite opaque, and we are technically passing in wrong HTML specifications
- I agree that we should update our dev docs! Good catch! We can also add on more information for hydration issues as we work on resolve this issue
It seems that the different behavior under the hood is that Vue 3 hydration tries to recover, causing this new issue of the div being deleted. (If i understand it correctly). Previously, since the browser corrects it, it would just bail and hence show the corrected version.
Yep, your explanation is correct! That <div> is seen as an unexpected sibling so Vue 3 patches the DOM by deleting it.
Is it also right to say that fixing this would also fix the missing 404 page?
Yes, currently the 404.md page uses the center-align -><- feature so it also suffers from the deleted <div>. We could implement a temporary fix for it by just replacing the use of -><- and directly using <div class="text-center">
I don't think we can easily adjust the hydration process, since the hydration process is quite opaque, and we are technically passing in wrong HTML specifications
I see, looks like we'll need to find a fix for the current invalid HTML then
Another fix I've thought of that doesn't introduce any obvious regressions is to modify
markdown-it-center-text.jsto wrap the current<div>with</p><p>to quickly close the<p></p>that inline-rules automatically attaches.
I've managed to implement this and I think it fixes the issue now. By keeping track of the leftmost and rightmost valid -> and <- indexes, we only need to insert </p> and <p> once to close off the <p></p> that markdown-it automatically wraps our content with.
This fix should not have any breaking changes or regressions. I've tested it with both the user guide and the 404 page and some other tests, e.g.:
However, there will now be redundant empty paragraphs in the site (although there shouldn't be too many unless authors excessively use the center-align feature)
One potential long-term fix is to change the center-text plugin to output a span instead of a div, since it's an inline rule and should ideally emit inline elements. This would avoid invalid HTML and Vue hydration issues caused by placing div inside p.
However, this would be persisting the breaking change, especially for existing sites like our 404 pages, which rely on block-level elements (e.g. -><p style="font-size: 10rem">404</p><-).
As a short-term workaround to maintain backwards compatibility, we could consider injecting the closing </p> tag before and reopening it after the center-text block as per @AgentHagu suggestion, to avoid nesting block elements improperly. But I wonder if this might cause issues in edge cases.
We’ll need to balance correctness with backward compatibility, and also maybe will have to investigate other markdown-it plugins to see if they have similar undetected issues if possible.