gristlabs_grist-core/app/client/widgets/MarkdownTextBox.ts
George Gevoian 89468bd9f0 (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
2024-10-15 09:54:04 -04:00

194 lines
4.7 KiB
TypeScript

import { domAsync } from 'app/client/lib/domAsync';
import { DataRowModel } from 'app/client/models/DataRowModel';
import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec';
import { buildCodeHighlighter } from 'app/client/ui/CodeHighlight';
import { renderer } from 'app/client/ui/MarkdownCellRenderer';
import { sanitizeHTML } from 'app/client/ui/sanitizeHTML';
import { theme, vars } from 'app/client/ui2018/cssVars';
import { gristThemeObs } from 'app/client/ui2018/theme';
import { NTextBox } from 'app/client/widgets/NTextBox';
import { AsyncCreate } from 'app/common/AsyncCreate';
import { dom, styled, subscribeElem } from 'grainjs';
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import markedLinkifyIt from 'marked-linkify-it';
/**
* Creates a widget for displaying Markdown-formatted text.
*/
export class MarkdownTextBox extends NTextBox {
private static _marked?: AsyncCreate<Marked>;
constructor(field: ViewFieldRec) {
super(field);
if (!MarkdownTextBox._marked) {
MarkdownTextBox._marked = new AsyncCreate(async () => {
const highlight = await buildCodeHighlighter({ maxLines: 60 });
return new Marked(
markedHighlight({
highlight: (code) => highlight(code),
}),
markedLinkifyIt()
);
});
}
}
public buildDom(row: DataRowModel) {
const value = row.cells[this.field.colId()];
return dom(
"div.field_clip",
domAsync(
MarkdownTextBox._marked!.get().then(({ parse }) => {
const renderMarkdown = (el: HTMLElement) => {
el.innerHTML = sanitizeHTML(
parse(String(value.peek()), {
async: false,
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 cssMarkdown = styled('div', `
white-space: nowrap;
&-text-wrap {
white-space: normal;
word-break: break-word;
}
&:not(&-text-wrap) p,
&:not(&-text-wrap) h1,
&:not(&-text-wrap) h2,
&:not(&-text-wrap) h3,
&:not(&-text-wrap) h4,
&:not(&-text-wrap) h5,
&:not(&-text-wrap) h6
{
overflow: hidden;
text-overflow: ellipsis;
}
& > *:first-child {
margin-top: 0px !important;
}
& > :not(blockquote, ol, pre, ul):last-child {
margin-bottom: 0px !important;
}
& h1, & h2, & h3, & h4, & h5, & h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
& h1 {
padding-bottom: .3em;
font-size: 2em;
}
& h2 {
padding-bottom: .3em;
font-size: 1.5em;
}
& h3 {
font-size: 1.25em;
}
& h4 {
font-size: 1em;
}
& h5 {
font-size: .875em;
}
& h6 {
color: ${theme.lightText};
font-size: .85em;
}
& p, & blockquote, & ul, & ol, & dl, & pre {
margin-top: 0px;
margin-bottom: 10px;
}
& code, & pre {
color: ${theme.text};
font-size: 85%;
background-color: ${theme.markdownCellLightBg};
border: 0;
border-radius: 6px;
}
& code {
padding: .2em .4em;
margin: 0;
white-space: nowrap;
}
&-text-wrap code {
white-space: break-spaces;
}
& pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
}
& pre code {
font-size: 100%;
display: inline;
max-width: auto;
margin: 0;
padding: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background: transparent;
}
& pre > code {
background: transparent;
white-space: nowrap;
word-break: normal;
margin: 0;
padding: 0;
}
&-text-wrap pre > code {
white-space: normal;
}
& pre .ace-chrome, & pre .ace-dracula {
background: ${theme.markdownCellLightBg} !important;
}
& .ace_indent-guide {
background: none;
}
& .ace_static_highlight {
white-space: nowrap;
}
& ul, & ol {
padding-left: 2em;
}
& li > ol, & li > ul {
margin: 0;
}
& li + li,
& li > ol > li:first-child,
& li > ul > li:first-child {
margin-top: .25em;
}
& blockquote {
font-size: ${vars.mediumFontSize};
border-left: .25em solid ${theme.markdownCellMediumBorder};
padding: 0 1em;
}
`);