Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enh #67 Add collapsible blocks with summary and settings #95

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from

Conversation

schlagmichdoch
Copy link

@schlagmichdoch schlagmichdoch commented May 25, 2023

Why this PR

See #67 for introduction and goal.
Supersedes #69 and #71 as most of it is rewritten everything is rebased into two commits and this PR is based on the develop instead of the master branch.

My contractor uses the humhub wiki as a place for documentation that can get quite long, messy and repetitive. He really craves a feature to have collapsible sections and asked me to give it another try eventhough I have not heard much from you guys. I would really like this feature to get implemented!

I have seen that you have actively worked on this repository a lot for some time now so I hope this is a good time to add this PR.

I really like the new possibility to be able to edit the markdown itself and switch between these two editor modes! Kudos for implementing this!

What does this PR include

This is now a fully working addition to humhub-prosemirror! It works similar to editing of images and links and includes some options for even more flexibility for the user. I can see this beeing used to tidy up very long pages or create FAQs

menu-bar

  • Click on the create button ‣ :
    A collapsible block is added that behaves similar to blockquotes:
    It is possible to nest multiple multiple blocks into each other, to drag and drop blocks (might need to select the block with the Escape key or via ctrl/cmd-click), to break out of the block using backspace while inside a block or delete a block
  • Click the summary tag:
    A menu is opened to edit the summary (which supports markdown), choose whether the collabsible block should be parsed open or closed after saving the page and what style is preferred for the block. For the style the user can choose between "default" and "box" although there could be more possibilities in the future to choose from. The look of these styles can be changed by themes with css

Markdown

The markdown-it-container is used to save the <details><summary> tags as markdown without using html tags.

Rendered Result Markdown
This is always shown

This content is shown by default

::: details state=open style=default summary=This is always shown
This content is shown by default

:::

This is always shown

This content is hidden by default

::: details state=closed style=default summary=This is always shown
This content is hidden by default

:::

With Markdown support

This content is hidden by default

::: details state=closed style=default summary=**With** Markdown `support`
This **content** is `hidden` by default

:::

Outer summary

Before nested collapsible block

Nested summary

Inside nested collapsible block

After second collapsible block

:::: details state=closed style=default summary=Outer summary
Before nested collapsible block
::: details state=closed style=default summary=Nested summary
Inside nested collapsible block
:::
After nested collapsible block
::::

Styling

Both styles need further css styling added to humhubs main repository:

  1. Edit humhub/static/less/markdown.less and add the following lines below the blockquote styling (line 94):
    details {
        margin: 0 0 1.2em;
    }

    details > summary {
        cursor: pointer;
    }

    details > summary p {
        display: inline;
    }

    details > div {
        display: block;
    }

    // style:default
    details:not(.box) > summary {
        display: list-item;
    }

    details:not(.box) > div {
        margin-left: 12px;
    }

    // style:box
    details.box {
        margin: 1em 0;
    }

    details.box > summary {
        list-style: none;
        border: 1px solid;
        background: #fafafa;
        padding: 1em 1.5em 1em 1em;
        position: relative;

        &::after {
            content: '';
            border-right: 4px solid;
            border-bottom: 4px solid;
            position: absolute;
            right: 1.1em;
            height: 1em;
            width: 1em;
            transform: rotate(45deg) translateY(0.2em);
        }

        &:focus {
            outline: 2px solid;
            outline-offset: -2px;
        }

        &::-webkit-details-marker {
            display: none;
        }
    }

    details.box[open] > summary::after {
        transform: rotate(-135deg) translatey(-0.35em) translatex(-0.35em);
        right: 1.3em;
    }

    details.box > div {
        padding: 1em;
        border: 1px solid;
        border-top: 0;
    }
  1. Run grunt build-theme

Screenshots

Default Style

StyleDefault

Box Style

StyleBox

Edit Menu

EditMenu

@luke-
Copy link
Contributor

luke- commented May 26, 2023

@schlagmichdoch Thanks for the updated PR and sorry for the delay.

