gristlabs_grist-core/app/client/widgets/MarkdownTextBox.ts

177 lines
4.5 KiB
TypeScript
Raw Normal View History

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 { dom, styled, subscribeBindable } 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 _marked: Marked;
constructor(field: ViewFieldRec) {
super(field);
const highlightCodePromise = buildCodeHighlighter({maxLines: 60});
this._marked = new Marked(
markedHighlight({
async: true,
highlight: async (code) => {
const highlightCode = await highlightCodePromise;
return highlightCode(code);
},
}),
markedLinkifyIt(),
);
}
public buildDom(row: DataRowModel) {
const value = row.cells[this.field.colId()];
return cssFieldClip(
cssFieldClip.cls('-text-wrap', this.wrapping),
dom.style('text-align', this.alignment),
(el) => dom.autoDisposeElem(el, subscribeBindable(value, async () => {
el.innerHTML = sanitizeHTML(await this._marked.parse(String(value.peek()), {gfm: false, renderer}));
this.field.viewSection().events.trigger('rowHeightChange');
})),
// Note: the DOM needs to be rebuilt on theme change, as Ace needs to switch between
// light and dark themes. If we switch to using a custom Grist Ace theme (with CSS
// variables used for highlighting), we can remove the listener below (and elsewhere).
(el) => dom.autoDisposeElem(el, subscribeBindable(gristThemeObs(), async () => {
el.innerHTML = sanitizeHTML(await this._marked.parse(String(value.peek()), {gfm: false, renderer}));
})),
);
}
}
const cssFieldClip = styled('div.field_clip', `
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;
}
`);