Skip to content

Commit

Permalink
Merge pull request #3 from JeelGajera/update-parsing-rendering
Browse files Browse the repository at this point in the history
fix(parser): update md parser handlers to manage nested elements
fix(rendering): support nested rendering for heading, paragraph & raw item components
  • Loading branch information
JeelGajera authored Dec 20, 2024
2 parents 71bb510 + f428b13 commit 6b2ee3b
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 130 deletions.
Binary file modified examples/test-pdf-gen/markdown_rendering_example.pdf
Binary file not shown.
43 changes: 43 additions & 0 deletions examples/test-pdf-gen/src/md-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// export const mdString = "# jsPDF Markdown Renderer\r\n\r\nA jsPDF utility to render Markdown directly into formatted PDFs with custom designs.\r\n\r\n## Table of Contents\r\n\r\n- [Installation](#installation)\r\n- [Usage](#usage)\r\n- [API](#api)\r\n- [Examples](#examples)\r\n- [Contributing](#contributing)\r\n- [License](#license)\r\n\r\n## Installation\r\n\r\nTo install the library, you can use npm:\r\n\r\n```sh\r\nnpm install jspdf-md-renderer\r\n```\r\n\r\n## Usage\r\n\r\n### Basic Example\r\n\r\nHere is a basic example of how to use the library to generate a PDF from Markdown content:\r\n\r\n```ts\r\nimport { jsPDF } from 'jspdf';\r\nimport { MdTextRender } from 'jspdf-md-renderer';\r\n\r\nconst mdString = `\r\n# Main Title\r\n\r\nThis is a brief introduction paragraph. It sets the tone for the document and introduces the main topic in a concise manner.\r\n\r\n## Section 1: Overview\r\n\r\nHere is a medium-length paragraph that goes into more detail about the first section. It explains the context, provides background information, and sets up the discussion for the subsections.\r\n\r\n## Section 2: Lists and Examples\r\n\r\nThis section showcases how to create simple and nested lists.\r\n\r\n### Simple List\r\n\r\n- Item 1\r\n- Item 2\r\n- Item 3\r\n\r\n### Nested List\r\n\r\n1. First Level 1\r\n - First Level 2\r\n - First Level 3\r\n2. Second Level 1\r\n - Second Level 2\r\n - Another Second Level 2\r\n - Nested deeper\r\n\r\n### Mixed List Example\r\n\r\n- Topic 1\r\n 1. Subtopic 1.1\r\n 2. Subtopic 1.2\r\n- Topic 2\r\n - Subtopic 2.1\r\n - Subtopic 2.2\r\n 1. Nested Subtopic 2.2.1\r\n 2. Nested Subtopic 2.2.2\r\n\r\n`;\r\n\r\nconst generatePDF = async () => {\r\n const doc = new jsPDF({\r\n unit: 'mm',\r\n format: 'a4',\r\n orientation: 'portrait',\r\n });\r\n\r\n const options = {\r\n cursor: { x: 10, y: 10 },\r\n page: {\r\n format: 'a4',\r\n unit: 'mm',\r\n orientation: 'portrait',\r\n maxContentWidth: 190,\r\n maxContentHeight: 277,\r\n lineSpace: 1.5,\r\n defaultLineHeightFactor: 1.2,\r\n defaultFontSize: 12,\r\n defaultTitleFontSize: 14,\r\n topmargin: 10,\r\n xpading: 10,\r\n xmargin: 10,\r\n indent: 10,\r\n },\r\n font: {\r\n bold: { name: 'helvetica', style: 'bold' },\r\n regular: { name: 'helvetica', style: 'normal' },\r\n light: { name: 'helvetica', style: 'light' },\r\n },\r\n endCursorYHandler: (y) => {\r\n console.log('End cursor Y position:', y);\r\n },\r\n };\r\n\r\n await MdTextRender(doc, mdString, options);\r\n doc.save('example.pdf');\r\n};\r\n\r\ngeneratePDF();\r\n```\r\n\r\n## API\r\n\r\n### `MdTextRender`\r\n\r\nRenders parsed markdown text into a jsPDF document.\r\n\r\n#### Parameters\r\n\r\n- `doc`: The jsPDF document instance.\r\n- `text`: The markdown content to render.\r\n- `options`: The render options (fonts, page margins, etc.).\r\n\r\n### `MdTextParser`\r\n\r\nParses markdown into tokens and converts to a custom parsed structure.\r\n\r\n#### Parameters\r\n\r\n- `text`: The markdown content to parse.\r\n\r\n#### Returns\r\n\r\n- `Promise<ParsedElement[]>`: Parsed markdown elements.\r\n\r\n\r\n## Supported Markdown Elements\r\n\r\nThe following Markdown elements are currently supported by `jspdf-md-renderer`:\r\n\r\n- **Headings**: `#`, `##`, `###`, etc.\r\n- **Paragraphs**\r\n- **Lists**:\r\n - Unordered lists: `-`, `*`, `+`\r\n - Ordered lists: `1.`, `2.`, `3.`, etc.\r\n\r\n\r\n## Examples\r\n\r\nYou can find more examples in the [examples](examples/test-pdf-gen) directory.\r\n\r\n## Contributing\r\n\r\nContributions are welcome! Please read the [contributing guidelines](CONTRIBUTING.md) first.\r\n\r\n## License\r\n\r\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\r\n"
export const mdString = "# Sample Markdown Document\n\n## Headings\n\n# Heading Level 1\n## Heading Level 2\n### Heading Level 3\n#### Heading Level 4\n##### Heading Level 5\n###### Heading Level 6\n\n---\n\n## Emphasis\n\n- *Italic* text using asterisks.\n- _Italic_ text using underscores.\n- **Bold** text using double asterisks.\n- __Bold__ text using double underscores.\n- ***Bold and Italic*** text using triple asterisks.\n\n---\n\n## Lists\n\n### Unordered List\n- Item 1\n - Subitem 1.1\n - Subitem 1.2\n- Item 2\n\n### Ordered List\n5. Item 1\n6. Item 2\n 7. Subitem 2.1\n 2. Subitem 2.2\n\n---\n\n## Links and Images\n\n[OpenAI](https://openai.com)\n\n![Sample Image](https://via.placeholder.com/150 \"Placeholder Image\")\n\n---\n\n## Code\n\n### Inline Code\nThis is `inline code`.\n\n### Code Block\n```javascript\nfunction greet(name) {\n return `Hello, ${name}!`;\n}\nconsole.log(greet('Markdown'));\n```"
// export const mdString = `
// # Main Title