i am a bit careful with syntax extensions, because they have to be supported by all internal & external parsers (see internal RichText Parser here: https://community.humhub.com/s/contribution-core-development/wiki/Richtext#converter-classes) and can hardly changed or removed later on.

@schlagmichdoch
Copy link
Author

How do you do this with the special syntax for internal links or mentions?
Should I check the internal parser for their compatibility with the fenced container syntax?
Or would you prefer to save the html nodes <details><summary> directly into markdown as GitHub does?

@luke-
Copy link
Contributor

luke- commented May 26, 2023

Special features such as Mentioning or OEmbed must be supported by the MarkdownConverters in addition to Prosemirror support.

See Extensions here:
https://github.com/humhub/humhub/tree/master/protected/humhub/modules/content/widgets/richtext/extensions

I am not sure atm which syntax (:: or <summary>) is the best one for collapsible blocks.

Probably tags and some HTML support would be best. However, such a change would have many other effects. (user possibilities, parser support, security).

@schlagmichdoch
Copy link
Author

schlagmichdoch commented May 26, 2023

Special features such as Mentioning or OEmbed must be supported by the MarkdownConverters in addition to Prosemirror support.

See Extensions here: https://github.com/humhub/humhub/tree/master/protected/humhub/modules/content/widgets/richtext/extensions

I will take a look into that. The goal would be that the converter successfully transforms the markdown snippet into the correct <details><summary> html block, wouldn't it? Would I then add a PR for humhub or is everything regarding richtext rolled up from a different repo?

Probably tags and some HTML support would be best. However, such a change would have many other effects. (user possibilities, parser support, security).

We would then be full circle where we started: #69 (comment) with an error regarding html_blocks not being supported. As you mentioned this would be a recipe for XSS attacks.

Something like this could work if not properly filtered:

<details onclick=alert("XSS")>

doapdjao

</details>

This is why I prefer the ::: syntax as it strips down the users possibility what to include into the page

@luke-
Copy link
Contributor

luke- commented May 26, 2023

@schlagmichdoch Not sure how to procced here. Currently I would probably prefer the way via <details>, as it seems to be the most common way. However, this has some side effects that we have discussed before.

I'm worried that workarounds like :: details [options] may cause (migration-) problems in future.

@schlagmichdoch
Copy link
Author

schlagmichdoch commented May 26, 2023

I'm worried that workarounds like :: details [options] may cause (migration-) problems in future.

I understand that.
There are some applications that implement Pandocs markdown that use attributes/options with fenced code blocks. These put attributes/options inside curly brackets which might help with migrations if implemented correctly:

My implementation is based on common marks implemention of fenced code blocks: https://spec.commonmark.org/0.30/#code-fence
that supports info strings like so:

```ruby startline=3
def foo(x)
  return 3
end
```

If you prefer GitHub Flavored Markdown we would need to make sure that html input that is parsed is stripped of all javascript tags like onclick or onmousehover
And specifically only allow certain html tags.

Interestingly, the newline tag already seems to be allowed eventhough html is off. Maybe there is a possibility to save html to the db without the need to switch html=true in render.js markdownItOptions:
Typing <br><br><br><br><br> does indead add linebreaks that get transformed to \ in markdown. When doing the same thing inside a table it is saved into the database as a html tag:

| header |
| ------ |
| content<br><br><br>content |

I could look further into this.

@schlagmichdoch
Copy link
Author

Typing <br><br><br><br><br> does indead add linebreaks that get transformed to \ in markdown.

This seems to be a result of allowing breaks with markdownItOptions:

let markdownItOptions = context && context.options.markdownIt || {html: false, breaks: true, linkify: true};

When switching breaks to false the linebreaks outside of tables are stripped away but they are still somehow preserved inside tables.

@schlagmichdoch
Copy link
Author

One more data point for your decision:
https://github.com/markdown-it/markdown-it/blob/master/docs/security.md

If switching html on, we would need an external sanitizer.

Following their plugin link we get two plugins using a slightly different syntax:

The latter uses two different chars:
Default OPEN:

+++ Click me!
Hidden text
+++

Default CLOSED:

>>> Click me!
Hidden text
>>>

I like to give the user two different styles to choose from but if you think the box style is enough we could also go that route.

@schlagmichdoch
Copy link
Author

Hey @luke-, Have you had the time, to take a look at the markdown-it documentation? How would you like to proceed? Can I do something to help with the implementation?

@luke-
Copy link
Contributor

luke- commented Jun 5, 2023

@schlagmichdoch Haven't had time for this topic yet. I hope I get to it soon. It's not something I want to decide on the fly, unfortunately.

@schlagmichdoch
Copy link
Author

Alright! Looking forward to it.

If we decide for the html variant this would probably the way to go:

@schlagmichdoch
Copy link
Author

schlagmichdoch commented Jun 21, 2023

Hi @luke- ,
I would appreciate it if you could give me some update!

  • Do you know when you and your team will have time to have a look at this and add it to the active development?
  • Is there any roadmap in place when you are going to merge the current develop branch into master and then into a release?

If there is anything I can do to help please don't hesitate to tell me here or send me an e-mail.

@luke-
Copy link
Contributor

luke- commented Jun 21, 2023

@schlagmichdoch Sorry, for your (syntax) extension I can't say concrete anything yet.
We will merge the develop branch quite soon and then release v.15.

Hopefully we will then have a bit more capacity to address the issue of Markdown extensions (either HTML or MarkdownSyntax).

@schlagmichdoch
Copy link
Author

Thanks for the update!
I guess we’ll have to wait until you’re ready then.
Are there any other markdown extensions you’re thinking about? Syntax Highlighting could be a valuable addition.

I would appreciate it, if you could keep me in the loop and tell me if I can do anything to speed things up or clarify anything.

@schlagmichdoch
Copy link
Author

Hi @luke- ,
Now that v1.15 is in beta, do you have any news regarding the implementation of this feature?

@luke-
Copy link
Contributor

luke- commented Sep 15, 2023

@schlagmichdoch Unfortunately not yet. I also don't know when and if/how we will tackle the issue.

Maybe it could be a solution for you - in the meantime - to fork the prosemirror project first and manually overwrite the editor in your installation?

@ArchBlood
Copy link

I think this isn't a bad idea, but I think it would be a nifty idea to do something similar to the Katex module for this.

@schlagmichdoch
Copy link
Author

@ArchBlood Thanks for joining the discussion!
I do not know the Katex module. How is it installed in Humhub? Is this an official plugin?

Why do you think collapsible sections would be better as a Katex-like plugin rather than as part of the richtext editor (prosemirror) itself?

@ArchBlood
Copy link

ArchBlood commented May 8, 2024

@ArchBlood Thanks for joining the discussion! I do not know the Katex module. How is it installed in Humhub? Is this an official plugin?

Why do you think collapsible sections would be better as a Katex-like plugin rather than as part of the richtext editor (prosemirror) itself?

The Katex module is a part of @humhub-contrib which is under the @humhub management, the module is installed just like any other module, the only difference with this module is that you must build the js bundle, see the README.md.

As to why I think it would be better as a module, I think it would be better to keep the current ProseMirror editor as it is without complexing it anymore than it should be and extend as part of a module so that it's an optional method that can be installed and uninstalled based on HumHub instance preference.

@schlagmichdoch
Copy link
Author

Sounds promising!
Especially the possibility to use database migrations when activating and deactivation a module sounds good.

If I understand it correctly, the Katex module only renders latex syntax written with the editor.
(https://github.com/humhub-contrib/katex/blob/master/resources/js/src/main.js)

Would I be able to add buttons/pop-ups to the ProseMirror GUI as well or would I only be able to render some special syntax?

@ArchBlood
Copy link

Sounds promising! Especially the possibility to use database migrations when activating and deactivation a module sounds good.

If I understand it correctly, the Katex module only renders latex syntax written with the editor. (https://github.com/humhub-contrib/katex/blob/master/resources/js/src/main.js)

Would I be able to add buttons/pop-ups to the ProseMirror GUI as well or would I only be able to render some special syntax?

I think it would be possible with extending an existing editor class;
https://github.com/humhub/humhub/tree/master/protected/humhub/modules/content/widgets/richtext

@schlagmichdoch
Copy link
Author

Sounds promising! Not sure when I will have time to get into that though.

As food for thought: It would also be possible to implement this alongside admonitions: https://squidfunk.github.io/mkdocs-material/reference/admonitions/#admonition-icons-fontawesome

This would allow users to add info or warning boxes on wiki pages and alike. All of them can be rendered collapsible, either collapsed or not collapsed.
image

There are also two markdown-it plugins, although they are both missing the collapsible admonations:

@schlagmichdoch
Copy link
Author

schlagmichdoch commented Jul 16, 2024

@luke- Do you think this is doable via a Humhub module? I wonder if this would only include extending the rendering of a certain syntax (as with the LaTeX module) or whether I could implement a functioning editor node as is the case with this pull request. Any ideas?

I'd prefer to implement this via a module as it would be easy to install / deinstall for admins, but I'm unsure whether it's worth it and whether everything would work in the end.

@luke-
Copy link
Contributor

luke- commented Jul 16, 2024

Adding such features as a Markdown extension would be a good and preferred solution.

Unfortunately, there is currently no such ready to use module API here.

However, I think the foundation for this has already been created on the PHP side with Markdown Extensions. https://github.com/humhub/humhub/tree/master/protected/humhub/modules/content/widgets/richtext/extensions

@schlagmichdoch
Copy link
Author

What would you do in my place? Is this already doable as a HumHub module or is there some work needed to add this API to the humhub code?
I have taken a look at the richtext/extensions folder but I guess it wouldn't be enough to use PHP only, as this is an extension of the prosemirror. How would I install such an extension? Would this rather be another PR to the humhub code or would this be able to be installed via the marketplace?

@luke-
Copy link
Contributor

luke- commented Jul 19, 2024

Good question. Unfortunately, I can't estimate how complex it is to make the Markdown features pluggable.
This is currently not possible and would have to be planned and developed.
Markdown features must always be handled in rendering both on the JS side (Prosemirror) and via the PHP renderer.


If we had such an Plugin API, we could, for example, put a Markdown Graph module in the Marketplace, which would register another Javascript file with the RichTextEditor Widget Rendering (Edit or View), which handles the Prosemirror Extension (Menu Entry & Rendering). In addition, it should register a PHP extension for the PHP renderer.

We could probably provide some help for the basic structure of the module.

But we need a prototype on the prosemirror side first. (Plugin)

@ArchBlood
Copy link

Good question. Unfortunately, I can't estimate how complex it is to make the Markdown features pluggable.
This is currently not possible and would have to be planned and developed.
Markdown features must always be handled in rendering both on the JS side (Prosemirror) and via the PHP renderer.


If we had such an Plugin API, we could, for example, put a Markdown Graph module in the Marketplace, which would register another Javascript file with the RichTextEditor Widget Rendering (Edit or View), which handles the Prosemirror Extension (Menu Entry & Rendering). In addition, it should register a PHP extension for the PHP renderer.

We could probably provide some help for the basic structure of the module.

But we need a prototype on the prosemirror side first. (Plugin)

I believe we'd probably also need a convertible process if we were going the pluggable route, but as you stated without a strong solid Plugin API this would be rather difficult, but would be intriguing to say the least. 🤔

@ArchBlood
Copy link

I'd suggest something similar to my current module that is under review by HumHub even though it's in its infancy, the PluginManager is rather powerful and will continue to mature in the future.

@schlagmichdoch
Copy link
Author

@ArchBlood can you provide a link with more information or explain what that PluginManager of yours is about?

@ArchBlood
Copy link

@ArchBlood can you provide a link with more information or explain what that PluginManager of yours is about?

Currently not due to it being part of a paid module that's in active development.

@ArchBlood
Copy link

ArchBlood commented Jul 22, 2024

But currently the idea behind the PluginManager is to allow for another layer of addons to be possible within my forums module, it currently works as expected as long as the plugin follows a simple file structure which is similar to that of a HumHub module.

Plugin Diagram

graph TD
    A[example] --> B[assets]
    A --> C[controllers]
    A --> D[models]
    A --> E[views]
    A --> F[Plugin.php]
    A --> G[config.php]
    A --> H[plugin.json]
    A --> I[README.md]

    B -->|"CSS, JS, images"| B1[Static files]
    C -->|"Action handlers"| C1[Controller classes]
    D -->|"Data structures"| D1[Model classes]
    E -->|"Templates"| E1[View files]
    F -->|"Main plugin class"| F1[Plugin logic]
    G -->|"Configuration"| G1[Plugin settings]
    H -->|"Metadata"| H1[Plugin information]
    I -->|"Documentation"| I1[Usage instructions]
Loading

Current example for such a plugin is found here, but again the PluginManager itself is built inside of my paid module so I can't really just throw it out into the world. But the idea of creating a Plugin Management module could be in theory possible to create.

I could however provide a possible example for a PluginManager for ProseMirror

Example PluginManager

<?php

namespace humhub\modules\content\components;

use yii\base\Component;
use yii\base\InvalidConfigException;

class PluginManager extends Component
{
    private $_plugins = [];

    public function registerPlugin($pluginClass)
    {
        if (!class_exists($pluginClass)) {
            throw new InvalidConfigException("Plugin class '{$pluginClass}' does not exist.");
        }

        $plugin = \Yii::createObject($pluginClass);

        if (!method_exists($plugin, 'getClientScript')) {
            throw new InvalidConfigException("Plugin '{$pluginClass}' must have a 'getClientScript' method.");
        }

        $this->_plugins[] = $plugin;
    }

    public function getClientPlugins()
    {
        return array_map(function($plugin) {
            return $plugin->getClientPlugin();
        }, $this->_plugins);
    }

    public function getClientScripts()
    {
        $scripts = [];
        foreach ($this->_plugins as $plugin) {
            $scripts[] = $plugin->getClientScript();
        }
        return $scripts;
    }
}

Example JS

humhub.module('ui.richtext', function(module, require, $) {
    var prosemirror = require('prosemirror');
    var Plugin = prosemirror.Plugin;

    var PluginManager = function() {
        this.plugins = [];
    };

    PluginManager.prototype.registerPlugin = function(pluginData) {
        if (typeof pluginData.plugin !== 'function') {
            console.error('Plugin must be a function that returns a ProseMirror Plugin instance');
            return;
        }

        var plugin = pluginData.plugin();
        if (!(plugin instanceof Plugin)) {
            console.error('Plugin factory must return a ProseMirror Plugin instance');
            return;
        }

        this.plugins.push(plugin);
    };

    PluginManager.prototype.getPlugins = function() {
        return this.plugins;
    };

    var init = function(config) {
        config = config || {};
        var pluginManager = new PluginManager();

        // Register plugins from PHP-side
        if (Array.isArray(config.plugins)) {
            config.plugins.forEach(function(pluginData) {
                pluginManager.registerPlugin({
                    key: pluginData.key,
                    plugin: eval('(' + pluginData.plugin + ')')
                });
            });
        }

        // Initialize richtext for all compatible textareas
        $('textarea[data-ui-richtext]').each(function() {
            var $textarea = $(this);
            var editorConfig = $textarea.data('ui-richtext') || {};

            var richTextEditor = new prosemirror.EditorView(this, {
                state: prosemirror.EditorState.create({
                    plugins: pluginManager.getPlugins(),
                    // Add other necessary state configuration
                }),
                // Add other necessary view configuration
            });

            $textarea.data('prosemirror-instance', richTextEditor);
        });
    };

    module.export({
        init: init,
        PluginManager: PluginManager
    });
});

// To initialize:
$(document).on('humhub:ready', function() {
    var richTextConfig = humhub.config.get('ui.richtext');
    humhub.modules.ui.richtext.init(richTextConfig);
});

@ArchBlood
Copy link

@luke- @schlagmichdoch I've been working on a similar method within a helper class which provides similar functionalities to this within my forums module, where I can confirm that it is very possible to implement this within a module, but requires some more testing of RichText::convert() and a custom widget class to append everything together, you can find the example screenshots within the community site, currently there is no ways around this way other than using an append injection method which is slightly hackish and can't be guaranteed to work in the future as code changes. My honest recommendation would be creating a widget class to replace the current use of RichText::output() that could internally use this method in combination with the convert() function, maybe something like this? This would make things more flexible when trying to implement functions alongside the RichText class.

<?php

namespace humhub\modules\content\widgets\richtext;

use humhub\components\Widget;
use humhub\modules\content\widgets\richtext\RichText;

/**
 * RichTextWidget processes rich text content using HumHub's RichText class.
 *
 * This widget can be used to output or convert rich text content. It uses
 * RichText::output() when no format is specified, and RichText::convert()
 * when a specific format is provided.
 */
class RichTextWidget extends Widget
{
    /** @var string The rich text content to process */
    public string $text;

    /** @var string|null The format to convert the text to (null for default output) */
    public ?string $format = null;

    /** @var array<string, mixed> Additional options for text processing */
    public array $options = [];

    /**
     * Runs the widget and returns the processed text.
     *
     * @return string The processed rich text content
     */
    public function run(): string
    {
        return self::processText($this->text, $this->format, $this->options);
    }

    /**
     * Processes the given rich text content.
     *
     * If no format is specified, it uses RichText::output().
     * If a format is specified, it uses RichText::convert().
     *
     * @param string $text The rich text content to process
     * @param string|null $format The format to convert the text to (null for default output)
     * @param array<string, mixed> $options Additional options for text processing
     * @return string The processed rich text content
     */
    public static function processText(string $text, ?string $format = null, array $options = []): string
    {
        if ($format === null) {
            return RichText::output($text, $options);
        }
        return RichText::convert($text, $format, $options);
    }
}

https://community.humhub.com/content/perma?id=280101

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants