lexical icon indicating copy to clipboard operation
lexical copied to clipboard

Bug: Block math equation is falsely converted to inline equation on MarkdownShortcutPlugin

Open MaxHamscher opened this issue 1 year ago • 5 comments

Typing an equation like "$$x^2 + y^2 = z^2$$" via "Insert Equation" and then pressing the MarkdownShortcutPlugin changes the equation to inline ($...$).

Lexical version: Current Playground version

Steps To Reproduce

  1. Enter an equation, e.g. "x^2 + y^2 = z^2" via the toolbar (Insert -> Insert Equation) and uncheck "inline".
  2. Press the Markdown button

Link to code example: https://playground.lexical.dev/

The current behavior

Block math equations like "$$...$$" are converted to inline "$...$" when using the ##MarkdownShortcutPlugin.

The expected behavior

Block math equations like "$$...$$" should stay the same when using the MarkdownShortcutPlugin.

Potential Solutions

Possibly quick: A new BLOCK_EQUATION transformer could be added to address the "$$" scenario. MarkdownTransformers Way more complicated: One might migrate "$, $$" in general to modern LaTex syntax like "[...]"

Impact of fix

Users who rely on markdown capabilities are type safe with equations.

MaxHamscher avatar Dec 10 '24 12:12 MaxHamscher

I may be wrong but I dont think there is a need for a new transformer... The issue might stem from EQUATION only handling inline. if its possible to accommodate both inline and block.

i tried to rewrite the EQUATION but it seems buggy and crashed the playground editor lol.

export const EQUATION: TextMatchTransformer = {
  dependencies: [EquationNode],
  export: (node) => {
    if (!$isEquationNode(node)) {
      return null;
    }

    const equation = node.getEquation();
    const isInline = node.isInline();
    return isInline ? `$${equation}$` : `$$${equation}$$`;
  },
  importRegExp: /\$([^$]+?)\$|\$\$([^$]+?)\$\$/,
  regExp: /\$([^$]+?)\$$|\$\$([^$]+?)\$\$$/,
  replace: (textNode, match) => {
    const [, inlineEquation, blockEquation] = match;
    const equation = inlineEquation || blockEquation;
    const isInline = !!inlineEquation;
    const equationNode = $createEquationNode(equation, isInline);
    textNode.replace(equationNode);
  },
  trigger: '$',
  type: 'text-match',
};

vantage-ola avatar Dec 12 '24 16:12 vantage-ola

I agree that a single transformer could probably handle both cases, but it needs to be implemented correctly. I would recommend writing up some unit tests, it's probably an issue with your regexes (it usually is, that syntax is tricky).

etrepum avatar Dec 14 '24 20:12 etrepum

@etrepum I hope following code resolves your issue

export const EQUATION: TextMatchTransformer = {
  dependencies: [EquationNode],
  export: (node) => {
    if (!$isEquationNode(node)) {
      return null;
    }
    const equation = node.getEquation().trim();
    const isInline = node.isInline();
    
    // For block equations, ensure proper spacing
    if (!isInline) {
      return `\n\$\$\n${equation}\n\$\$\n`;
    }
    
    // For inline equations, keep them in the same line
    return `\$\{equation\}\$`;
  },
  // Modified regex to better handle both inline and block equations
  importRegExp: /\$\$([^$]+?)\$\$|\$([^$\n]+?)\$/,
  regExp: /(\$\$([^$]+?)\$\$|\$([^$\n]+?)\$)$/,
  replace: (textNode, match) => {
    const fullMatch = match[0];
    const blockEq = match[1];
    const inlineEq = match[2];
    
    // If the equation has $$ it's a block equation
    const isBlock = fullMatch.startsWith('$$') && fullMatch.endsWith('$$');
    const equation = isBlock ? blockEq : inlineEq;

    if (!equation) {
      return;
    }

    const equationNode = $createEquationNode(equation.trim(), !isBlock);
    textNode.replace(equationNode);
  },
  trigger: '$',
  type: 'text-match',
};

nmzabith avatar Feb 18 '25 07:02 nmzabith

If you’d like to have that included in lexical you should submit a PR

etrepum avatar Feb 18 '25 14:02 etrepum

Sure, i'll make a PR

nmzabith avatar Feb 18 '25 14:02 nmzabith