Skip to content

Conversation

ogui11aume
Copy link

Context

So far Grist only permits raw text to appear as a one-liner in modal during a document tour : not a very nice user experience for somebody to get a first impression of your super Grist document. 🙁
With Markdown, the document tours are much better:

  • rich text with new lines and line spacing
  • multiple clickable links (multiple) directly within the text, not handled as a separate data
  • images

Proposed solution

Technically :
Just parsing the Body cell from GristDocTour table with already available marked library, sanitize and following the renderer approach to better select which aspect of markdown to render.

Documentation : https://github.com/ogui11aume/hackdays2025/blob/ergonogrist/submissions/ergonogrist/assets/markdown-for-gristdoctour/deliverable1.md

Related issues

Didn't find any issue ( and I didn't open one although I made the change at the request of other users).

Has this been tested?

Screenshots / Screencasts

https://github.com/ogui11aume/hackdays2025/blob/ergonogrist/submissions/ergonogrist/assets/markdown-for-gristdoctour/deliverable1.md

Example of Markdown during a document tour

Copy link
Collaborator

@fflorent fflorent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me. Thank you very much!

I just wondered why the iframe were allowed in sanitizeTutorialHTML. Sounds like it is required and used in the official tutorial to embed a Youtube video.

We will ask for an official review from Grist Labs.

@fflorent fflorent requested a review from paulfitz June 5, 2025 09:53
@fflorent fflorent moved this to Needs feedback in French administration Board Jun 5, 2025
@georgegevoian georgegevoian requested review from georgegevoian and removed request for paulfitz June 18, 2025 14:29
import sortBy = require('lodash/sortBy');
import {marked} from "marked";
import {renderer} from 'app/client/ui/DocTutorialRenderer';
import {sanitizeTutorialHTML} from "./sanitizeHTML";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import {sanitizeTutorialHTML} from "./sanitizeHTML";
import {sanitizeHTML} from "app/client/ui/sanitizeHTML";

You can use this variant instead, unless you need to tweak the default sanitization behavior, in which case you can just add a new variant like sanitizeTourHTML.

If no tweaking is necessary, you can just use the markdown helper from app/client/lib/markdown.ts.

Minor lint: absolute module path is preferred over relative.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback, I'm going the new variant way with 'sanitizeTourHTML' (with absolute module path) to keep things clear and have the ability to embed videos as @fflorent noted earlier.

import {dom} from 'grainjs';
import sortBy = require('lodash/sortBy');
import {marked} from "marked";
import {renderer} from 'app/client/ui/DocTutorialRenderer';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import {renderer} from 'app/client/ui/DocTutorialRenderer';
import {renderer} from 'app/client/ui/DocTourRenderer';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DocTourRenderer isn't being imported here.

Comment on lines 51 to 61

const bodyHtmlContent = sanitizeTutorialHTML(marked.parse(getValue("Body"), {
async: false, renderer
}));
const element = document.createElement('div');
element.innerHTML = bodyHtmlContent;


let body: HTMLElement = element;


Copy link
Contributor

@georgegevoian georgegevoian Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can replace the dom('p', body), below to something like dom('p', markdown(body)), or dom('p', el => el.innerHTML = bodyHtmlContent), depending on whether you're using the markdown helper or your own renderer. This way, you only need at most bodyHtmlContent and none of the other changes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @georgegevoian for the feedback.
I'm going to look into incorporating these changes and adding tests to check for MarkDown content rendering.

@georgegevoian
Copy link
Contributor

Thanks for the PR @ogui11aume.

For tests, we'll work on moving some tour-related browser tests to this repo by next week. That way, you can test your changes against them and add a test. A test that asserts a tour containing Markdown text added the expected HTML to the DOM should be enough, but you're welcome to add more.

@georgegevoian
Copy link
Contributor

Tests are now in grist-core: 2997ca8.

@berhalak berhalak self-requested a review June 25, 2025 15:48
@hexaltation hexaltation moved this from Needs feedback to In Progress in French administration Board Jul 9, 2025
@ogui11aume ogui11aume force-pushed the markdown-for-gristdoctour branch from 391b30b to 5c4f6a7 Compare July 11, 2025 14:16
Copy link
Contributor

github-actions bot commented Jul 11, 2025

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@hexaltation
Copy link
Collaborator

@ogui11aume
It seems that you've broken the following test :

1) DocTour
       should create correct popups from a GristDocTour table:

You're modifications do not produce the same html has before, can you fix it ?

An can you add the mail you're signing your commits with in your email account and verify it? you will need It to sign the new CLA.

@ogui11aume ogui11aume force-pushed the markdown-for-gristdoctour branch from 5c4f6a7 to a3d0ab6 Compare July 25, 2025 12:42

export const renderer = new marked.Renderer();

renderer.link = ({href, text}) =>
Copy link
Contributor

@berhalak berhalak Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the default marked.Renderer allows arbitrary html, which potentially allows some phishing attacks. For example with <div style="position: fixed; color: red; inset: 0">Boo</div> you can cover whole Grist layout and provide your own.

