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

Inline sync plugin #695

Merged
merged 19 commits into from
Aug 30, 2022
Merged

Inline sync plugin #695

merged 19 commits into from
Aug 30, 2022

Conversation

Saul-Mirone
Copy link
Member

  • I read the contributing guide
  • I agree to follow the code of conduct

Summary

There're lots of bugs caused by consistency between markdown AST and UI.

For example:
#637
#553
#622

The inline-sync-plugin can help to solve this issue. Instead of using inputrules to check user input, this plugin keep sync the prosemirror node between markdown AST for inline nodes (such as paragraph and heading).

How did you test this change?

Manually test and e2e test.

Sorry, something went wrong.

@nx-cloud
Copy link

nx-cloud bot commented Aug 29, 2022

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 52efc1a. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this branch


✅ Successfully ran 2 targets

Sent with 💌 from NxCloud.

@vercel
Copy link

vercel bot commented Aug 29, 2022

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated
milkdown ✅ Ready (Inspect) Visit Preview Aug 30, 2022 at 5:08AM (UTC)

@Saul-Mirone Saul-Mirone changed the title Try inline sync plugin Inline sync plugin Aug 29, 2022
@4-1-1
Copy link

4-1-1 commented Aug 31, 2022

Seemly , It will broken the editor history.

@Saul-Mirone
Copy link
Member Author

Seemly , It will broken the editor history.

Do you have reproducable steps of videos?

@4-1-1
Copy link

4-1-1 commented Aug 31, 2022

Seemly , It will broken the editor history.

Do you have reproducable steps of videos?

#704 like this

@4-1-1
Copy link

4-1-1 commented Sep 1, 2022

Could you make the Inline sync plugin as an option. because I have do support abc**de** style .
by override a package name "micromark-util-classify-character"

@Saul-Mirone
Copy link
Member Author

Could you make the Inline sync plugin as an option. because I have do support abc**de** style . by override a package name "micromark-util-classify-character"

I can certiainly make it can be turned off, but in my opinion abc**de** can work as usual.
https://milkdown.dev/online-demo?text=IYIwxgVBAmCmUCgg

@4-1-1
Copy link

4-1-1 commented Sep 1, 2022

Could you make the Inline sync plugin as an option. because I have do support abc**de** style . by override a package name "micromark-util-classify-character"

I can certiainly make it can be turned off, but in my opinion abc**de** can work as usual. https://milkdown.dev/online-demo?text=IYIwxgVBAmCmUCgg

sorry , abc**'de**abc**'de**

@Saul-Mirone
Copy link
Member Author

Could you make the Inline sync plugin as an option. because I have do support abc**de** style . by override a package name "micromark-util-classify-character"

I can certiainly make it can be turned off, but in my opinion abc**de** can work as usual. https://milkdown.dev/online-demo?text=IYIwxgVBAmCmUCgg

sorry , abc**'de**abc**'de**

Do you mean you need abd**'de** to render the 'de as a strong mark? I think that's not allowed by markdown and only possible if you make a remark plugin to support that. The previous render behavior of milkdown is a bug.

@4-1-1
Copy link

4-1-1 commented Sep 2, 2022

I have done it. but maybe will bring some other bugs,not sure, runs fine right now

@z-950
Copy link
Contributor

z-950 commented Sep 4, 2022

Early parsing markdown break plugins that uses remark-directive.
Because remark-directive has no name whitelist. It will throw error Cannot match target parser at the first character after : and then can not input but delete :.
So any plugins that like this is also broken.

example

import { createCmd, createCmdKey } from '@milkdown/core';
import { AtomList, createMark } from '@milkdown/utils';
import directive from 'remark-directive';

export const ModifyColorlulText = createCmdKey<string>('ModifyColorlulText');
const id = 'colorfulText';

