Skip to content

Commit

Permalink
fix(list): handle order and unorder list bulleting with start index
Browse files Browse the repository at this point in the history
  • Loading branch information
JeelGajera committed Dec 28, 2024
1 parent 824fc16 commit 518aa94
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 37 deletions.
Binary file modified examples/test-pdf-gen/markdown_rendering_example.pdf
Binary file not shown.
60 changes: 30 additions & 30 deletions examples/test-pdf-gen/src/md-text.ts
Original file line number Diff line number Diff line change
@@ -1,43 +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
// 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.
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
## 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.
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
## Section 2: Lists and Examples
// This section showcases how to create simple and nested lists.
This section showcases how to create simple and nested lists.
// ### Simple List
### Simple List
// - Item 1
// - Item 2
// - Item 3
- Item 1
- Item 2
- Item 3
// ### Nested List
### 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
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
### 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
- 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
// `;
`;
6 changes: 6 additions & 0 deletions src/renderer/MdTextRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const MdTextRender = async (
element: ParsedElement,
indentLevel: number = 0,
hasRawBullet: boolean = false,
start: number = 0,
ordered: boolean = false,
) => {
const indent = indentLevel * options.page.indent;
if (
Expand Down Expand Up @@ -76,6 +78,8 @@ export const MdTextRender = async (
indentLevel,
options,
renderElement,
start,
ordered,
);
break;
case MdTokenType.Raw:
Expand All @@ -89,6 +93,8 @@ export const MdTextRender = async (
hasRawBullet,
options,
renderElement,
start,
ordered,
);
break;
default:
Expand Down
13 changes: 11 additions & 2 deletions src/renderer/components/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,22 @@ const renderList = (
element: ParsedElement,
indentLevel: number,
hasRawBullet?: boolean,
start?: number,
ordered?: boolean,
) => number,
): number => {
doc.setFontSize(options.page.defaultFontSize);
// doc.setFont(options.font.light.name, options.font.light.style);
for (const point of element?.items ?? []) {
for (const [i, point] of element?.items?.entries() ?? []) {
const _start = element.ordered ? (element.start ?? 0) + i : element.start;
y =
parentElementRenderer(point, indentLevel + 1, true) +
parentElementRenderer(
point,
indentLevel + 1,
true,
_start,
element.ordered,
) +
getCharHight(doc, options) * 0.2; // Recursively render nested list items
}
return y;
Expand Down
15 changes: 13 additions & 2 deletions src/renderer/components/listItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ const renderListItem = (
element: ParsedElement,
indentLevel: number,
hasRawBullet?: boolean,
start?: number,
ordered?: boolean,
) => number,
start: number,
ordered: boolean,
): number => {
const indent = indentLevel * options.page.indent;
const bullet = ordered ? `${start}. ` : '\u2022 ';
if (
y +
doc.splitTextToSize(
Expand All @@ -41,7 +46,7 @@ const renderListItem = (
y =
justifyText(
doc,
'\u2022 ' + element.content,
bullet + element.content,
x + indent,
y,
options.page.maxContentWidth - indent,
Expand All @@ -52,7 +57,13 @@ const renderListItem = (
if (element.items && element.items.length > 0) {
for (const subItem of element.items) {
y =
parentElementRenderer(subItem, indentLevel + 1, true) +
parentElementRenderer(
subItem,
indentLevel + 1,
true,
start,
ordered,
) +
getCharHight(doc, options) * 0.2;
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/components/rawItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ const renderRawItem = (
element: ParsedElement,
indentLevel: number,
hasRawBullet?: boolean,
start?: number,
ordered?: boolean,
) => number,
start: number,
ordered: boolean,
): number => {
if (element?.items && element?.items.length > 0) {
for (const item of element?.items ?? []) {
y = parentElementRenderer(item, indentLevel, hasRawBullet);
y = parentElementRenderer(item, indentLevel, hasRawBullet, start, ordered);
}
} else {
const indent = indentLevel * options.page.indent;
const bullet = hasRawBullet ? '\u2022 ' : ''; // unicode for bullet point
const bullet = hasRawBullet ? (ordered ? `${start}. ` : '\u2022 ') : ''; // unicode for bullet point
const lines = doc.splitTextToSize(
bullet + element.content,
options.page.maxContentWidth - indent,
Expand Down
2 changes: 1 addition & 1 deletion src/types/parsedElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type ParsedElement = {
depth?: number;
items?: ParsedElement[];
ordered?: boolean;
start?: number | string;
start?: number;
lang?: string;
code?: string;
src?: string;
Expand Down

0 comments on commit 518aa94

Please sign in to comment.