// This is a brief introduction paragraph. It sets the tone for the document and introduces the main topic in a concise manner.

// ## Section 1: Overview

// Here is a medium-length paragraph that goes into more detail about the first section. It explains the context, provides background information, and sets up the discussion for the subsections.

// ## Section 2: Lists and Examples

// This section showcases how to create simple and nested lists.

// ### Simple List

// - Item 1
// - Item 2
// - Item 3

// ### Nested List

// 1. First Level 1
// - First Level 2
// - First Level 3
// 2. Second Level 1
// - Second Level 2
// - Another Second Level 2
// - Nested deeper

// ### Mixed List Example

// - Topic 1
// 1. Subtopic 1.1
// 2. Subtopic 1.2
// - Topic 2
// - Subtopic 2.1
// - Subtopic 2.2
// 1. Nested Subtopic 2.2.1
// 2. Nested Subtopic 2.2.2

// `;
54 changes: 1 addition & 53 deletions examples/test-pdf-gen/src/pdfGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,7 @@
import { RenderOption } from "jspdf-md-renderer/types";
import { MdTextRender } from "jspdf-md-renderer";
import jsPDF from "jspdf";

const mdString = `
# Main Title
This is a brief introduction paragraph. It sets the tone for the document and introduces the main topic in a concise manner.
## Section 1: Overview
Here is a medium-length paragraph that goes into more detail about the first section. It explains the context, provides background information, and sets up the discussion for the subsections.
### Subsection 1.1: Details
A longer paragraph with detailed explanations about the subsection. This paragraph is significantly longer to showcase how text can expand on key ideas, provide examples, and explain technical concepts in a way that engages the reader. It includes a lot of descriptive content to fill out the space and make the information comprehensive.
#### Sub-subsection 1.1.1: Specifics
An even shorter paragraph. Sometimes, brevity is key.
## Section 2: Lists and Examples
This section showcases how to create simple and nested lists.
### Simple List
- Item 1
- Item 2
- Item 3
### Nested List
1. First Level 1
- First Level 2
- First Level 3
2. Second Level 1
- Second Level 2
- Another Second Level 2
- Nested deeper
### Mixed List Example
- Topic 1
1. Subtopic 1.1
2. Subtopic 1.2
- Topic 2
- Subtopic 2.1
- Subtopic 2.2
1. Nested Subtopic 2.2.1
2. Nested Subtopic 2.2.2
## Section 3: Conclusion
Finally, we wrap up with a short paragraph that highlights the key takeaways and invites the reader to reflect on the content.
`
import { mdString } from "./md-text";

