-
Notifications
You must be signed in to change notification settings - Fork 106
Callbacks and MathJax
After a post decrypts, you may need to re-initialize page-level libraries that scanned the DOM at load time:
- MathJax / KaTeX — re-typeset newly-revealed math
- highlight.js / Prism — re-highlight newly-revealed code blocks
- mermaid / plantuml — render diagrams
- Table-of-contents widgets — refresh from the now-visible headings
- Lazy-loaders — observe newly-visible images
The plugin gives you two hooks for this.
The browser bundle dispatches a hexo-blog-decrypt CustomEvent on window immediately after decryption succeeds.
Put this in your theme's layout (so it runs on every page, not just decrypted ones):
<script>
window.addEventListener('hexo-blog-decrypt', function (e) {
var mode = (e && e.detail && e.detail.mode) || 'unknown';
console.log('[hbe] decrypted, mode:', mode);
});
</script>e.detail.mode is 'manual' for a fresh password decrypt or 'cached' when an opt-in autoSave reload auto-decrypts. v3-style listeners that ignore detail keep working unchanged — the event fires either way.
Live demo: /demo/mathjax/ (password hello).
You can put MathJax loading either in the page layout (loads on every page) or inside the encrypted body (loads only after decryption — recommended for math-heavy locked posts to avoid the cost on every page).
The cleanest pattern uses MathJax 3's auto-typeset on the decrypted DOM:
<!-- 1. Configure MathJax before it loads -->
<script>
window.MathJax = {
tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] },
startup: { typeset: true }
};
</script>
<!-- 2. Load MathJax (async, from CDN) -->
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" async></script>Place the two <script> tags inside the encrypted post body. The browser bundle re-creates <script> tags inside decrypted HTML as live nodes (see "How scripts in encrypted posts work" below), so MathJax loads only after the user enters the right password and finds the math the moment it's ready.
If MathJax is loaded site-wide and you only need it to re-typeset the decrypted post:
<script>
window.addEventListener('hexo-blog-decrypt', function () {
if (window.MathJax && window.MathJax.typesetPromise) {
window.MathJax.typesetPromise();
}
});
</script>If hljs ran at page load and missed the encrypted code blocks:
<script>
window.addEventListener('hexo-blog-decrypt', function () {
if (window.hljs) {
document
.querySelectorAll('#hexo-blog-encrypt pre code')
.forEach(function (el) { window.hljs.highlightElement(el); });
}
});
</script>The live callback demo at /demo/callback/ uses:
<script>
window.addEventListener('hexo-blog-decrypt', function (e) {
var mode = (e && e.detail && e.detail.mode) || 'unknown';
console.log('[demo] hexo-blog-decrypt fired, mode:', mode);
window.alert('Decryption callback fired! mode = ' + mode);
});
</script>Type the password → an alert() pops up → click OK → content reveals. Confirms the listener saw the event.
Anything you put in the encrypted markdown body — including <script> tags — is re-executed when the post decrypts. The browser bundle's convertHTMLToElement helper re-creates encrypted <script> tags as live DOM nodes (the standard "innerHTML doesn't run scripts" workaround).
Use this when you want code that runs only for readers who unlock the post:
---
title: Locked Notes
password: hello
---
Below the cut is private.
<!-- more -->
# Private notes
Some private text.
<script>
// Only runs when this post is decrypted.
console.log('reader unlocked the notes');
</script>innerHTML = htmlString does not execute <script> tags by default — that's a browser security policy. The bundle works around it by:
- Setting
innerHTMLon a temporary wrapper to parse the HTML - Walking every
<script>in the wrapper, creating a fresh<script>element with the same attributes + text content, and replacing the parsed-but-inert script with it - Appending the wrapper to the DOM — at which point the new script nodes execute
Then the bundle dispatches hexo-blog-decrypt. Because the swap happens before the dispatch, listeners registered inside the body's <script> are present when the event fires.
Source: src/browser/dom.js.
If your theme renders a TOC, it'll be empty for encrypted posts because the headings are inside the ciphertext. Workaround for the landscape theme — edit themes/landscape/layout/_partial/article.ejs:
<% if(post.toc == true){ %>
<div id="toc-div" class="toc-article" <% if (post.encrypt == true) { %>style="display:none" <% } %>>
<strong class="toc-title">Index</strong>
<% if (post.encrypt == true) { %>
<%- toc(post.origin, {list_number: true}) %>
<% } else { %>
<%- toc(post.content, {list_number: true}) %>
<% } %>
</div>
<% } %>
<%- post.content %>post.origin holds the original markdown source which the plugin preserves on the post object — TOC generation reads from that instead of the (encrypted) post.content. Apply the equivalent edit to other themes.