mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(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
This commit is contained in:
@@ -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
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
</div>`;
|
||||
};
|
||||
|
||||
renderer.link = (href: string | null, _title: string | null, text: string) => {
|
||||
renderer.link = ({href, text}) => {
|
||||
return `<a href="${href}" target="_blank">${text}</a>`;
|
||||
};
|
||||
|
||||
12
app/client/ui/MarkdownCellRenderer.ts
Normal file
12
app/client/ui/MarkdownCellRenderer.ts
Normal file
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user