Skip to content

Commit 85f7b00

Browse files
committed
Fix: edit doc blocks with untypeset math; retypeset when edits finish.
Fixes data corruption: typeset math was saved, instead of LaTeX for the math.
1 parent 6dd7e37 commit 85f7b00

File tree

2 files changed

+71
-35
lines changed

2 files changed

+71
-35
lines changed

client/src/CodeChatEditor.mts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
CodeMirror_load,
5454
CodeMirror_save,
5555
mathJaxTypeset,
56+
mathJaxUnTypeset,
5657
} from "./CodeMirror-integration.mjs";
5758
import "./EditorComponents.mjs";
5859
import "./graphviz-webcomponent-setup.mts";
@@ -301,11 +302,7 @@ const save_lp = async () => {
301302
const codechat_body = document.getElementById(
302303
"CodeChat-body",
303304
) as HTMLDivElement;
304-
window.MathJax.startup.document
305-
.getMathItemsWithin(codechat_body)
306-
.forEach((item: any) => {
307-
item.removeFromDocument(true);
308-
});
305+
mathJaxUnTypeset(codechat_body);
309306
// To save a document only, simply get the HTML from the only Tiny MCE
310307
// div.
311308
tinymce.activeEditor!.save();
@@ -316,7 +313,7 @@ const save_lp = async () => {
316313
mathJaxTypeset(codechat_body);
317314
} else {
318315
source = CodeMirror_save();
319-
await codechat_html_to_markdown(source);
316+
codechat_html_to_markdown(source);
320317
}
321318

322319
let update: UpdateMessageContents = {
@@ -332,7 +329,8 @@ const save_lp = async () => {
332329
return update;
333330
};
334331

335-
// Per[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform#examples),
332+
// Per
333+
// [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform#examples),
336334
// here's the least bad way to choose between the control key and the command
337335
// key.
338336
const os_is_osx =
@@ -357,7 +355,7 @@ const on_save = async (only_if_dirty: boolean = false) => {
357355
is_dirty = false;
358356
};
359357

360-
const codechat_html_to_markdown = async (source: any) => {
358+
const codechat_html_to_markdown = (source: any) => {
361359
const entries = source.doc_blocks.entries();
362360
for (const [index, doc_block] of entries) {
363361
const wordWrapMargin = Math.max(

client/src/CodeMirror-integration.mts

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -351,13 +351,13 @@ class DocBlockWidget extends WidgetType {
351351
// Handle both cases.
352352
const [contents_div, is_tinymce] = get_contents(dom);
353353
if (is_tinymce) {
354-
tinymce_singleton!.setContent(this.contents);
355354
ignore_next_dirty = true;
355+
tinymce_singleton!.setContent(this.contents);
356356
tinymce_singleton!.save();
357357
} else {
358358
contents_div.innerHTML = this.contents;
359+
mathJaxTypeset(contents_div);
359360
}
360-
mathJaxTypeset(contents_div);
361361

362362
// Indicate the update was successful.
363363
return true;
@@ -380,12 +380,8 @@ class DocBlockWidget extends WidgetType {
380380
destroy(dom: HTMLElement): void {
381381
// If this is the TinyMCE editor, save it.
382382
const [contents_div, is_tinymce] = get_contents(dom);
383-
// Revert the typeset math to its original form.
384-
window.MathJax.startup.document
385-
.getMathItemsWithin(contents_div)
386-
.forEach((item: any) => {
387-
item.removeFromDocument(true);
388-
});
383+
// Forget about any typeset math in this node.
384+
window.MathJax.typesetClear([contents_div]);
389385
if (is_tinymce) {
390386
const codechat_body = document.getElementById("CodeChat-body")!;
391387
const tinymce_div = document.getElementById("TinyMCE-inst")!;
@@ -396,12 +392,28 @@ class DocBlockWidget extends WidgetType {
396392

397393
// Typeset the provided node; taken from the [MathJax
398394
// docs](https://docs.mathjax.org/en/latest/web/typeset.html#handling-asynchronous-typesetting).
399-
export const mathJaxTypeset = (node: HTMLElement) => {
400-
window.MathJax.typesetPromise([node]).catch((err: any) =>
401-
console.log("Typeset failed: " + err.message),
402-
);
395+
export const mathJaxTypeset = async (
396+
// The node to typeset.
397+
node: HTMLElement,
398+
// An optional function to run when the typeset finishes.
399+
afterTypesetFunc: () => void = () => {}) => {
400+
try {
401+
await window.MathJax.typesetPromise([node]);
402+
afterTypesetFunc();
403+
} catch(err: any) {
404+
console.log("Typeset failed: " + err.message);
405+
}
403406
};
404407

408+
// Transform a typeset node back to the original (untypeset) text.
409+
export const mathJaxUnTypeset = (node: HTMLElement) => {
410+
window.MathJax.startup.document
411+
.getMathItemsWithin(node)
412+
.forEach((item: any) => {
413+
item.removeFromDocument(true);
414+
});
415+
}
416+
405417
// Given a doc block div element, return the contents div and if TinyMCE is
406418
// attached to that div.
407419
const get_contents = (element: HTMLElement): [HTMLDivElement, boolean] => {
@@ -412,10 +424,12 @@ const get_contents = (element: HTMLElement): [HTMLDivElement, boolean] => {
412424

413425
// Determine if the element which generated the provided event was in a doc
414426
// block or not. If not, return false; if so, return the doc block div.
415-
const event_is_in_doc_block = (event: Event): boolean | HTMLDivElement => {
416-
const target = event.target as HTMLElement;
427+
const element_is_in_doc_block = (target: EventTarget | null): boolean | HTMLDivElement => {
428+
if (target === null) {
429+
return false;
430+
}
417431
// Look for either a CodeMirror ancestor or a CodeChat doc block ancestor.
418-
const ancestor = target.closest(".cm-line, .CodeChat-doc");
432+
const ancestor = (target as HTMLElement).closest(".cm-line, .CodeChat-doc");
419433
// If it's a doc block, then tell Code Mirror not to handle this event.
420434
if (ancestor?.classList.contains("CodeChat-doc")) {
421435
return ancestor as HTMLDivElement;
@@ -442,12 +456,6 @@ const on_dirty = (
442456
const indent = indent_div.innerHTML;
443457
const delimiter = indent_div.getAttribute("data-delimiter")!;
444458
const [contents_div, is_tinymce] = get_contents(target);
445-
// Revert the typeset math to its original form.
446-
window.MathJax.startup.document
447-
.getMathItemsWithin(contents_div)
448-
.forEach((item: any) => {
449-
item.removeFromDocument(true);
450-
});
451459
tinymce_singleton!.save();
452460
const content = is_tinymce
453461
? tinymce_singleton!.getContent()
@@ -464,8 +472,6 @@ const on_dirty = (
464472

465473
current_view.dispatch({ effects });
466474

467-
// Re-typeset.
468-
mathJaxTypeset(contents_div);
469475
return false;
470476
};
471477

@@ -480,8 +486,8 @@ const DocBlockPlugin = ViewPlugin.fromClass(
480486
// so it can be edited. A simpler alternative is to do this in the
481487
// update() method above, but this is VERY slow, since update is
482488
// called frequently.
483-
focusin: (event: Event, view: EditorView) => {
484-
const target_or_false = event_is_in_doc_block(event);
489+
focusin: (event: FocusEvent, view: EditorView) => {
490+
const target_or_false = element_is_in_doc_block(event.target);
485491
if (!target_or_false) {
486492
return false;
487493
}
@@ -514,7 +520,11 @@ const DocBlockPlugin = ViewPlugin.fromClass(
514520

515521
// See if this is already a TinyMCE instance; if not, move it
516522
// here.
517-
if (!is_tinymce) {
523+
if (is_tinymce) {
524+
ignore_next_dirty = true;
525+
mathJaxUnTypeset(contents_div);
526+
ignore_next_dirty = false;
527+
} else {
518528
// Wait until the focus event completes; this causes the
519529
// cursor position (the selection) to be set in the
520530
// contenteditable div. Then, save that location.
@@ -591,8 +601,15 @@ const DocBlockPlugin = ViewPlugin.fromClass(
591601
// Move TinyMCE to the new location, then remove the old
592602
// div it will replace.
593603
target.insertBefore(tinymce_div, null);
594-
tinymce_singleton!.setContent(contents_div.innerHTML);
604+
// TinyMCE edits booger MathJax. Also, the math is
605+
// uneditable. So, translate it back to its untypeset
606+
// form. When editing is done, it will be re-rendered.
607+
mathJaxUnTypeset(contents_div);
608+
609+
// Setting the content makes TinyMCE consider it dirty
610+
// -- ignore this "dirty" event.
595611
ignore_next_dirty = true;
612+
tinymce_singleton!.setContent(contents_div.innerHTML);
596613
tinymce_singleton!.save();
597614
contents_div.remove();
598615

@@ -833,6 +850,27 @@ export const CodeMirror_load = async (
833850
}
834851
on_dirty(target_or_false);
835852
});
853+
// When leaving a TinyMCE block, retypeset the math. (It's
854+
// untypeset when entering the block, to avoid editing
855+
// problems.)
856+
editor.on("focusout", (event: any) => {
857+
const target_or_false = event.target;
858+
if (target_or_false == null) {
859+
return false;
860+
}
861+
if (!tinymce_singleton!.isDirty()) {
862+
ignore_next_dirty = true;
863+
}
864+
// When switching from one doc block to another, the MathJax
865+
// typeset finishes after the new doc block has been
866+
// updated. To prevent saving the "dirty" content from
867+
// typesetting, wait until this finishes to clear the
868+
// `ignore_next_dirty` flag.
869+
mathJaxTypeset(target_or_false, () => {
870+
tinymce_singleton!.save();
871+
ignore_next_dirty = false;
872+
});
873+
})
836874
},
837875
})
838876
)[0];

0 commit comments

Comments
 (0)