From 292c894b931e4bba77b1ba6ef6a2a625f4704564 Mon Sep 17 00:00:00 2001 From: George Gevoian Date: Thu, 22 Aug 2024 23:51:09 -0400 Subject: [PATCH] (core) Add Markdown cell format Summary: Text columns can now display their values as Markdown-formatted text by changing their cell format to "Markdown". A minimal subset of the Markdown specification is currently supported. Test Plan: Browser tests. Reviewers: Spoffy, dsagal Reviewed By: Spoffy, dsagal Subscribers: dsagal, Spoffy Differential Revision: https://phab.getgrist.com/D4326 --- app/client/components/FormRenderer.ts | 4 +- app/client/components/Forms/styles.ts | 2 +- app/client/lib/markdown.ts | 2 +- app/client/ui/DocTutorial.ts | 10 +- app/client/ui/DocTutorialRenderer.ts | 4 +- app/client/ui/MarkdownCellRenderer.ts | 12 ++ app/client/ui/sanitizeHTML.ts | 36 +++-- app/client/ui2018/IconList.ts | 2 + app/client/ui2018/cssVars.ts | 7 + app/client/ui2018/icons.ts | 33 +++-- app/client/ui2018/links.ts | 35 +++-- app/client/widgets/FormulaAssistant.ts | 31 ++-- app/client/widgets/HyperLinkTextBox.ts | 6 +- app/client/widgets/MarkdownTextBox.ts | 179 +++++++++++++++++++++++ app/client/widgets/NTextBox.ts | 12 +- app/client/widgets/UserType.ts | 9 ++ app/client/widgets/UserTypeImpl.ts | 2 + app/common/ThemePrefs-ti.ts | 3 + app/common/ThemePrefs.ts | 5 + app/common/themes/GristDark.ts | 5 + app/common/themes/GristLight.ts | 5 + package.json | 4 +- static/icons/icons.css | 1 + static/ui-icons/Fields/FieldMarkdown.svg | 1 + test/nbrowser/CellColor.ts | 9 +- yarn.lock | 18 +-- 26 files changed, 353 insertions(+), 84 deletions(-) create mode 100644 app/client/ui/MarkdownCellRenderer.ts create mode 100644 app/client/widgets/MarkdownTextBox.ts create mode 100644 static/ui-icons/Fields/FieldMarkdown.svg diff --git a/app/client/components/FormRenderer.ts b/app/client/components/FormRenderer.ts index b3b3c5d7..a3cbac4d 100644 --- a/app/client/components/FormRenderer.ts +++ b/app/client/components/FormRenderer.ts @@ -135,7 +135,9 @@ class ParagraphRenderer extends FormRenderer { return css.paragraph( css.paragraph.cls(`-alignment-${this.layoutNode.alignment || 'left'}`), el => { - el.innerHTML = sanitizeHTML(marked(this.layoutNode.text || '**Lorem** _ipsum_ dolor')); + el.innerHTML = sanitizeHTML(marked(this.layoutNode.text || '**Lorem** _ipsum_ dolor', { + async: false, + })); }, ); } diff --git a/app/client/components/Forms/styles.ts b/app/client/components/Forms/styles.ts index 935269d2..2bc6d76c 100644 --- a/app/client/components/Forms/styles.ts +++ b/app/client/components/Forms/styles.ts @@ -505,7 +505,7 @@ export const cssMarkdownRender = styled('div', ` export function markdown(obs: BindableValue, ...args: IDomArgs) { return cssMarkdownRender(el => { dom.autoDisposeElem(el, subscribeBindable(obs, val => { - el.innerHTML = sanitizeHTML(marked(val)); + el.innerHTML = sanitizeHTML(marked(val, {async: false})); })); }, ...args); } diff --git a/app/client/lib/markdown.ts b/app/client/lib/markdown.ts index ce9d1ab5..816a3703 100644 --- a/app/client/lib/markdown.ts +++ b/app/client/lib/markdown.ts @@ -25,5 +25,5 @@ export function markdown(markdownObs: BindableValue): DomElementMethod { } function setMarkdownValue(elem: Element, markdownValue: string): void { - elem.innerHTML = sanitizeHTML(marked(markdownValue)); + elem.innerHTML = sanitizeHTML(marked(markdownValue, {async: false})); } diff --git a/app/client/ui/DocTutorial.ts b/app/client/ui/DocTutorial.ts index 892e1937..2fd1c037 100644 --- a/app/client/ui/DocTutorial.ts +++ b/app/client/ui/DocTutorial.ts @@ -4,7 +4,7 @@ import {logTelemetryEvent} from 'app/client/lib/telemetry'; import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState'; import {renderer} from 'app/client/ui/DocTutorialRenderer'; import {cssPopupBody, FLOATING_POPUP_TOOLTIP_KEY, FloatingPopup} from 'app/client/ui/FloatingPopup'; -import {sanitizeHTML} from 'app/client/ui/sanitizeHTML'; +import {sanitizeTutorialHTML} from 'app/client/ui/sanitizeHTML'; import {hoverTooltip, setHoverTooltip} from 'app/client/ui/tooltips'; import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons'; import {mediaXSmall, theme, vars} from 'app/client/ui2018/cssVars'; @@ -13,7 +13,7 @@ import {loadingSpinner} from 'app/client/ui2018/loaders'; import {confirmModal, modal} from 'app/client/ui2018/modals'; import {parseUrlId} from 'app/common/gristUrls'; import {dom, makeTestId, Observable, styled} from 'grainjs'; -import {marked} from 'marked'; +import {marked, Token} from 'marked'; import debounce = require('lodash/debounce'); import range = require('lodash/range'); import sortBy = require('lodash/sortBy'); @@ -219,7 +219,7 @@ export class DocTutorial extends FloatingPopup { return value ? String(value) : undefined; }; - const walkTokens = (token: marked.Token) => { + const walkTokens = (token: Token) => { if (token.type === 'image') { imageUrls.push(token.href); } @@ -231,13 +231,13 @@ export class DocTutorial extends FloatingPopup { let slideContent = getValue('slide_content'); if (!slideContent) { return null; } - slideContent = sanitizeHTML(await marked.parse(slideContent, { + slideContent = sanitizeTutorialHTML(await marked.parse(slideContent, { async: true, renderer, walkTokens })); let boxContent = getValue('box_content'); if (boxContent) { - boxContent = sanitizeHTML(await marked.parse(boxContent, { + boxContent = sanitizeTutorialHTML(await marked.parse(boxContent, { async: true, renderer, walkTokens })); } diff --git a/app/client/ui/DocTutorialRenderer.ts b/app/client/ui/DocTutorialRenderer.ts index 0cecc08f..56d87755 100644 --- a/app/client/ui/DocTutorialRenderer.ts +++ b/app/client/ui/DocTutorialRenderer.ts @@ -2,7 +2,7 @@ import {marked} from 'marked'; export const renderer = new marked.Renderer(); -renderer.image = (href: string | null, title: string | null, _text: string) => { +renderer.image = ({href, title}) => { let classes = 'doc-tutorial-popup-thumbnail'; const hash = href?.split('#')?.[1]; if (hash) { @@ -17,6 +17,6 @@ renderer.image = (href: string | null, title: string | null, _text: string) => { `; }; -renderer.link = (href: string | null, _title: string | null, text: string) => { +renderer.link = ({href, text}) => { return `${text}`; }; diff --git a/app/client/ui/MarkdownCellRenderer.ts b/app/client/ui/MarkdownCellRenderer.ts new file mode 100644 index 00000000..036572ff --- /dev/null +++ b/app/client/ui/MarkdownCellRenderer.ts @@ -0,0 +1,12 @@ +import {gristIconLink} from 'app/client/ui2018/links'; +import escape from 'lodash/escape'; +import {marked} from 'marked'; + +export const renderer = new marked.Renderer(); + +renderer.link = ({href, text}) => gristIconLink(href, text).outerHTML; + +// Disable Markdown features that we aren't ready to support yet. +renderer.hr = ({raw}) => raw; +renderer.html = ({raw}) => escape(raw); +renderer.image = ({raw}) => raw; diff --git a/app/client/ui/sanitizeHTML.ts b/app/client/ui/sanitizeHTML.ts index e793ca7f..e979959c 100644 --- a/app/client/ui/sanitizeHTML.ts +++ b/app/client/ui/sanitizeHTML.ts @@ -1,16 +1,30 @@ -import DOMPurify from 'dompurify'; +import createDOMPurifier from 'dompurify'; -const config = { - ADD_TAGS: ['iframe'], - ADD_ATTR: ['allowFullscreen'], -}; +export function sanitizeHTML(source: string | Node): string { + return defaultPurifier.sanitize(source); +} -DOMPurify.addHook('uponSanitizeAttribute', (node) => { +export function sanitizeTutorialHTML(source: string | Node): string { + return tutorialPurifier.sanitize(source, { + ADD_TAGS: ['iframe'], + ADD_ATTR: ['allowFullscreen'], + }); +} + +const defaultPurifier = createDOMPurifier(); +defaultPurifier.addHook('uponSanitizeAttribute', handleSanitizeAttribute); + +const tutorialPurifier = createDOMPurifier(); +tutorialPurifier.addHook('uponSanitizeAttribute', handleSanitizeAttribute); +tutorialPurifier.addHook('uponSanitizeElement', handleSanitizeTutorialElement); + +function handleSanitizeAttribute(node: Element) { if (!('target' in node)) { return; } node.setAttribute('target', '_blank'); -}); -DOMPurify.addHook('uponSanitizeElement', (node, data) => { +} + +function handleSanitizeTutorialElement(node: Element, data: createDOMPurifier.SanitizeElementHookEvent) { if (data.tagName !== 'iframe') { return; } const src = node.getAttribute('src'); @@ -18,9 +32,5 @@ DOMPurify.addHook('uponSanitizeElement', (node, data) => { return; } - return node.parentNode?.removeChild(node); -}); - -export function sanitizeHTML(source: string | Node): string { - return DOMPurify.sanitize(source, config); + node.parentNode?.removeChild(node); } diff --git a/app/client/ui2018/IconList.ts b/app/client/ui2018/IconList.ts index 39bbf15d..c9ab4e66 100644 --- a/app/client/ui2018/IconList.ts +++ b/app/client/ui2018/IconList.ts @@ -23,6 +23,7 @@ export type IconName = "ChartArea" | "FieldFunctionEqual" | "FieldInteger" | "FieldLink" | + "FieldMarkdown" | "FieldNumeric" | "FieldReference" | "FieldSpinner" | @@ -185,6 +186,7 @@ export const IconList: IconName[] = ["ChartArea", "FieldFunctionEqual", "FieldInteger", "FieldLink", + "FieldMarkdown", "FieldNumeric", "FieldReference", "FieldSpinner", diff --git a/app/client/ui2018/cssVars.ts b/app/client/ui2018/cssVars.ts index dcac5402..f88dc1bf 100644 --- a/app/client/ui2018/cssVars.ts +++ b/app/client/ui2018/cssVars.ts @@ -895,6 +895,13 @@ export const theme = { undefined, colors.slate), widgetGallerySecondaryHeaderBgHover: new CustomProp( 'theme-widget-gallery-secondary-header-bg-hover', undefined, '#7E7E85'), + + /* Markdown Cell */ + markdownCellLightBg: new CustomProp('theme-markdown-cell-light-bg', undefined, colors.lightGrey), + markdownCellLightBorder: new CustomProp('theme-markdown-cell-light-border', undefined, + colors.mediumGreyOpaque), + markdownCellMediumBorder: new CustomProp('theme-markdown-cell-medium-border', undefined, + colors.darkGrey), }; const cssColors = values(colors).map(v => v.decl()).join('\n'); diff --git a/app/client/ui2018/icons.ts b/app/client/ui2018/icons.ts index ee1e97d1..b0889223 100644 --- a/app/client/ui2018/icons.ts +++ b/app/client/ui2018/icons.ts @@ -56,7 +56,7 @@ import { IconName } from './IconList'; /** * Defaults for all icons. */ -const iconDiv = styled('div', ` +const iconStyles = ` position: relative; display: inline-block; vertical-align: middle; @@ -66,24 +66,35 @@ const iconDiv = styled('div', ` width: 16px; height: 16px; background-color: var(--icon-color, var(--grist-theme-text, black)); -`); +`; -export const cssIconBackground = styled(iconDiv, ` - background-color: var(--icon-background, inherit); - -webkit-mask: none; - & .${iconDiv.className} { - transition: inherit; - display: block; - } -`); +const cssIconDiv = styled('div', iconStyles); + +const cssIconSpan = styled('span', iconStyles); export function icon(name: IconName, ...domArgs: DomElementArg[]): HTMLElement { - return iconDiv( + return cssIconDiv( dom.style('-webkit-mask-image', `var(--icon-${name})`), ...domArgs ); } +export function iconSpan(name: IconName, ...domArgs: DomElementArg[]): HTMLElement { + return cssIconSpan( + dom.style('-webkit-mask-image', `var(--icon-${name})`), + ...domArgs + ); +} + +export const cssIconSpanBackground = styled(cssIconSpan, ` + background-color: var(--icon-background, inherit); + -webkit-mask: none; + & .${cssIconSpan.className} { + transition: inherit; + display: block; + } +`); + /** * Container box for an icon to serve as a button.. */ diff --git a/app/client/ui2018/links.ts b/app/client/ui2018/links.ts index e46bc21c..f460c3fe 100644 --- a/app/client/ui2018/links.ts +++ b/app/client/ui2018/links.ts @@ -1,9 +1,9 @@ import {findLinks} from 'app/client/lib/textUtils'; -import { sameDocumentUrlState, urlState } from 'app/client/models/gristUrlState'; -import { hideInPrintView, testId, theme } from 'app/client/ui2018/cssVars'; -import {cssIconBackground, icon} from 'app/client/ui2018/icons'; -import { CellValue } from 'app/plugin/GristData'; -import { dom, DomArg, IDomArgs, Observable, styled } from 'grainjs'; +import {sameDocumentUrlState, urlState} from 'app/client/models/gristUrlState'; +import {hideInPrintView, testId, theme} from 'app/client/ui2018/cssVars'; +import {cssIconSpanBackground, iconSpan} from 'app/client/ui2018/icons'; +import {CellValue} from 'app/plugin/GristData'; +import {dom, DomArg, IDomArgs, Observable, styled} from 'grainjs'; /** * Styling for a simple link. @@ -37,6 +37,19 @@ export function gristLink(href: string|Observable, ...args: IDomArgs; private _element: HTMLElement; + private _marked: Marked; constructor(private _options: { column: ColumnRec, @@ -845,6 +847,17 @@ class ChatHistory extends Disposable { this.lastSuggestedFormula = Computed.create(this, use => { return [...use(this.conversationHistory)].reverse().find(({formula}) => formula)?.formula ?? null; }); + + const highlightCodePromise = buildCodeHighlighter({maxLines: 60}); + this._marked = new Marked( + markedHighlight({ + async: true, + highlight: async (code) => { + const highlightCode = await highlightCodePromise; + return highlightCode(code); + }, + }) + ); } public thinking(on = true) { @@ -864,10 +877,6 @@ class ChatHistory extends Disposable { } } - public supportsMarkdown() { - return this._options.column.chatHistory.peek().get().state !== undefined; - } - public addResponse(message: ChatMessage) { // Clear any thinking from messages. this.thinking(false); @@ -958,6 +967,10 @@ class ChatHistory extends Disposable { ); } + private _supportsMarkdown() { + return this._options.column.chatHistory.peek().get().state !== undefined; + } + private _buildIntroMessage() { return cssAiIntroMessage( cssAvatar(cssAiImage()), @@ -1010,14 +1023,10 @@ class ChatHistory extends Disposable { * Renders the message as markdown if possible, otherwise as a code block. */ private _render(message: string, ...args: DomElementArg[]) { - if (this.supportsMarkdown()) { + if (this._supportsMarkdown()) { return dom('div', (el) => subscribeElem(el, gristThemeObs(), async () => { - const highlightCode = await buildCodeHighlighter({maxLines: 60}); - const content = sanitizeHTML(marked(message, { - highlight: (code) => highlightCode(code) - })); - el.innerHTML = content; + el.innerHTML = sanitizeHTML(await this._marked.parse(message)); }), ...args ); diff --git a/app/client/widgets/HyperLinkTextBox.ts b/app/client/widgets/HyperLinkTextBox.ts index 501b61f3..348e349a 100644 --- a/app/client/widgets/HyperLinkTextBox.ts +++ b/app/client/widgets/HyperLinkTextBox.ts @@ -2,7 +2,7 @@ import { DataRowModel } from 'app/client/models/DataRowModel'; import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec'; import { constructUrl } from 'app/client/models/gristUrlState'; import { testId, theme } from 'app/client/ui2018/cssVars'; -import { cssIconBackground, icon } from 'app/client/ui2018/icons'; +import { cssIconSpanBackground, iconSpan } from 'app/client/ui2018/icons'; import { cssHoverIn, gristLink } from 'app/client/ui2018/links'; import { NTextBox } from 'app/client/widgets/NTextBox'; import { CellValue } from 'app/common/DocActions'; @@ -27,8 +27,8 @@ export class HyperLinkTextBox extends NTextBox { dom.cls('text_wrapping', this.wrapping), dom.maybe((use) => Boolean(use(value)), () => gristLink(url, - cssIconBackground( - icon("FieldLink", testId('tb-link-icon')), + cssIconSpanBackground( + iconSpan("FieldLink", testId('tb-link-icon')), dom.cls(cssHoverOnField.className), ), testId('tb-link'), diff --git a/app/client/widgets/MarkdownTextBox.ts b/app/client/widgets/MarkdownTextBox.ts new file mode 100644 index 00000000..9c8fa1c9 --- /dev/null +++ b/app/client/widgets/MarkdownTextBox.ts @@ -0,0 +1,179 @@ +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'; + +/** + * 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); + }, + }) + ); + } + + 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; + } + & > *: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 { + list-style-position: inside; + padding-left: 1em; + } + & li > ol, & li > ul { + margin: 0; + } + &:not(&-text-wrap) li { + overflow: hidden; + text-overflow: ellipsis; + } + & 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; + } +`); diff --git a/app/client/widgets/NTextBox.ts b/app/client/widgets/NTextBox.ts index b346c4d6..9d7127be 100644 --- a/app/client/widgets/NTextBox.ts +++ b/app/client/widgets/NTextBox.ts @@ -28,9 +28,7 @@ export class NTextBox extends NewAbstractWidget { this.alignment = fromKo(this.options.prop('alignment')); this.wrapping = fromKo(this.field.wrap); - this.autoDispose(this.wrapping.addListener(() => { - this.field.viewSection().events.trigger('rowHeightChange'); - })); + this._addRowHeightListeners(); } public buildConfigDom(_gristDoc: GristDoc): DomContents { @@ -112,4 +110,12 @@ export class NTextBox extends NewAbstractWidget { makeLinks(use(this.valueFormatter).formatAny(use(value), t))) ); } + + private _addRowHeightListeners() { + for (const obs of [this.wrapping, fromKo(this.field.config.widget)]) { + this.autoDispose(obs.addListener(() => { + this.field.viewSection().events.trigger('rowHeightChange'); + })); + } + } } diff --git a/app/client/widgets/UserType.ts b/app/client/widgets/UserType.ts index 06586c53..fe7d7a6a 100644 --- a/app/client/widgets/UserType.ts +++ b/app/client/widgets/UserType.ts @@ -66,6 +66,15 @@ export const typeDefs: any = { wrap: undefined, } }, + Markdown: { + cons: 'MarkdownTextBox', + editCons: 'TextEditor', + icon: 'FieldMarkdown', + options: { + alignment: 'left', + wrap: undefined, + } + }, HyperLink: { cons: 'HyperLinkTextBox', editCons: 'HyperLinkEditor', diff --git a/app/client/widgets/UserTypeImpl.ts b/app/client/widgets/UserTypeImpl.ts index e2907595..89aece77 100644 --- a/app/client/widgets/UserTypeImpl.ts +++ b/app/client/widgets/UserTypeImpl.ts @@ -11,6 +11,7 @@ import DateTimeEditor from 'app/client/widgets/DateTimeEditor'; import DateTimeTextBox from 'app/client/widgets/DateTimeTextBox'; import {HyperLinkEditor} from 'app/client/widgets/HyperLinkEditor'; import {HyperLinkTextBox} from 'app/client/widgets/HyperLinkTextBox'; +import {MarkdownTextBox} from 'app/client/widgets/MarkdownTextBox'; import {NewAbstractWidget} from 'app/client/widgets/NewAbstractWidget'; import {NewBaseEditor} from 'app/client/widgets/NewBaseEditor'; import {NTextBox} from 'app/client/widgets/NTextBox'; @@ -36,6 +37,7 @@ export const nameToWidget = { 'NumericEditor': NumericEditor, 'HyperLinkTextBox': HyperLinkTextBox, 'HyperLinkEditor': HyperLinkEditor, + 'MarkdownTextBox': MarkdownTextBox, 'Spinner': Spinner, 'CheckBox': ToggleCheckBox, 'CheckBoxEditor': CheckBoxEditor, diff --git a/app/common/ThemePrefs-ti.ts b/app/common/ThemePrefs-ti.ts index 09d4add9..ef8ae2e0 100644 --- a/app/common/ThemePrefs-ti.ts +++ b/app/common/ThemePrefs-ti.ts @@ -447,6 +447,9 @@ export const ThemeColors = t.iface([], { "widget-gallery-secondary-header-fg": "string", "widget-gallery-secondary-header-bg": "string", "widget-gallery-secondary-header-bg-hover": "string", + "markdown-cell-light-bg": "string", + "markdown-cell-light-border": "string", + "markdown-cell-medium-border": "string", }); const exportedTypeSuite: t.ITypeSuite = { diff --git a/app/common/ThemePrefs.ts b/app/common/ThemePrefs.ts index 03f45784..49fe9587 100644 --- a/app/common/ThemePrefs.ts +++ b/app/common/ThemePrefs.ts @@ -583,6 +583,11 @@ export interface ThemeColors { 'widget-gallery-secondary-header-fg': string; 'widget-gallery-secondary-header-bg': string; 'widget-gallery-secondary-header-bg-hover': string; + + /* Markdown Cell */ + 'markdown-cell-light-bg': string; + 'markdown-cell-light-border': string; + 'markdown-cell-medium-border': string; } export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT; diff --git a/app/common/themes/GristDark.ts b/app/common/themes/GristDark.ts index 5c64906a..7d2a4eeb 100644 --- a/app/common/themes/GristDark.ts +++ b/app/common/themes/GristDark.ts @@ -562,4 +562,9 @@ export const GristDark: ThemeColors = { 'widget-gallery-secondary-header-fg': '#FFFFFF', 'widget-gallery-secondary-header-bg': '#70707D', 'widget-gallery-secondary-header-bg-hover': '#60606D', + + /* Markdown Cell */ + 'markdown-cell-light-bg': '#494958', + 'markdown-cell-light-border': '#32323F', + 'markdown-cell-medium-border': '#555563', }; diff --git a/app/common/themes/GristLight.ts b/app/common/themes/GristLight.ts index 60d1193c..0183dbbf 100644 --- a/app/common/themes/GristLight.ts +++ b/app/common/themes/GristLight.ts @@ -562,4 +562,9 @@ export const GristLight: ThemeColors = { 'widget-gallery-secondary-header-fg': '#FFFFFF', 'widget-gallery-secondary-header-bg': '#929299', 'widget-gallery-secondary-header-bg-hover': '#7E7E85', + + /* Markdown Cell */ + 'markdown-cell-light-bg': '#F7F7F7', + 'markdown-cell-light-border': '#E8E8E8', + 'markdown-cell-medium-border': '#D9D9D9', }; diff --git a/package.json b/package.json index 78916f46..74af590e 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "@types/jsonwebtoken": "7.2.8", "@types/lodash": "4.14.117", "@types/lru-cache": "5.1.1", - "@types/marked": "4.0.8", "@types/mime-types": "2.1.0", "@types/mocha": "10.0.1", "@types/moment-timezone": "0.5.9", @@ -165,7 +164,8 @@ "knockout": "3.5.0", "locale-currency": "0.0.2", "lodash": "4.17.21", - "marked": "4.2.12", + "marked": "14.0.0", + "marked-highlight": "2.1.4", "minio": "8.0.0", "moment": "2.29.4", "moment-timezone": "0.5.35", diff --git a/static/icons/icons.css b/static/icons/icons.css index f4851513..99b4068e 100644 --- a/static/icons/icons.css +++ b/static/icons/icons.css @@ -24,6 +24,7 @@ --icon-FieldFunctionEqual: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUuNSw2IEwxMC41LDYgQzEwLjc3NjE0MjQsNiAxMSw2LjIyMzg1NzYzIDExLDYuNSBDMTEsNi43NzYxNDIzNyAxMC43NzYxNDI0LDcgMTAuNSw3IEw1LjUsNyBDNS4yMjM4NTc2Myw3IDUsNi43NzYxNDIzNyA1LDYuNSBDNSw2LjIyMzg1NzYzIDUuMjIzODU3NjMsNiA1LjUsNiBaIE01LjUsOSBMMTAuNSw5IEMxMC43NzYxNDI0LDkgMTEsOS4yMjM4NTc2MyAxMSw5LjUgQzExLDkuNzc2MTQyMzcgMTAuNzc2MTQyNCwxMCAxMC41LDEwIEw1LjUsMTAgQzUuMjIzODU3NjMsMTAgNSw5Ljc3NjE0MjM3IDUsOS41IEM1LDkuMjIzODU3NjMgNS4yMjM4NTc2Myw5IDUuNSw5IFogTTQsMyBDMy40NDc3MTUyNSwzIDMsMy40NDc3MTUyNSAzLDQgTDMsMTIgQzMsMTIuNTUyMjg0NyAzLjQ0NzcxNTI1LDEzIDQsMTMgTDEyLDEzIEMxMi41NTIyODQ3LDEzIDEzLDEyLjU1MjI4NDcgMTMsMTIgTDEzLDQgQzEzLDMuNDQ3NzE1MjUgMTIuNTUyMjg0NywzIDEyLDMgTDQsMyBaIE00LDIgTDEyLDIgQzEzLjEwNDU2OTUsMiAxNCwyLjg5NTQzMDUgMTQsNCBMMTQsMTIgQzE0LDEzLjEwNDU2OTUgMTMuMTA0NTY5NSwxNCAxMiwxNCBMNCwxNCBDMi44OTU0MzA1LDE0IDIsMTMuMTA0NTY5NSAyLDEyIEwyLDQgQzIsMi44OTU0MzA1IDIuODk1NDMwNSwyIDQsMiBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4='); --icon-FieldInteger: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzLDEzIEwxMywzIEwxMiwzIEwxMiwyIEwxNCwyIEwxNCwzIEwxNCwxMyBMMTQsMTQgTDEyLDE0IEwxMiwxMyBMMTMsMTMgWiBNMiwxMyBMMiwzIEwyLDIgTDQsMiBMNCwzIEwzLDMgTDMsMTMgTDQsMTMgTDQsMTQgTDIsMTQgTDIsMTMgWiBNOC45MDIzNDM3NSwxMiBMNy44MzU5Mzc1LDEyIEw3LjgzNTkzNzUsNS4xNzM4MjgxMiBMNy43ODMyMDMxMiw1LjE3MzgyODEyIEw1LjkzMTY0MDYyLDYuNTA5NzY1NjIgTDUuOTMxNjQwNjIsNS40MDgyMDMxMiBMNy44MzU5Mzc1LDQuMDAxOTUzMTIgTDguOTAyMzQzNzUsNC4wMDE5NTMxMiBMOC45MDIzNDM3NSwxMiBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4='); --icon-FieldLink: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEyLjI5Mjg5MzIsMyBMOC41LDMgQzguMjIzODU3NjMsMyA4LDIuNzc2MTQyMzcgOCwyLjUgQzgsMi4yMjM4NTc2MyA4LjIyMzg1NzYzLDIgOC41LDIgTDEzLjUsMiBDMTMuNzc2MTQyNCwyIDE0LDIuMjIzODU3NjMgMTQsMi41IEwxNCw3LjUgQzE0LDcuNzc2MTQyMzcgMTMuNzc2MTQyNCw4IDEzLjUsOCBDMTMuMjIzODU3Niw4IDEzLDcuNzc2MTQyMzcgMTMsNy41IEwxMywzLjcwNzEwNjc4IEw3Ljg1MzU1MzM5LDguODUzNTUzMzkgQzcuNjU4MjkxMjQsOS4wNDg4MTU1NCA3LjM0MTcwODc2LDkuMDQ4ODE1NTQgNy4xNDY0NDY2MSw4Ljg1MzU1MzM5IEM2Ljk1MTE4NDQ2LDguNjU4MjkxMjQgNi45NTExODQ0Niw4LjM0MTcwODc2IDcuMTQ2NDQ2NjEsOC4xNDY0NDY2MSBMMTIuMjkyODkzMiwzIFogTTExLDEwLjUgQzExLDEwLjIyMzg1NzYgMTEuMjIzODU3NiwxMCAxMS41LDEwIEMxMS43NzYxNDI0LDEwIDEyLDEwLjIyMzg1NzYgMTIsMTAuNSBMMTIsMTIuNSBDMTIsMTMuMzI4NDI3MSAxMS4zMjg0MjcxLDE0IDEwLjUsMTQgTDMuNSwxNCBDMi42NzE1NzI4OCwxNCAyLDEzLjMyODQyNzEgMiwxMi41IEwyLDUuNSBDMiw0LjY3MTU3Mjg4IDIuNjcxNTcyODgsNCAzLjUsNCBMNS41LDQgQzUuNzc2MTQyMzcsNCA2LDQuMjIzODU3NjMgNiw0LjUgQzYsNC43NzYxNDIzNyA1Ljc3NjE0MjM3LDUgNS41LDUgTDMuNSw1IEMzLjIyMzg1NzYzLDUgMyw1LjIyMzg1NzYzIDMsNS41IEwzLDEyLjUgQzMsMTIuNzc2MTQyNCAzLjIyMzg1NzYzLDEzIDMuNSwxMyBMMTAuNSwxMyBDMTAuNzc2MTQyNCwxMyAxMSwxMi43NzYxNDI0IDExLDEyLjUgTDExLDEwLjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); + --icon-FieldMarkdown: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PGcgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGNsYXNzPSJuYy1pY29uLXdyYXBwZXIiPjxwYXRoIGQ9Ik0uNSAxMS41TC41IDQuNSA0IDguNSA3LjUgNC41IDcuNSAxMS41TTE1LjUgOC41TDEzIDExIDEwLjUgOC41TTEzIDExTDEzIDUuNSIvPjwvZz48L3N2Zz4='); --icon-FieldNumeric: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTQuOTAyMzQzNzUsMTIgTDMuODM1OTM3NSwxMiBMMy44MzU5Mzc1LDUuMTczODI4MTIgTDMuNzgzMjAzMTIsNS4xNzM4MjgxMiBMMS45MzE2NDA2Miw2LjUwOTc2NTYyIEwxLjkzMTY0MDYyLDUuNDA4MjAzMTIgTDMuODM1OTM3NSw0LjAwMTk1MzEyIEw0LjkwMjM0Mzc1LDQuMDAxOTUzMTIgTDQuOTAyMzQzNzUsMTIgWiBNNS41NTI3MzQzOCwxMS4yNzM0Mzc1IEM1LjU1MjczNDM4LDEwLjc2OTUzMTIgNS45Mjc3MzQzOCwxMC40MTc5Njg4IDYuMzkwNjI1LDEwLjQxNzk2ODggQzYuODc2OTUzMTIsMTAuNDE3OTY4OCA3LjI0NjA5Mzc1LDEwLjc2OTUzMTIgNy4yNDYwOTM3NSwxMS4yNzM0Mzc1IEM3LjI0NjA5Mzc1LDExLjc1MzkwNjIgNi44NzY5NTMxMiwxMi4xMDU0Njg4IDYuMzkwNjI1LDEyLjEwNTQ2ODggQzUuOTI3NzM0MzgsMTIuMTA1NDY4OCA1LjU1MjczNDM4LDExLjc1MzkwNjIgNS41NTI3MzQzOCwxMS4yNzM0Mzc1IFogTTguMzA2NjQwNjIsNy44NjMyODEyNSBMOC4zMDY2NDA2Miw4LjE0NDUzMTI1IEM4LjMwNjY0MDYyLDEwLjA2MDU0NjkgOC45OTgwNDY4OCwxMS4yMjA3MDMxIDEwLjA5Mzc1LDExLjIyMDcwMzEgQzExLjE5NTMxMjUsMTEuMjIwNzAzMSAxMS44ODA4NTk0LDEwLjA2MDU0NjkgMTEuODgwODU5NCw4LjE0NDUzMTI1IEwxMS44ODA4NTk0LDcuODYzMjgxMjUgQzExLjg4MDg1OTQsNS45NTMxMjUgMTEuMTk1MzEyNSw0Ljc4MTI1IDEwLjA5Mzc1LDQuNzgxMjUgQzguOTk4MDQ2ODgsNC43ODEyNSA4LjMwNjY0MDYyLDUuOTUzMTI1IDguMzA2NjQwNjIsNy44NjMyODEyNSBaIE03LjIzNDM3NSw4LjE1NjI1IEw3LjIzNDM3NSw3Ljg1NzQyMTg4IEM3LjIzNDM3NSw1LjQzNzUgOC4yODMyMDMxMiwzLjgzNzg5MDYyIDEwLjEwNTQ2ODgsMy44Mzc4OTA2MiBDMTEuOTE2MDE1NiwzLjgzNzg5MDYyIDEyLjk1MzEyNSw1LjQyNTc4MTI1IDEyLjk1MzEyNSw3Ljg1NzQyMTg4IEwxMi45NTMxMjUsOC4xNTYyNSBDMTIuOTUzMTI1LDEwLjU3NjE3MTkgMTEuOTEwMTU2MiwxMi4xNjQwNjI1IDEwLjA5Mzc1LDEyLjE2NDA2MjUgQzguMjgzMjAzMTIsMTIuMTY0MDYyNSA3LjIzNDM3NSwxMC41ODIwMzEyIDcuMjM0Mzc1LDguMTU2MjUgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-FieldReference: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUuODUzNTUzMzksMTAuODUzNTUzNCBDNS42NTgyOTEyNCwxMS4wNDg4MTU1IDUuMzQxNzA4NzYsMTEuMDQ4ODE1NSA1LjE0NjQ0NjYxLDEwLjg1MzU1MzQgQzQuOTUxMTg0NDYsMTAuNjU4MjkxMiA0Ljk1MTE4NDQ2LDEwLjM0MTcwODggNS4xNDY0NDY2MSwxMC4xNDY0NDY2IEwxMC4xNDY0NDY2LDUuMTQ2NDQ2NjEgQzEwLjM0MTcwODgsNC45NTExODQ0NiAxMC42NTgyOTEyLDQuOTUxMTg0NDYgMTAuODUzNTUzNCw1LjE0NjQ0NjYxIEMxMS4wNDg4MTU1LDUuMzQxNzA4NzYgMTEuMDQ4ODE1NSw1LjY1ODI5MTI0IDEwLjg1MzU1MzQsNS44NTM1NTMzOSBMNS44NTM1NTMzOSwxMC44NTM1NTM0IFogTTcuMzUzNTUzMzksNS4zNTM1NTMzOSBDNy4xNTgyOTEyNCw1LjU0ODgxNTU0IDYuODQxNzA4NzYsNS41NDg4MTU1NCA2LjY0NjQ0NjYxLDUuMzUzNTUzMzkgQzYuNDUxMTg0NDYsNS4xNTgyOTEyNCA2LjQ1MTE4NDQ2LDQuODQxNzA4NzYgNi42NDY0NDY2MSw0LjY0NjQ0NjYxIEw4LjMxODUxODIsMi45NzQzNzUwMyBDOS42MTg0NjQ0NSwxLjY3NDk1NTEgMTEuNzI1NTM1NiwxLjY3NDk1NTEgMTMuMDI1NjI1LDIuOTc0NTE4MiBDMTQuMzI1MDQ0OSw0LjI3NDQ2NDQ1IDE0LjMyNTA0NDksNi4zODE1MzU1NSAxMy4wMjU1NTM0LDcuNjgxNTUzMzkgTDExLjM1MzU1MzQsOS4zNTM1NTMzOSBDMTEuMTU4MjkxMiw5LjU0ODgxNTU0IDEwLjg0MTcwODgsOS41NDg4MTU1NCAxMC42NDY0NDY2LDkuMzUzNTUzMzkgQzEwLjQ1MTE4NDUsOS4xNTgyOTEyNCAxMC40NTExODQ1LDguODQxNzA4NzYgMTAuNjQ2NDQ2Niw4LjY0NjQ0NjYxIEwxMi4zMTgzNzUsNi45NzQ1MTgyIEMxMy4yMjc0NjE1LDYuMDY1MDYzNDcgMTMuMjI3NDYxNSw0LjU5MDkzNjUzIDEyLjMxODUxODIsMy42ODE2MjQ5NyBDMTEuNDA5MDYzNSwyLjc3MjUzODQ2IDkuOTM0OTM2NTMsMi43NzI1Mzg0NiA5LjAyNTU1MzM5LDMuNjgxNTUzMzkgTDcuMzUzNTUzMzksNS4zNTM1NTMzOSBaIE00LjY0NjQ0NjYxLDYuNjQ2NDQ2NjEgQzQuODQxNzA4NzYsNi40NTExODQ0NiA1LjE1ODI5MTI0LDYuNDUxMTg0NDYgNS4zNTM1NTMzOSw2LjY0NjQ0NjYxIEM1LjU0ODgxNTU0LDYuODQxNzA4NzYgNS41NDg4MTU1NCw3LjE1ODI5MTI0IDUuMzUzNTUzMzksNy4zNTM1NTMzOSBMMy42ODE2MjQ5Nyw5LjAyNTQ4MTggQzIuNzcyNTM4NDYsOS45MzQ5MzY1MyAyLjc3MjUzODQ2LDExLjQwOTA2MzUgMy42ODE0ODE4LDEyLjMxODM3NSBDNC41OTA5MzY1MywxMy4yMjc0NjE1IDYuMDY1MDYzNDcsMTMuMjI3NDYxNSA2Ljk3NDQ0NjYxLDEyLjMxODQ0NjYgTDguNjQ2NDQ2NjEsMTAuNjQ2NDQ2NiBDOC44NDE3MDg3NiwxMC40NTExODQ1IDkuMTU4MjkxMjQsMTAuNDUxMTg0NSA5LjM1MzU1MzM5LDEwLjY0NjQ0NjYgQzkuNTQ4ODE1NTQsMTAuODQxNzA4OCA5LjU0ODgxNTU0LDExLjE1ODI5MTIgOS4zNTM1NTMzOSwxMS4zNTM1NTM0IEw3LjY4MTQ4MTgsMTMuMDI1NjI1IEM2LjM4MTUzNTU1LDE0LjMyNTA0NDkgNC4yNzQ0NjQ0NSwxNC4zMjUwNDQ5IDIuOTc0Mzc1MDMsMTMuMDI1NDgxOCBDMS42NzQ5NTUxLDExLjcyNTUzNTYgMS42NzQ5NTUxLDkuNjE4NDY0NDUgMi45NzQ0NDY2MSw4LjMxODQ0NjYxIEw0LjY0NjQ0NjYxLDYuNjQ2NDQ2NjEgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-FieldSpinner: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTExLjE3NDYwNDMsMTAuMTIwMzcxNyBDMTEuMzg0MjY3Miw5Ljk0MDY2MDYyIDExLjY5OTkxNzIsOS45NjQ5NDEzOSAxMS44Nzk2MjgzLDEwLjE3NDYwNDMgQzEyLjA1OTMzOTQsMTAuMzg0MjY3MiAxMi4wMzUwNTg2LDEwLjY5OTkxNzIgMTEuODI1Mzk1NywxMC44Nzk2MjgzIEw4LjMyNTM5NTY5LDEzLjg3OTYyODMgQzguMTM4MTUwODIsMTQuMDQwMTIzOSA3Ljg2MTg0OTE4LDE0LjA0MDEyMzkgNy42NzQ2MDQzMSwxMy44Nzk2MjgzIEw0LjE3NDYwNDMxLDEwLjg3OTYyODMgQzMuOTY0OTQxMzksMTAuNjk5OTE3MiAzLjk0MDY2MDYyLDEwLjM4NDI2NzIgNC4xMjAzNzE3LDEwLjE3NDYwNDMgQzQuMzAwMDgyNzcsOS45NjQ5NDEzOSA0LjYxNTczMjc3LDkuOTQwNjYwNjIgNC44MjUzOTU2OSwxMC4xMjAzNzE3IEw4LDEyLjg0MTQ2MTEgTDExLjE3NDYwNDMsMTAuMTIwMzcxNyBaIE00LjgyNTM5NTY5LDUuODc5NjI4MyBDNC42MTU3MzI3Nyw2LjA1OTMzOTM4IDQuMzAwMDgyNzcsNi4wMzUwNTg2MSA0LjEyMDM3MTcsNS44MjUzOTU2OSBDMy45NDA2NjA2Miw1LjYxNTczMjc3IDMuOTY0OTQxMzksNS4zMDAwODI3NyA0LjE3NDYwNDMxLDUuMTIwMzcxNyBMNy42NzQ2MDQzMSwyLjEyMDM3MTcgQzcuODYxODQ5MTgsMS45NTk4NzYxIDguMTM4MTUwODIsMS45NTk4NzYxIDguMzI1Mzk1NjksMi4xMjAzNzE3IEwxMS44MjUzOTU3LDUuMTIwMzcxNyBDMTIuMDM1MDU4Niw1LjMwMDA4Mjc3IDEyLjA1OTMzOTQsNS42MTU3MzI3NyAxMS44Nzk2MjgzLDUuODI1Mzk1NjkgQzExLjY5OTkxNzIsNi4wMzUwNTg2MSAxMS4zODQyNjcyLDYuMDU5MzM5MzggMTEuMTc0NjA0Myw1Ljg3OTYyODMgTDgsMy4xNTg1Mzg4OSBMNC44MjUzOTU2OSw1Ljg3OTYyODMgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); diff --git a/static/ui-icons/Fields/FieldMarkdown.svg b/static/ui-icons/Fields/FieldMarkdown.svg new file mode 100644 index 00000000..72a1208d --- /dev/null +++ b/static/ui-icons/Fields/FieldMarkdown.svg @@ -0,0 +1 @@ +markdown \ No newline at end of file diff --git a/test/nbrowser/CellColor.ts b/test/nbrowser/CellColor.ts index acb02f18..a6752cce 100644 --- a/test/nbrowser/CellColor.ts +++ b/test/nbrowser/CellColor.ts @@ -125,8 +125,7 @@ describe('CellColor', function() { await gu.getSection('TABLE1').click(); let cell = await gu.getCell('B', 1).doClick(); await gu.enterCell('foo'); - await driver.findContent('.test-select-button', /HyperLink/).click(); - await gu.waitForServer(); + await gu.setFieldWidgetType('HyperLink'); // check default color of hyperlink cell = await gu.getCell('B', 1).find('.field_clip'); @@ -219,8 +218,7 @@ describe('CellColor', function() { await gu.getSection('TABLE1').click(); // change widget to hyper link - await driver.findContent('.test-select-button', /HyperLink/).click(); - await gu.waitForServer(); + await gu.setFieldWidgetType('HyperLink'); const cell = gu.getCell('A', 1).find('.field_clip'); // check cell show hyperlink @@ -450,8 +448,7 @@ describe('CellColor', function() { await gu.waitForServer(); // change format to hyperlink - await driver.findContent('.test-select-button', /HyperLink/).click(); - await gu.waitForServer(); + await gu.setFieldWidgetType('HyperLink'); // check color is still ok cell = gu.getCell('A', 1).find('.field_clip'); diff --git a/yarn.lock b/yarn.lock index 798534bf..42deee14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -944,11 +944,6 @@ resolved "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz" integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw== -"@types/marked@4.0.8": - version "4.0.8" - resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.8.tgz#b316887ab3499d0a8f4c70b7bd8508f92d477955" - integrity sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw== - "@types/mime-types@2.1.0": version "2.1.0" resolved "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz" @@ -5535,10 +5530,15 @@ make-fetch-happen@^9.1.0: socks-proxy-agent "^6.0.0" ssri "^8.0.0" -marked@4.2.12: - version "4.2.12" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.12.tgz#d69a64e21d71b06250da995dcd065c11083bebb5" - integrity sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw== +marked-highlight@2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/marked-highlight/-/marked-highlight-2.1.4.tgz#33d4d74b55e5acc76ee95fa5117e847795392a42" + integrity sha512-D1GOkcdzP+1dzjoColL7umojefFrASDuLeyaHS0Zr/Uo9jkr1V6vpLRCzfi1djmEaWyK0SYMFtHnpkZ+cwFT1w== + +marked@14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-14.0.0.tgz#79a1477358a59e0660276f8fec76de2c33f35d83" + integrity sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ== md5.js@^1.3.4: version "1.3.5"