(core) Switch to sync Markdown rendering

Summary:
There was a bug where stale Markdown values were sometimes shown when moving between pages. It appears to have been an issue with async rendering of Markdown and how cell values were being updated.

As a fix, we now use synchronous rendering, which doesn't have any perceptible performance penalty, and behaves predictably.

Test Plan: Manual.

Reviewers: jarek

Reviewed By: jarek

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D4379
This commit is contained in:
George Gevoian 2024-10-14 17:27:47 -04:00
parent 2a978c4313
commit 89468bd9f0

View File

@ -1,3 +1,4 @@
import { domAsync } from 'app/client/lib/domAsync';
import { DataRowModel } from 'app/client/models/DataRowModel'; import { DataRowModel } from 'app/client/models/DataRowModel';
import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec'; import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec';
import { buildCodeHighlighter } from 'app/client/ui/CodeHighlight'; import { buildCodeHighlighter } from 'app/client/ui/CodeHighlight';
@ -6,7 +7,8 @@ import { sanitizeHTML } from 'app/client/ui/sanitizeHTML';
import { theme, vars } from 'app/client/ui2018/cssVars'; import { theme, vars } from 'app/client/ui2018/cssVars';
import { gristThemeObs } from 'app/client/ui2018/theme'; import { gristThemeObs } from 'app/client/ui2018/theme';
import { NTextBox } from 'app/client/widgets/NTextBox'; import { NTextBox } from 'app/client/widgets/NTextBox';
import { dom, styled, subscribeBindable } from 'grainjs'; import { AsyncCreate } from 'app/common/AsyncCreate';
import { dom, styled, subscribeElem } from 'grainjs';
import { Marked } from 'marked'; import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight'; import { markedHighlight } from 'marked-highlight';
import markedLinkifyIt from 'marked-linkify-it'; import markedLinkifyIt from 'marked-linkify-it';
@ -15,44 +17,59 @@ import markedLinkifyIt from 'marked-linkify-it';
* Creates a widget for displaying Markdown-formatted text. * Creates a widget for displaying Markdown-formatted text.
*/ */
export class MarkdownTextBox extends NTextBox { export class MarkdownTextBox extends NTextBox {
private _marked: Marked; private static _marked?: AsyncCreate<Marked>;
constructor(field: ViewFieldRec) { constructor(field: ViewFieldRec) {
super(field); super(field);
const highlightCodePromise = buildCodeHighlighter({maxLines: 60}); if (!MarkdownTextBox._marked) {
this._marked = new Marked( MarkdownTextBox._marked = new AsyncCreate(async () => {
markedHighlight({ const highlight = await buildCodeHighlighter({ maxLines: 60 });
async: true, return new Marked(
highlight: async (code) => { markedHighlight({
const highlightCode = await highlightCodePromise; highlight: (code) => highlight(code),
return highlightCode(code); }),
}, markedLinkifyIt()
}), );
markedLinkifyIt(), });
); }
} }
public buildDom(row: DataRowModel) { public buildDom(row: DataRowModel) {
const value = row.cells[this.field.colId()]; const value = row.cells[this.field.colId()];
return cssFieldClip( return dom(
cssFieldClip.cls('-text-wrap', this.wrapping), "div.field_clip",
dom.style('text-align', this.alignment), domAsync(
(el) => dom.autoDisposeElem(el, subscribeBindable(value, async () => { MarkdownTextBox._marked!.get().then(({ parse }) => {
el.innerHTML = sanitizeHTML(await this._marked.parse(String(value.peek()), {gfm: false, renderer})); const renderMarkdown = (el: HTMLElement) => {
this.field.viewSection().events.trigger('rowHeightChange'); el.innerHTML = sanitizeHTML(
})), parse(String(value.peek()), {
// Note: the DOM needs to be rebuilt on theme change, as Ace needs to switch between async: false,
// light and dark themes. If we switch to using a custom Grist Ace theme (with CSS gfm: false,
// variables used for highlighting), we can remove the listener below (and elsewhere). renderer,
(el) => dom.autoDisposeElem(el, subscribeBindable(gristThemeObs(), async () => { })
el.innerHTML = sanitizeHTML(await this._marked.parse(String(value.peek()), {gfm: false, renderer})); );
})), };
return cssMarkdown(
cssMarkdown.cls("-text-wrap", this.wrapping),
dom.style("text-align", this.alignment),
(el) => {
subscribeElem(el, value, () => renderMarkdown(el));
// Picking up theme changes currently requires a re-render.
// If we switch to using a custom Ace theme with CSS variables
// from `cssVars.ts`, we can remove this.
subscribeElem(el, gristThemeObs(), () => renderMarkdown(el));
this.field.viewSection().events.trigger("rowHeightChange");
},
);
})
)
); );
} }
} }
const cssFieldClip = styled('div.field_clip', ` const cssMarkdown = styled('div', `
white-space: nowrap; white-space: nowrap;
&-text-wrap { &-text-wrap {