diegomura / react-pdf

📄 Create PDF files using React
https://react-pdf.org
MIT License
14.28k stars 1.12k forks source link

Requesting Help: Rendering Markdown inside the PDF #2676

Open Leviathan91 opened 2 months ago

Leviathan91 commented 2 months ago

I am having a hard time supporting Markdown within the generated Document by react-pdf/renderer.

What I have so far: I am using react-markdown to parse Markdown and transform it into HTML tags. I then map these tags into their respective PDF components.

const markDownComponents = (extraStyle: any) => {
    return {
      h1: defaultComponent,
      h2: defaultComponent,
      h3: defaultComponent,
      h4: defaultComponent,
      h5: defaultComponent,
      p: ({ children }: { children: ReactNode }) => (
        <Text style={[styles.p, extraStyle]}>{children}</Text>
      ),
      i: defaultComponent,
      b: ({ children }: { children: ReactNode }) => (
        <Text style={[styles.b, extraStyle]}>{children}</Text>
      ),
      strong: ({ children }: { children: ReactNode }) => (
        <Text style={[styles.strong, extraStyle]}>{children}</Text>
      ),
      a: defaultComponent,
      ol: ({ children }: { children: ReactNode }) => {
        return renderList(children, 'ol');
      },

      ul: ({ children }: { children: ReactNode }) => {
        return renderList(children, 'ul');
      },

      li: ({ children }: { children: ReactNode }) => <Text>{'wont get rendered'}</Text>,
    };
  };
  type MDProps = {
    children: any;
    style?: React.CSSProperties;
  };

  const MD = ({ children, style }: MDProps) => {
    return (
      // @ts-ignore
      <Markdown components={markDownComponents(style)}>{children}</Markdown>
    );
  };

Two issues here: 1) I'd actually love for the user to see that some Markdown is not supported. So when he types some Markdown inside an input like # MyHeading, I want exactly that to be displayed to him in the pdf. So with the hashtag and everything. I couldn't quite get this to work (tried tokenizer plugin), so I settled for defaultComponents so far. Any working examples would be highly appreciated.

2) It is unbelievably hard to style list items, depending on whether they are an ordered or unordered List.

It works for lists which are not nested:

  function renderList(list, listType) {
    let result = [];
    let itemCounter = 0;
    for (let i = 0; i < list.length; i++) {
      if (list[i].props?.node?.tagName === 'li') {
        list[i].props.node.children.map((child) => {
          if (child.type === 'text' && child.value != '\n') {
            if (listType === 'ol') {
              result.push(
                <Text style={styles.listItem}>{`${++itemCounter}. ${child.value}`}</Text>
              );
            }
            if (listType === 'ul') {
              result.push(<Text style={styles.listItem}>{`• ${child.value}`}</Text>);
            }
          } else if (child.type === 'element') {
            result.push(renderList(child.children, child.tagName));
          }
        });
      }
    }
    return result;
  }

But as soon as these lists are nested, the structure of the children changes in a way that I dont quite get. props changes to properties, there are children inside children.

Closest I got for nested lists was this:

function renderList(list, listType) {
    console.log('renderList', list, 'listType', listType);
    let result = [];
    let itemCounter = 0;
    for (let i = 0; i < list.length; i++) {
      if (list[i].props?.node?.tagName === 'li') {
        list[i].props.node.children.map((child) => {
          if (child.type === 'text' && child.value != '\n') {
            if (listType === 'ol') {
              result.push(
                <Text style={styles.listItem}>{`${++itemCounter}. ${child.value}`}</Text>
              );
            }
            if (listType === 'ul') {
              result.push(<Text style={styles.listItem}>{`• ${child.value}`}</Text>);
            }
          } else if (child.type === 'element') {
            result.push(renderList(child.children, child.tagName));
          }
        });
      } else {
        if (list[i].type === 'element') {
          list[i].children?.map((child) => {
            if (child.tagName === 'p') {
              if (listType === 'ol') {
                result.push(
                  <Text style={styles.listItem}>{`${++itemCounter}. ${
                    child.children[0].value
                  }`}</Text>
                );
              }
              if (listType === 'ul') {
                result.push(<Text style={styles.listItem}>{`• ${child.children[0].value}`}</Text>);
              }
            }
          });
        }
      }
    }
    return result;
  }

But then I realized that some tags are completly ignored and parts of the list omitted. Any hints on how to tackle this issue are, again, highly appreciated.