What do you think about reusing the renderer from app/client/ui/MarkdownCellRenderer.ts? It clears out any html content.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used ALLOWED_TAGS with marked.Renderer in app/client/ui/sanitizeHTML.ts to limit abuse and only allow those HTML elements that have been tested (+ table).

  • Agreed, this does not yet avoid the problem you described ( covering div ). To avoid the problem in your example, I would also need to limit the usage of style at least in div, should I go that far ?
  • The problem with app/client/ui/MarkdownCellRenderer.ts is that, according to comment, it will not permit images or other HTML elements. So I didn't go that way.
    * This does not support HTML or images in the markdown value, and renders those as text.
    :

This does not support HTML or images in the markdown value, and renders those as text.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ogui11aume, what if we disallowed HTML for the time being?

renderer.html = ({raw}) => escape(raw);

Would there be an immediate need for tours to support arbitrary HTML?

Having a robust and shared sanitization approach across different UI components like tutorials, tours, etc. seems desirable, but could perhaps be reconciled later. (Tutorials in particular should probably also disallow HTML outside of the specific iframe that's permitted, but that's unrelated to your proposed changes.)

@ogui11aume ogui11aume force-pushed the markdown-for-gristdoctour branch from a3d0ab6 to c8fb883 Compare July 25, 2025 14:14
@ogui11aume
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

github-actions bot added a commit that referenced this pull request Jul 25, 2025
@fflorent
Copy link
Collaborator

@ogui11aume Do you need our review again?

@ogui11aume ogui11aume force-pushed the markdown-for-gristdoctour branch from 1b18447 to 4c3ddcd Compare August 1, 2025 09:52
@ogui11aume
Copy link
Author

ogui11aume commented Aug 1, 2025

@ogui11aume Do you need our review again?
Yes please, I need a new review.

For my commits I have :

  • Added the mail I'm signing my commits with in Github account and verified it.
  • Signed my commits
  • Taken into account your advice regarding some code refactoring (thanks @georgegevoian, I've only just seen @berhalak sanitizeHTMLIntoDOM suggestion from last week )
  • Fixed existing nbrowser tests without altering content in existing lines in doctour.grist (thanks @hexaltation )
  • Added new lines in doctour.grist to test rendering Markdown formatting syntax, headings, image, link, quote, lists (ordered and unordered), code blocks.

Most other Markdown extended syntax are not currently being rendered so I didn't create tests for them. Let me know if you thinki other tests would be useful.

@ogui11aume ogui11aume force-pushed the markdown-for-gristdoctour branch from 4c3ddcd to 26d9687 Compare August 1, 2025 13:52
@ogui11aume
Copy link
Author

Hi, please help ...
I'm not sure why this nbrowser test won't pass : https://github.com/gristlabs/grist-core/actions/runs/16676842047/job/47205430863?pr=1653#step:23:850

Refactor using DOM_FRAGMENT like in sanitizeHTMLIntoDOM
gristlabs#1653 (comment)
and specify allowed HTML tags to limit abuse according to
gristlabs#1653 (comment)
@ogui11aume ogui11aume requested a review from berhalak August 6, 2025 13:38
@fflorent fflorent moved this from In Progress to Needs Internal Feedback in French administration Board Aug 6, 2025
@fflorent
Copy link
Collaborator

fflorent commented Aug 7, 2025

@ogui11aume The LeftPanel test is unstable, not related to your work (though I can't make it fail locally).
I take a look a the tests for the Grist Tour.

Copy link
Collaborator

@fflorent fflorent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ogui11aume I don't seem to be able to make the test file fail locally (I ran the DocTour tests 20 times with no failure).

Do you reproduce the failure on your side?

@ogui11aume
Copy link
Author

Hi @fflorent

@ogui11aume I don't seem to be able to make the test file fail locally (I ran the DocTour tests 20 times with no failure).

With my latest commit, all tests pass.

If you refer to my previous call for help then @georgegevoian suggested that the test was the wrong way around and I provided confirmation with the actual intended purpose of the test. I then fixed the problem 😄 .

Do you reproduce the failure on your side?

What that the test failure you were trying to reproduce ?

@fflorent
Copy link
Collaborator

fflorent commented Aug 8, 2025

@ogui11aume I was trying to reproduce this failure your mentioned earlier:
https://github.com/gristlabs/grist-core/actions/runs/16676842047/job/47205430863?pr=1653#step:23:850

As Github collapses the resolved comments, I miss that point (I should be more vigilant next time).

Anyway, glad you fixed the issue :)

import {dom} from 'grainjs';
import sortBy = require('lodash/sortBy');
import {marked} from "marked";
import {renderer} from 'app/client/ui/DocTutorialRenderer';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DocTourRenderer isn't being imported here.

if (!title && !(bodyValue.trim()) ) {
return null;
}
//const bodyHtmlContent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//const bodyHtmlContent


export const renderer = new marked.Renderer();

renderer.link = ({href, text}) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ogui11aume, what if we disallowed HTML for the time being?

renderer.html = ({raw}) => escape(raw);

Would there be an immediate need for tours to support arbitrary HTML?

Having a robust and shared sanitization approach across different UI components like tutorials, tours, etc. seems desirable, but could perhaps be reconciled later. (Tutorials in particular should probably also disallow HTML outside of the specific iframe that's permitted, but that's unrelated to your proposed changes.)

@hexaltation hexaltation moved this from Needs Internal Feedback to In Progress in French administration Board Aug 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

5 participants