diegomura / react-pdf

đź“„ Create PDF files using React
https://react-pdf.org
MIT License
14.65k stars 1.16k forks source link

Render custom HTML markup inside pdf document #888

Open abollam opened 4 years ago

abollam commented 4 years ago

Hi all, I am trying to render HTML inside the <View>/<Text> component while rendering the PDF inside the <PDFViewer> using a Template.

I'd like to render my custom HTML code shown below in my pdf

<p><span style="font-size: 24px;">Pre Interview Notes</span></p><p><br></p><p><strong style="font-size: 14px;"><u>This is a test Pre Interview Notes:</u></strong></p><p><br></p><p><em style="font-size: 14px;">The Guest requires a wheel chair for the show</em></p><p><br></p><p><br></p>

Environment

tomgallagher commented 4 years ago

I'm also doing exactly this.

The way I'm doing it is to convert the html into json structure, then parse the json structure into @react-pdf elements. So you have a routine for that converts <p> elements into paragraph Views and then places children <span> elements as nested Text children etc.

That could be a problem if you're not sure what the incoming HTML is going to be - you'll have to do a complete parser for every html element.

alvnrapada commented 4 years ago

Hi all, I am trying to render HTML inside the <View>/<Text> component while rendering the PDF inside the <PDFViewer> using a Template.

I'd like to render my custom HTML code shown below in my pdf

<p><span style="font-size: 24px;">Pre Interview Notes</span></p><p><br></p><p><strong style="font-size: 14px;"><u>This is a test Pre Interview Notes:</u></strong></p><p><br></p><p><em style="font-size: 14px;">The Guest requires a wheel chair for the show</em></p><p><br></p><p><br></p>

Environment

  • Browser [e.g. Chrome ]:
  • React-PDF version [1.6.8]:
  • React version [e.g. 16.8.6]:

Same here, Im trying to render my data coming from CKEditor. Any Updates?

stevenbrown-85 commented 4 years ago

@tomgallagher don't suppose you have a basic example of how you're doing this? I need to render html from tinymce

tomgallagher commented 4 years ago

@stevenbrown-85 it's a bit more involved than a basic example but I can give you some pointers.

HTML and the mirror react-pdf code are both tree structures, so you need two different functions to walk/save the tree and then rebuild it. You're looking at recursion to do this.

As you are using tinyMCE, you're in luck as you can limit the html structures that can be in your tree, you don't need to do every single HTML element. One thing to note is that tinyMCE onEditorChange event just provides a list of HTML elements, so you need to provide a 'wrapper' parent element for the below to work. I use a document fragment.

Look at this function: dom-to-json, which is where I started. The tree structure is preserved by using a childNodes array of sub-nodes. So you start with a document fragment with all the tinyMCE elements as childNodes.

Then you will need to write your own json-to-pdf function. Mine has an entry point that looks like this

export const JsonToPdfComponent = (input, bodyTextColorString) => {

    /*
        the object was created with following values 
        { 
            nodeType: <integer>, 
            nodeTagName: <string>, 
            nodeName: <string>, 
            nodeValue: <string>, 
            childNodes: <array> 
            attributes: <array of attributes arrays>
        }
    */

    //first we have an error handler which will result in blank elements in the pdf rather than crashes
    if (input === undefined) { console.error("JsonToPdfComponent: Undefined JSON Input"); return null; }
    //create the object, either parsing a JSON string or just using an existing javascript object
    let json = typeof input === 'string' ? JSON.parse(input) : input;
    //define the node type
    let nodeType = json.nodeType;
    //define the tag type
    let tagType = json.nodeTagName;
    //then the construction process is different depending on the type of node
    switch (nodeType) {
        //MOST OF THE WORK DONE ON ELEMENT_NODES
        case 1: 
            //then we need to create styled component views for each tag
            switch(tagType) {
                case 'h1':
                   //SEE EXAMPLE BELOW
                    return createH1ViewElement(json, bodyTextColorString);
                case 'h2':
                    return createH2ViewElement(json, bodyTextColorString);
                case 'h3':
                    return createH3ViewElement(json, bodyTextColorString);
                case 'h4':
                    return createH4ViewElement(json, bodyTextColorString);
                case 'h5':
                    return createH5ViewElement(json, bodyTextColorString);
                case 'h6':
                    return createH6ViewElement(json, bodyTextColorString);
                case 'strong':
                    return createStrongTextElement(json, bodyTextColorString);
                case 'em':
                    return createEmphasisTextElement(json, bodyTextColorString);
                case 'p':
                    return createParagraphTextElement(json, bodyTextColorString);
                case 'span':
                    return createSpanTextElement(json, bodyTextColorString);
                //we add a link tag only when the anchor tag has children, i.e. text
                case 'a':
                    return createLinkElement(json, bodyTextColorString);
                case 'img':
                    return createImageElement(json);
                //special processing for ordered and unordered list components
                case 'ol':
                    return createListComponent(json, bodyTextColorString, tagType);
                case 'ul':
                    return createListComponent(json, bodyTextColorString, tagType);
                //special processing for tables
                case 'table':
                    return createTableComponent(json, bodyTextColorString);
                //special processing for sup text
                case 'sup':
                    return createSupComponent(json, bodyTextColorString);
                default:
                    console.log(`No Processing for Tag ${tagType.toUpperCase()}`);
            }
            break;
        //TEXT_NODE - we can just create the simple text item
        case 3: 
            //this will return a null value if the text filtered for formatting characters has a length of zero
            return createTextComponent(json.nodeValue);
        default: 
            console.log("Skipping Node", json);
    }

};