export const pdfGenerator = async () => {
const doc = new jsPDF({
Expand Down
2 changes: 2 additions & 0 deletions src/enums/mdTokenType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum MdTokenType {
ListItem = 'list_item',
Blockquote = 'blockquote',
Code = 'code',
CodeSpan = 'codespan',
Table = 'table',
Html = 'html',
Hr = 'hr',
Expand All @@ -15,4 +16,5 @@ export enum MdTokenType {
TableHeader = 'table_header',
TableCell = 'table_cell',
Raw = 'raw',
Text = 'text',
}
45 changes: 35 additions & 10 deletions src/parser/MdTextParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,21 @@ export const MdTextParser = async (text: string): Promise<ParsedElement[]> => {
const convertTokens = (tokens: TokensList): ParsedElement[] => {
const parsedElements: ParsedElement[] = [];
tokens.forEach((token) => {
const handler = tokenHandlers[token.type];
if (handler) {
parsedElements.push(handler(token));
} else {
parsedElements.push({ type: MdTokenType.Raw, content: token.raw });
try {
const handler = tokenHandlers[token.type];
if (handler) {
parsedElements.push(handler(token));
} else {
parsedElements.push({
type: MdTokenType.Raw,
content: token.raw,
});
}
} catch (error) {
console.error('Failed to handle token ==>', token, error);
}
});
return parsedElements.map((element) =>
element.type === MdTokenType.Raw && element.content === '\n\n'
? { ...element, content: element.content.replace('\n\n', '\n') }
: element,
);
return parsedElements;
};

/**
Expand All @@ -45,13 +48,17 @@ const tokenHandlers: Record<string, (token: any) => ParsedElement> = {
type: MdTokenType.Heading,
depth: token.depth,
content: token.text,
items: token.tokens ? convertTokens(token.tokens) : [],
}),
[MdTokenType.Paragraph]: (token) => ({
type: MdTokenType.Paragraph,
content: token.text,
items: token.tokens ? convertTokens(token.tokens) : [],
}),
[MdTokenType.List]: (token) => ({
type: MdTokenType.List,
ordered: token.ordered,
start: token.start,
items: token.items ? convertTokens(token.items) : [],
}),
[MdTokenType.ListItem]: (token) => ({
Expand Down Expand Up @@ -86,13 +93,31 @@ const tokenHandlers: Record<string, (token: any) => ParsedElement> = {
type: MdTokenType.Link,
href: token.href,
text: token.text,
items: token.tokens ? convertTokens(token.tokens) : [],
}),
[MdTokenType.Strong]: (token) => ({
type: MdTokenType.Strong,
content: token.text,
items: token.tokens ? convertTokens(token.tokens) : [],
}),
[MdTokenType.Em]: (token) => ({
type: MdTokenType.Em,
content: token.text,
items: token.tokens ? convertTokens(token.tokens) : [],
}),
[MdTokenType.Text]: (token) => ({
type: MdTokenType.Text,
content: token.text,
items: token.tokens ? convertTokens(token.tokens) : [],
}),
[MdTokenType.Hr]: (token) => ({
type: MdTokenType.Hr,
content: token.raw,
items: token.tokens ? convertTokens(token.tokens) : [],
}),
[MdTokenType.CodeSpan]: (token) => ({
type: MdTokenType.CodeSpan,
content: token.text,
items: token.tokens ? convertTokens(token.tokens) : [],
}),
};
13 changes: 7 additions & 6 deletions src/renderer/MdTextRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export const MdTextRender = async (
) => {
const parsedElements = await MdTextParser(text);
console.log(parsedElements);
console.log(doc);

let y = options.cursor.y;
const x = options.cursor.x;
Expand All @@ -53,10 +52,10 @@ export const MdTextRender = async (

switch (element.type) {
case MdTokenType.Heading:
y = renderHeading(doc, element, x, y, indent, options);
y = renderHeading(doc, element, x, y, indent, options, renderElement);
break;
case MdTokenType.Paragraph:
y = renderParagraph(doc, element, x, y, indent, options);
y = renderParagraph(doc, element, x, y, indent, options, renderElement);
break;
case MdTokenType.List:
y = renderList(
Expand All @@ -80,6 +79,7 @@ export const MdTextRender = async (
);
break;
case MdTokenType.Raw:
case MdTokenType.Text:
y = renderRawItem(
doc,
element,
Expand All @@ -88,14 +88,15 @@ export const MdTextRender = async (
indentLevel,
hasRawBullet,
options,
renderElement,
);
break;
default:
console.warn(
`Warning: Unsupported element type encountered: ${element.type}.
If you believe this element type should be supported, please create an issue at:
https://github.com/JeelGajera/jspdf-md-renderer/issues
with details of the element and expected behavior. Thank you for helping improve this library!`,
If you believe this element type should be supported, please create an issue at:
https://github.com/JeelGajera/jspdf-md-renderer/issues
with details of the element and expected behavior. Thank you for helping improve this library!`,
);
break;
}
Expand Down
22 changes: 17 additions & 5 deletions src/renderer/components/heading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,27 @@ const renderHeading = (
y: number,
indent: number,
options: RenderOption,
parentElementRenderer: (
element: ParsedElement,
indentLevel: number,
hasRawBullet?: boolean,
) => number,
): number => {
const size = 6 - (element?.depth ?? 0) > 0 ? 6 - (element?.depth ?? 0) : 0;
// doc.setFont(options.font.regular.name, options.font.regular.style);
doc.setFontSize(options.page.defaultFontSize + size);
doc.text(element?.content ?? '', x + indent, y, {
align: 'left',
maxWidth: options.page.maxContentWidth - indent,
});
y += 1.5 * getCharHight(doc, options);
if (element?.items && element?.items.length > 0) {
for (const item of element?.items ?? []) {
y = parentElementRenderer(item, indent, false);
}
} else {
doc.text(element?.content ?? '', x + indent, y, {
align: 'left',
maxWidth: options.page.maxContentWidth - indent,
});
y += 1.5 * getCharHight(doc, options);
}

// doc.setFont(options.font.light.name, options.font.light.style);
doc.setFontSize(options.page.defaultFontSize);
return y;
Expand Down
93 changes: 52 additions & 41 deletions src/renderer/components/paragraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,63 +15,74 @@ const renderParagraph = (
y: number,
indent: number,
options: RenderOption,
parentElementRenderer: (
element: ParsedElement,
indentLevel: number,
hasRawBullet?: boolean,
) => number,
): number => {
doc.setFontSize(options.page.defaultFontSize);
// doc.setFont(options.font.light.name, options.font.light.style);
let content = element.content;
const lineHeight =
doc.getTextDimensions('A').h * options.page.defaultLineHeightFactor;
if (
y +
doc.splitTextToSize(
if (element?.items && element?.items.length > 0) {
for (const item of element?.items ?? []) {
y = parentElementRenderer(item, indent, false);
}
} else {
if (
y +
doc.splitTextToSize(
content ?? '',
options.page.maxContentWidth - indent,
).length *
lineHeight -
3 * lineHeight >=
options.page.maxContentHeight
) {
// ADD Possible text to Page bottom
const contentLeft: string[] = doc.splitTextToSize(
content ?? '',
options.page.maxContentWidth - indent,
).length *
lineHeight -
3 * lineHeight >=
options.page.maxContentHeight
) {
// ADD Possible text to Page bottom
const contentLeft: string[] = doc.splitTextToSize(
content ?? '',
options.page.maxContentWidth - indent,
);
const possibleContentLines: string[] = [];
const possibleContentY = y;
for (let j = 0; j < contentLeft.length; j++) {
if (y - 2 * lineHeight < options.page.maxContentHeight) {
possibleContentLines.push(contentLeft[j]);
y += options.page.lineSpace;
} else {
// set left content to move next page
if (j <= contentLeft.length - 1) {
content = contentLeft.slice(j).join('');
);
const possibleContentLines: string[] = [];
const possibleContentY = y;
for (let j = 0; j < contentLeft.length; j++) {
if (y - 2 * lineHeight < options.page.maxContentHeight) {
possibleContentLines.push(contentLeft[j]);
y += options.page.lineSpace;
} else {
// set left content to move next page
if (j <= contentLeft.length - 1) {
content = contentLeft.slice(j).join('');
}
break;
}
break;
}
if (possibleContentLines.length > 0) {
y = justifyText(
doc,
possibleContentLines.join(' '),
x + indent,
possibleContentY,
options.page.maxContentWidth - indent,
options.page.defaultLineHeightFactor,
);
}
HandlePageBreaks(doc, options);
y = options.page.topmargin;
}
if (possibleContentLines.length > 0) {
y = justifyText(
y =
justifyText(
doc,
possibleContentLines.join(' '),
content ?? '',
x + indent,
possibleContentY,
y,
options.page.maxContentWidth - indent,
options.page.defaultLineHeightFactor,
);
}
HandlePageBreaks(doc, options);
y = options.page.topmargin;
) + getCharHight(doc, options);
}
y =
justifyText(
doc,
content ?? '',
x + indent,
y,
options.page.maxContentWidth - indent,
options.page.defaultLineHeightFactor,
) + getCharHight(doc, options);

return y;
};
Expand Down
Loading

0 comments on commit 6b2ee3b

Please sign in to comment.