const colorfulTextMark = createMark(() => {
    return {
        id,
        schema: () => ({
            attrs: {
                hex: {},
            },
            inclusive: true,
            parseDOM: [
                {
                    tag: 'span',
                    getAttrs: (dom) => {
                        if (!(dom instanceof HTMLElement)) {
                            console.error(new Error('impossible!'));
                            return false;
                        }
                        let { color } = dom.style;
                        if (color === '') return false;
                        color = `#${color
                            .slice(4, -1)
                            .split(', ')
                            .map((v) => parseInt(v).toString(16))
                            .join('')}`;
                        return { hex: color };
                    },
                },
            ],
            toDOM: (mark) => {
                const style = `color:${mark.attrs['hex']};`;
                return ['span', { style }];
            },
            parseMarkdown: {
                match: (node) => node.type === 'textDirective' && node['name'] === 'colorful',
                runner: (state, node, markType) => {
                    state.openMark(markType, { hex: (node['attributes'] as { hex: string }).hex });
                    state.next(node.children);
                    state.closeMark(markType);
                },
            },
            toMarkdown: {
                match: (node) => node.type.name === id,
                runner: (state, mark) => {
                    state.withMark(mark, 'textDirective', undefined, {
                        name: 'colorful',
                        attributes: {
                            hex: mark.attrs['hex'],
                        },
                    });
                },
            },
        }),
        commands: (markType) => [
            createCmd(ModifyColorlulText, (hex = '') => (state, dispatch) => {
                if (!dispatch) return false;

                const { from, to } = state.selection;

                const { tr } = state;

                if (hex === '') {
                    dispatch(tr.removeMark(from, to, markType));
                } else {
                    const colorMark = markType.create({ hex });
                    if (!colorMark) return false;
                    dispatch(tr.addMark(from, to, colorMark));
                }
                return true;
            }),
        ],
        remarkPlugins: () => [directive],
    };
});

// color
export const colorfulText = AtomList.create([colorfulTextMark()]);

Strictly speaking, remark-directive must have name whitelist, because it will match :name as text (also leaf, container) directive. :name is so common to appear in content.

@4-1-1
Copy link

4-1-1 commented Sep 5, 2022

@z-950
Copy link
Contributor

z-950 commented Sep 5, 2022

@4-1-1 It's not a perfect realization of the fallback. I try do this, but it will lost some others directive.
This plugin can't solve nested directives. And some other lost like: :i[lovely] becomes :i.

@z-950
Copy link
Contributor

z-950 commented Sep 5, 2022

@4-1-1
The whitelist of remark-directive is the only one solution.
So the current fallback is like this: writing our remark-directive, which means to control the markdown ast result.
Only need change factory-name in micromark-extension-directive imported by remark-directive.

export function factoryName(effects, ok, nok, type) {
  const self = this;

  let nameCache = ''; // {1}

  return start;

  /** @type {State} */
  function start(code) {
    if (asciiAlpha(code)) {
      nameCache = String.fromCharCode(code); // {2}
      effects.enter(type);
      effects.consume(code);
      return name;
    }

    return nok(code);
  }

  /** @type {State} */
  function name(code) {
    if (
      code === codes.dash ||
      code === codes.underscore ||
      asciiAlphanumeric(code)
    ) {
      nameCache += String.fromCharCode(code); // {3}
      effects.consume(code);
      return name;
    }

    effects.exit(type);
    return self.previous === codes.dash || self.previous === codes.underscore || !whitelist.includes(nameCache) //{4}
      ? nok(code)
      : ok(code);
  }
}

@Saul-Mirone
Copy link
Member Author

I've added some options for sync plugin to make the behavior can be controled by users.

@@ -13,6 +12,7 @@ export const strong = createMark<Keys>((utils) => {
return {
id,
schema: () => ({
inclusive: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

inclusive: false causes the mark to end after one character is entered.

inclusive

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a trade off to support inline sync. We need to provide custom toggle mark command to fix this.

Copy link
Contributor

@iHaPBoy iHaPBoy Nov 11, 2022

Choose a reason for hiding this comment

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

@Saul-Mirone Okay, I've created an issue for this. #809

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.

None yet

4 participants