Note that I do some intermediate processing, so there's a crucial bit missing from this code, which is how to process the parent fragment element. You would do this by case 11, where you would, for example, create the react-pdf Page element using a routine like the following code sample and then all the childNodes accordingly.

To rebuild, you use React.createElement for each node, so for a simple h1 example:

import { View, Text } from '@react-pdf/renderer';

const createH1ViewElement = (node, bodyTextColorString) => {

    return React.createElement(
        //every html element should have its own view
        View,
        //add the special style props to the view, if you have any, from the JSON attributes array
        {
            //stops headers appearing at the bottom of the page
            minPresenceAhead: 1,
        },
        //then we pass in the children of the json object, as children of the React element
        React.createElement(
            Text,
            //add the special style props to the text element
            {
                //add the custom styles you have set for the pdf component
                style: {...dynamicStyles.styledHeader1, color: bodyTextColorString},
            },
            //then we pass in the children of the json object = recursion or the end of recursion
            node.childNodes.map(child => JsonToPdfComponent(child, bodyTextColorString))
        )
    );

};

That should get you started with basic text. Lists and tables are a bit tougher to get right.

Styles need to be converted from CSS to something understood by react-pdf. TinyMCE does most of the styling with span elements.

If you have all the default fonts from tinyMCE, they will need to be loaded from somewhere. Images are also a pain as they have CORS issues.

Hope that helps a bit.

There's a big project potentially for react-pdf that converts any HTML string to pdf.

Tom

stevenbrown-85 commented 4 years ago

Thanks @tomgallagher - very helpful, much appreciated. I'll give this a bash over next few days and see how i get on. I have disabled fonts and images for tinymce so they wont be a problem - dealing with tables will be interesting though!

tomgallagher commented 4 years ago

@stevenbrown-85, no problem and good luck! The lists and tables are a bit annoying but the worst bit was inline <sup> elements. There's no good way to do those in @react-pdf, whatever you might read in related issues.

iamacoderguy commented 3 years ago

For later readers, I think that parsing HTML string is the hardest part and react-html-parser can help.

It will parse the string to ReactElement[] so that we can map the ReactElement to React-PDF elements.

DubstepKnight commented 3 years ago

For later readers, I think that parsing HTML string is the hardest part and react-html-parser can help.

It will parse the string to ReactElement[] so that we can map the ReactElement to React-PDF elements.

@iamacoderguy Hello, I do not understand how to map ReactElement to ReactPdfElements and I could not find anything on it. Could you please elaborate on that?

ArnasDickus commented 3 years ago

Here's my simple method. Html Render page

const parsedHtml = htmlParser(taskDescription);
return (
<View>
      <Text>Description:</Text>
          {parsedHtml}
  </View>
)

html-parser:


import ReactHtmlParser from 'react-html-parser';
import { Text } from '@react-pdf/renderer';
import React from 'react';

export const htmlParser = (taskDescription: string | null) => {
  let returnContentConst;
  if (taskDescription) {
    const parsedHtml = ReactHtmlParser(taskDescription);
    parsedHtml.forEach(element => {
      const type = element.type;

      element.props.children.forEach((content: string) => {
        switch (type) {
          case 'p':
            returnContentConst = (<Text>{content}</Text>)
            break;
          default:
            returnContentConst = (<Text>{content}</Text>)
            break;
        }
      })
    })
    return returnContentConst;
  } else {
    return returnContentConst;
  }
}
DubstepKnight commented 3 years ago

@ArnasDickus Thanks! I appreciate it, Will try it once we start refactoring the code and adding more features.

danomatic commented 3 years ago

Using node-html-parser, I assembled a some helper functions and an <Html> tag here: https://github.com/danomatic/react-pdf-html

Feedback would be welcome! I'm hoping it can become a standardized part of the react-pdf library at some point. Ideally we can make it extensible so that users can implement custom tag renderers.

hotcakedev628 commented 3 years ago

Here's my simple method. Html Render page

const parsedHtml = htmlParser(taskDescription);
return (
<View>
      <Text>Description:</Text>
          {parsedHtml}
  </View>
)

html-parser:


import ReactHtmlParser from 'react-html-parser';
import { Text } from '@react-pdf/renderer';
import React from 'react';

export const htmlParser = (taskDescription: string | null) => {
  let returnContentConst;
  if (taskDescription) {
    const parsedHtml = ReactHtmlParser(taskDescription);
    parsedHtml.forEach(element => {
      const type = element.type;

      element.props.children.forEach((content: string) => {
        switch (type) {
          case 'p':
            returnContentConst = (<Text>{content}</Text>)
            break;
          default:
            returnContentConst = (<Text>{content}</Text>)
            break;
        }
      })
    })
    return returnContentConst;
  } else {
    return returnContentConst;
  }
}

