I had wanted to be able to keep notes on my computer in a basic text file, including math symbols, which is not simple, and sometimes not possible, even using the unicode character set. Then I discovered MathJax. HTML, with extensions like MathJax, seems to be the most general solution to generating free-form notes from an ASCII keyboard. To that goal, I began something like this real-time html editor over a decade ago, but never followed-through with loading and saving the edited files, so this solution is very much appreciated!
Supposing that MathJax is being used in an html page being edited, there arises the complication of writing dynamic content. MathJax normally runs its typesetting function only once, when the page first loads. Dynamic MathJax content requires additional code, and would be a very nice feature to have here.
It turns out that this can be addressed exclusively in the user's document, but can be handled much more elegantly by adding a "gating" function to HTML Editor which checks for the presence of MathJax and delays the document rendering until the user moves their mouse cursor into, or outof and into, the render iframe. There is further discussion at https://github.com/mathjax/MathJax/issues/3094, Scroll Position corruption.
Reading at MathJax in Dynamic Content, and then at Handling Asynchronous Typesetting, these considerations only apply to a document being modified in place - which is not what is happening here.
After looking more carefully at this real-time editor, the editor itself simply loads the edited document and then the browser renders this version as an entirely new document. The real-time editor is not making incremental changes to the Document Object Model, and there is no "extra" work needed from the real-time editor to manage MathJax.
Still, to avoid re-running the MathJax typesetting function after every character entry, it is easiest to simply uncheck the "Run" check box during editing, and then enable "Run" again to render the modified document, including running the MathJax typesetting function, as always.
An alternative - which requires no change to the real-time editor - involves modifying the user document, with the advantage of not needing to bother with the "Run" checkbox. The idea is to move the mouse cursor outside of the iframe, then run the MathJax typesetting function once and only after the mouse cursor moves back onto the rendered document iframe, using some MathJax code from Configuring and Loading in One Script. This simply requires modifying the user document MathJax head
element, for instance, like this:
<head>
...
<!--
<script type="text/javascript" id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js">
</script>
-->
<script>
document.querySelector('html').onmouseenter = function() {
var scriptx = document.createElement('script');
scriptx.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js';
// scriptx.src = 'mathjax/tex-chtml-full.js';
scriptx.async = true;
document.head.appendChild(scriptx);
};
</script>
...
</head>
The biggest issue I notice with HTML Editor is that whenever the textarea
source is edited, the display position in the rendered iframe
jumps to the top of the page. This has the effect of the user never being able to see what has just been edited without having to repeatedly scroll back to the original edit position, which is tiresome in a long document. The solution is to save, and then restore, the iframe
scroll position in the preview()
function.
When running MathJax, restoring the scroll position is not perfect. The resulting scroll position will be close to the same position as before the editing, after MathJax has finished its typesetting, but the position will not be the same as before. It turns out that this is because simply saving and restoring the scroll position will position the user's document before MathJax has finished its typesetting.
The more elegant solution requires modifying HTML Editor as mentioned above, in addition to adding the proper MathJax header into the user's document. The modification to HTML Editor, including the MathJax "gating" function and changing the name of the original preview()
function to ifpreview()
, looks like this:
...
var mjax = false;
function preview() {
if (!mjax) {
ifpreview();
} else {
console.log("mjax = " + mjax);
document.querySelector('iframe').addEventListener("mouseenter", ifpreview);
}
}
function ifpreview() {
if (run.checked) {
let myIframe = document.querySelector('iframe');
let xscroll = myIframe.contentWindow.scrollX;
let yscroll = myIframe.contentWindow.scrollY;
console.log("ifpreview mjax = " + mjax);
const iframex = document.createElement('iframe'); // a fresh iframe to delete JavaScript variables
document.querySelector('iframe').replaceWith(iframex);
iframex.contentWindow.mjx = {xscroll, yscroll};
...
Altogether, the proper MathJax headers added in the user's document then look like this:
...
<script>
MathJax = {
// run scrollTo() a second time after MathJax typesetting is complete
startup: {
pageReady: () => {
return MathJax.startup.defaultPageReady().then(() => {
scrollTo({
left: mjx.xscroll,
top: mjx.yscroll,
behavior: "smooth"
});
console.log("Second Scroll: " + mjx.xscroll + " " + mjx.yscroll);
parent.mjax = true;
});
},
...
};
...
</script>
...
<script>
document.querySelector('html').onmouseenter = function() {
var scriptx = document.createElement('script');
scriptx.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js';
// scriptx.src = 'mathjax/tex-chtml-full.js';
scriptx.async = true;
document.head.appendChild(scriptx);
};
</script>
BTW, I would prefer the traditional movable iframe divider, over the "Textarea size" slider, which can be a bit "twitchy". A simple alternative, still using your clever frame divider solution, is to spread the slider across the entire screen. With a vertical divider, the slider knob and the divider bar track very closely, and the control is then not so "twitchy". This change can be done by leaving off the label
and output
elements, moving the input
element to last in the header, and using this style
:
<input type="range" id="textareaSize" style="width:100%;height:5px;">
The same could be done in horizontal mode, first creating a new position for the slider, outside of the header
, and adding orient="vertical"
to the input
style. I'm too lazy to work this out for now, and the full-width horizontal slider is good enough for both display modes.
Also, the footer links, except for "Feedback" and "Created by", are all getting jumbled on top of each other by Firefox Nightly on linux, which does not seem to handle the img alt
attribute gracefully. One alternative is to add float: left; overflow: hidden;
to the img
style. A better looking alternative is to move the img alt
attribute text into the anchor
content proper, after the img
tag.
Hmm - the coding style in the page is often using the same exact text string as 1) an HTML Element or 2) an element Attribute or 3) a Document Object Model Property for 4) an element ID, and 5) a javascript Variable. I find that this style can be misleading and prefer visually distinguishing the strings for an ID or Variable from an Element or Attribute or Property, and from each other.
In the "Reset" button function, the line download.download = 'template.html';
appears to be redundant, since this value has already been set in the corresponding Anchor Attribute. This value is not being changed globally by the "input file"/"Browse" button function, in download.download = file.name;
.
I find the Anchor tag content "Download" used in the page header to be misleading. The connotation of the term "Download" is that this link will transfer some data from a remote location to the browser. But in fact, the exact opposite function is being performed, where the local browser content is instead being sent to a "remote" location. In this case, the "local" content is being "downloaded" and the user, usually, is then prompted to save their local content to file.
A more intuitive moniker for this Anchor tag would be "Save", rather than "Download", preferably styled as a button, in similarity to the other function buttons in the page header.
This can be done by simply replacing the Anchor content, as:
<a href="" download="template.html" title="Save your HTML document" id="download"><button>Save</button></a>
Following is a version of the Real-Time HTML Editor with my changes, including the code to restore scroll position while editing. Running the code snippet here will not actually render the document in the iframe, so, to use it, the code needs to be recombined, with CSS in the <head>
, between <style>...</style>
tags, and the javascript inside the <body>
, between <script>...</script>
tags. You can play with the slider, though.
const run = document.getElementById('run'),
textarea = document.querySelector('textarea'),
download = document.getElementById('download'),
chooseFile = document.getElementById('chooseFile');
var mjax = false;
function preview() {
if (!mjax) {
ifpreview();
} else {
console.log("mjax = " + mjax);
document.querySelector('iframe').addEventListener("mouseenter", ifpreview);
}
}
function ifpreview() {
if (run.checked) {
let myIframe = document.querySelector('iframe');
let xscroll = myIframe.contentWindow.scrollX;
let yscroll = myIframe.contentWindow.scrollY;
console.log("ifpreview mjax = " + mjax);
const iframex = document.createElement('iframe'); // a fresh iframe to delete JavaScript variables
document.querySelector('iframe').replaceWith(iframex);
iframex.contentWindow.mjx = {
xscroll,
yscroll
};
const iframeDoc = iframex.contentDocument;
iframeDoc.write(textarea.value);
iframeDoc.close();
document.querySelector('iframe').contentWindow.scrollTo({
left: xscroll,
top: yscroll,
behavior: "instant"
});
}
}
textarea.addEventListener('input', preview);
run.addEventListener('change', preview);
for (const eventx of ['click', 'contextmenu']) {
download.addEventListener(eventx, function() {
const blob = new Blob([textarea.value], {
type: 'text/html; charset=utf-8'
});
this.href = URL.createObjectURL(blob);
});
}
document.getElementById('fontSize').addEventListener('change', function() {
textarea.style.fontSize = this.value + 'px';
});
document.getElementById('reset').addEventListener('click', function() {
if (!textarea.value || textarea.value != textarea.defaultValue && confirm('Your input will be lost.\nAre you sure you want to reset?')) {
chooseFile.value = '';
textarea.value = textarea.defaultValue;
preview();
}
});
document.getElementById('select').addEventListener('click', function() {
textarea.select();
});
chooseFile.addEventListener('change', async function() {
const file = this.files[0];
if (file) { // to ensure that there's a file to read so Chrome, for example, doesn't run this function when you cancel choosing a new file
download.download = file.name;
textarea.value = await file.text();
preview();
}
});
document.getElementById('textareaSize').addEventListener('input', function() {
const range = this.value;
document.getElementById('textareaWrap').style.flexGrow = range;
document.getElementById('iframeWrap').style.flexGrow = 100 - range;
});
document.getElementById('viewsToggle').addEventListener('change', function() {
document.querySelector('main').classList.toggle('horizontal');
});
document.getElementById('themesToggle').addEventListener('change', function() {
textarea.classList.toggle('dark');
});
document.getElementById('footerToggle').addEventListener('click', function() {
this.classList.toggle('on');
document.querySelector('footer').toggleAttribute('hidden');
});
document.getElementById('copy').addEventListener('click', function() {
navigator.clipboard.writeText(location);
function toggleNotification() {
document.getElementById('notification').toggleAttribute('hidden');
}
toggleNotification();
setTimeout(toggleNotification, 1500);
});
window.addEventListener('beforeunload', function(event) {
if (textarea.value && textarea.value != textarea.defaultValue) {
event.preventDefault();
event.returnValue = '';
}
});
preview();
html,
body {
margin: 0;
padding: 0;
height: 100%;
background: #777;
}
body {
display: flex;
flex-direction: column;
}
header,
footer:not([hidden]) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
padding: 5px;
}
header {
background: linear-gradient(#FFF, #CCC);
}
label,
#download,
#fontSize,
#reset,
#select,
#chooseFile,
output,
span {
font: bold 11px Arial;
color: #333;
}
[type="checkbox"] {
margin: 0 5px 0 0;
}
[for="fontSize"] {
margin-left: 5px;
}
#fontSize,
button,
#textareaSize {
margin: 0;
}
#chooseFile {
margin: 0 auto 0 0;
}
#textareaSize,
iframe {
padding: 0;
}
output {
margin-right: 5px;
font-family: monospace;
}
#footerToggle {
width: 16px;
height: 16px;
border: 1px solid #666;
border-bottom-width: 5px;
padding: 0;
background: transparent;
}
#footerToggle.on {
border-color: #333;
background: #FFF;
}
main {
flex: 1;
display: flex;
background: #777;
}
main.horizontal {
flex-direction: column;
}
div {
flex: 0px;
position: relative;
}
div * {
position: absolute;
width: 100%;
height: 100%;
margin: 0;
border: 0;
background: #FFF;
}
#iframeWrap {
border-left: 5px solid #CCC;
}
main.horizontal #iframeWrap {
border-left: 0;
border-top: 5px solid #CCC;
}
textarea {
box-sizing: border-box;
padding: 5px;
outline: 0;
resize: none;
font-size: 14px;
color: #333;
}
textarea.dark {
background: #333;
color: #FFF;
}
footer {
background: linear-gradient(#CCC, #FFF);
}
img {
display: block;
float: left;
overflow: hidden;
}
#copy {
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
}
address {
margin-left: auto;
font: italic 16px 'Times New Roman';
color: #333;
}
address a {
color: inherit;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Edit your HTML, CSS, and JavaScript code and monitor the instant live preview.">
<title>HTML Editor: online HTML editor with real-time preview</title>
<link rel="icon" href="favicon.ico">
</head>
<body>
<header>
<label for="run">Run</label>
<input type="checkbox" id="run" checked>
<a href="" download="template.html" title="Save your HTML document" id="download"><button>Save</button></a>
<label for="fontSize">Font size</label>
<select id="fontSize">
<option>12</option>
<option>13</option>
<option selected>14</option>
<option>15</option>
<option>16</option>
<option>17</option>
<option>18</option>
<option>19</option>
<option>20</option>
</select>
<button type="button" id="reset">Reset</button>
<button type="button" id="select">Select</button>
<input type="file" accept="text/html" id="chooseFile">
<label for="viewsToggle">Horizontal view</label>
<input type="checkbox" id="viewsToggle">
<label for="themesToggle">Dark theme</label>
<input type="checkbox" id="themesToggle">
<button type="button" title="Toggle footer" id="footerToggle"></button>
<input type="range" id="textareaSize" style="width:100%;height:5px;">
</header>
<main>
<div id="textareaWrap">
<textarea spellcheck="false">
<!doctype html>
<html lang="en">
<head>
<title>HTML Document Template</title>
<style>
p {
font-family: Arial;
}
</style>
</head>
<body>
<p>Hello, world!</p>
<p>While editing documents containing MathJax markup, uncheck the "Run" checkbox, or run MathJax
typesetting conditionally, by moving the mouse cursor into, or outof and into, the rendered page window, and
including the following Javascipt in the head of your document:</p>
<div style="background: #f7fff7">
<pre>
&lt;head>
...
&lt;script>
MathJax = {
// run scrollTo() a second time after MathJax typesetting is complete
startup: {
pageReady: () => {
return MathJax.startup.defaultPageReady().then(() => {
console.log("Second Scroll: " + mjx.xscroll + " " + mjx.yscroll);
scrollTo({
left: mjx.xscroll,
top: mjx.yscroll,
behavior: "smooth"
});
});
}
},
...
};
...
&lt;/script>
...
&lt;script>
document.querySelector('html').onmouseenter = function() {
var scriptx = document.createElement('script');
scriptx.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js';
// scriptx.src = 'mathjax/tex-chtml-full.js';
scriptx.async = true;
document.head.appendChild(scriptx);
};
&lt;/script>
...
&lt;/head>
</pre>
</div>
<p>Test your browser's scroll position stability while editing by watching the position of these long
lines:</p>
aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>aa<br>
ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>bb<br>
<script>
console.log(document.querySelector('p').textContent);
</script>
</body>
</html>
</textarea>
</div>
<div id="iframeWrap">
<iframe></iframe>
</div>
</main>
<footer hidden>
<span>Share</span>
<a href="https://twitter.com/intent/tweet?text=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview&url=https%3A%2F%2Fhtmleditor.gitlab.io" target="_blank">
<img src="images/twitter.svg" width="16" height="16">Twitter</a>
<a href="https://www.facebook.com/sharer.php?u=https%3A%2F%2Fhtmleditor.gitlab.io&t=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview" target="_blank">
<img src="images/facebook.svg" width="16" height="16">Facebook</a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fhtmleditor.gitlab.io" target="_blank">
<img src="images/linkedin.svg" width="16" height="16">LinkedIn</a>
<a href="mailto:?subject=HTML%20Editor%3A%20online%20HTML%20editor%20with%20real-time%20preview&body=https%3A%2F%2Fhtmleditor.gitlab.io" target="_blank">
<img src="images/email.svg" width="16" height="16">Email</a>
<button type="button" id="copy">
<img src="images/link.svg" width="16" height="16">Link</button>
<span id="notification" hidden>Copied!</span>
<address><a href="https://codereview.stackexchange.com/questions/284918/html-editor-online-html-editor-with-real-time-preview"
title="Code Review Stack Exchange">Feedback</a> | Created by <a href="https://mori.pages.dev" rel="author">Mori</a></address>
</footer>
</body>
</html>
To address your question, and paraphrasing:
Including certain Math Symbols in a simple text file is not possible using the unicode character set. Can you provide an example?
A quick example of graphemes which are commonly seen in mathematical expressions and which are not to be found in the current Unicode character set can be found in "Proposal to encode missing Latin small capital and modifier letters in the UCS", dated 2011 March 12, https://www.unicode.org/L2/L2011/11208-n4068.pdf . The "missing" graphemes there have the notation "This character is proposed here." However, this proposal was never accepted by the Unicode Consortium.
In particular, consider "U+A7F3 MODIFIER LETTER SMALL Q" and "U+ABB5 LATIN SUBSCRIPT SMALL LETTER Q". These are the superscript and subscript versions of the lower case letter "q". This letter is commonly used, for instance, to express the signature of a Clifford Algebra, as a subscript "p,q,r", or might be used as abstract indexes of a Tensor. There are unicode code points for the subscript lower case letters "p" and "r", but nothing for the subscript letter "q". As a consequence, that abstract signature or Tensor index cannot be represented directly from the unicode character set.
Similarly, there is a unicode code point for the "dagger" grapheme, "U+2020 Dagger", but there is no such unicode code point for the "superscript dagger" grapheme, which might otherwise be used to indicate the "Reverse" of a Multivector Geometric Product. However, the Unicode Consortium does concede at times. For instance, there now exists a code point for the superscript lower case letter "q", "U+107A5 MODIFIER LETTER SMALL Q", and some others. But, subscript "q" is still not possible.
Just as with the principle distinction between "CSS vs HTML"/"style vs basis"/"skin vs skeleton", Unicode strives to encode only the basis of all written language, with only a single code point for every essential grapheme. In conflict, the historic foundation of Unicode also required that it subsume all then existing character encodings, to guarantee the complete fidelity of "forward and reverse"/"round-trip" conversions between Unicode and other competing encodings, leaving remnants of confusion, for which see https://en.wikipedia.org/wiki/Duplicate_characters_in_Unicode .
Many, but not all, people consider mathematical superscripts and subscripts to be strictly "style" and not ever "basis", especially because mathematical superscripts and subscripts may cascade, as, for instance, in
e - t/t0 .
Arguments about the use of unicode characters in mathematical expressions continue to the present day. For instance, https://corp.unicode.org/pipermail/unicode/2023-March/010472.html . There also exists a Unicode Consortium FAQ addressing the issue, https://unicode.org/faq/ligature_digraph.html#Pf8 , and reference https://unicode.org/reports/tr25 , which see. There are also numerous stackexchange/stackoverflow questions addressing the topic, for instance https://stackoverflow.com/questions/73409631/why-is-there-no-super-or-subscript-q-or-q-characters-defined-in-utf-8 .