Hey @ArnasDickus Thanks for the posting, but I am getting an error Error: Invalid element of type div passed to PDF renderer. Do you know why I am getting the error?

brxshncy commented 3 years ago

any updates on this?

danomatic commented 3 years ago

@brxshncy

any updates on this?

I've continued working on https://github.com/danomatic/react-pdf-html. Please give it a try and let me know what you think!

brxshncy commented 3 years ago

@brxshncy

any updates on this?

I've continued working on https://github.com/danomatic/react-pdf-html. Please give it a try and let me know what you think!

Hello @danomatic , it says not available as an NPM package, how do I install this on an existing project? what specific files to download

danomatic commented 3 years ago

@brxshncy I have not bundled and published as an npm package in the event that the author of react-pdf was interested in including it. To use it at this time, just copy the files from src in the repo and make sure you have these dependencies:

danomatic commented 3 years ago

@diegomura are you interested in including https://github.com/danomatic/react-pdf-html as part of react-pdf so that it can be maintained together or would you prefer that the Html component be maintained separately?

thismax commented 3 years ago

Hi @danomatic, while we wait for @diegomura to consider including react-pdf-html as part of react-pdf, would it be possible for you to publish https://github.com/danomatic/react-pdf-html to npm? I have a use-case that would definitely benefit from npm availability. Thanks!

danomatic commented 3 years ago

@thismax and @brxshncy I just published it to NPM:

https://www.npmjs.com/package/react-pdf-html

thismax commented 3 years ago

@danomatic brilliant, thank you!

Luisparr14 commented 1 year ago

@danomatic You're Amazing!

geralddevelop commented 2 months ago

Edit from @danomatic, a version that handles nested elements and elements if yall need it.

import ReactHtmlParser from "react-html-parser";
import { Text } from "@react-pdf/renderer";
import React from "react";

interface ParsedElementProps {
  children: React.ReactNode;
  [key: string]: any;
}

export const htmlParser = (taskDescription: string | null): JSX.Element => {
  const parseElements = (elements: React.ReactNode): React.ReactNode[] => {
    const returnContentConst: React.ReactNode[] = [];

    React.Children.forEach(elements, (element) => {
      if (typeof element === "string") {
        // Handle string content
        returnContentConst.push(<Text key={Math.random()}>{element}</Text>);
      } else if (React.isValidElement(element)) {
        const elementProps = element.props as ParsedElementProps;
        const type = element.type;
        const children = parseElements(elementProps.children);

        switch (type) {
          case "p":
            returnContentConst.push(
              <Text key={Math.random()}>{children}</Text>
            );
            break;
          case "strong":
            returnContentConst.push(
              <Text
                key={Math.random()}
                style={{ fontFamily: "Helvetica-Bold", fontWeight: 600 }}
              >
                {children}
              </Text>
            );
            break;
          // Add more cases as needed for other HTML tags
          default:
            returnContentConst.push(
              <Text key={Math.random()}>{children}</Text>
            );
            break;
        }
      }
    });

    return returnContentConst;
  };

  if (taskDescription) {
    const parsedHtml = ReactHtmlParser(taskDescription);

    const returnContentConst = parseElements(parsedHtml);

    return <>{returnContentConst}</>;
  } else {
    return <></>;
  }
};

Hopes this help!

danomatic commented 1 month ago

@geralddevelop this is nice! I'm thinking of switching react-pdf-html to use react-html-parser for its parsing. I like it's transform callback option.

erickfabiandev commented 1 month ago

Para los lectores posteriores, creo que analizar cadenas HTML es la parte más difícil y react-html-parser puede ayudar. Analizará la cadena como ReactElement[] para que podamos asignar ReactElement a los elementos React-PDF.

@iamacoderguy Hola, no entiendo cómo mapear ReactElement a ReactPdfElements y no pude encontrar nada al respecto. ¿Podrías explicarme más sobre eso?

you can do it in the following way, The only solution for this was to convert the hmtl to a react component with html-to-react.

`const HtmlToReactParser = HtmlToReact.Parser; const htmlToReactParser = new HtmlToReactParser();

const HtmlText = ({ html }) => { const reactElement = htmlToReactParser.parse(html); const convertElement = (element) => { if (!element) return null;

const { type, props } = element;
const { children } = props;

switch (type) {
  case 'h1':
    return <Text style={styles.heading}>{children.map(convertElement)}</Text>;
  case 'h2':
    return <Text style={styles.subheading}>{children.map(convertElement)}</Text>;
  case 'p':
    return <Text style={styles.paragraph}>{children.map(convertElement)}</Text>;
  case 'b':
    return <Text style={styles.bold}>{children.map(convertElement)}</Text>;
  case 'i':
    return <Text style={styles.italic}>{children.map(convertElement)}</Text>;
  case 'u':
    return <Text style={styles.underline}>{children.map(convertElement)}</Text>;
  default:
    return <Text>{children.map(convertElement)}</Text>;
}

};

return convertElement(reactElement); };`