@openpolicy/renderers 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # @openpolicy/renderers
2
+
3
+ ## 0.0.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 2372fdb: - Adds @openpolicy/react library.
8
+ - Adds PDF renderer
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@openpolicy/renderers",
3
+ "version": "0.0.14",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ },
8
+ "scripts": {
9
+ "check-types": "tsc --noEmit",
10
+ "test": "bun test"
11
+ },
12
+ "dependencies": {
13
+ "pdfkit": "^0.18.0"
14
+ },
15
+ "devDependencies": {
16
+ "@openpolicy/core": "workspace:*",
17
+ "@openpolicy/tooling": "workspace:*",
18
+ "@types/pdfkit": "^0.17.5"
19
+ }
20
+ }
@@ -0,0 +1,166 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { Document } from "@openpolicy/core";
3
+ import { renderHTML } from "./html";
4
+
5
+ function doc(sections: Document["sections"]): Document {
6
+ return { type: "document", policyType: "privacy", sections };
7
+ }
8
+
9
+ test("renders heading as <h2>", () => {
10
+ const result = renderHTML(
11
+ doc([
12
+ {
13
+ type: "section",
14
+ id: "s1",
15
+ content: [{ type: "heading", value: "Introduction" }],
16
+ },
17
+ ]),
18
+ );
19
+ expect(result).toContain("<h2>Introduction</h2>");
20
+ });
21
+
22
+ test("renders paragraph as <p>", () => {
23
+ const result = renderHTML(
24
+ doc([
25
+ {
26
+ type: "section",
27
+ id: "s1",
28
+ content: [
29
+ {
30
+ type: "paragraph",
31
+ children: [{ type: "text", value: "Hello world" }],
32
+ },
33
+ ],
34
+ },
35
+ ]),
36
+ );
37
+ expect(result).toContain("<p>Hello world</p>");
38
+ });
39
+
40
+ test("renders bold as <strong>", () => {
41
+ const result = renderHTML(
42
+ doc([
43
+ {
44
+ type: "section",
45
+ id: "s1",
46
+ content: [
47
+ {
48
+ type: "paragraph",
49
+ children: [{ type: "bold", value: "Important" }],
50
+ },
51
+ ],
52
+ },
53
+ ]),
54
+ );
55
+ expect(result).toContain("<strong>Important</strong>");
56
+ });
57
+
58
+ test("renders link as <a>", () => {
59
+ const result = renderHTML(
60
+ doc([
61
+ {
62
+ type: "section",
63
+ id: "s1",
64
+ content: [
65
+ {
66
+ type: "paragraph",
67
+ children: [
68
+ { type: "link", href: "https://example.com", value: "here" },
69
+ ],
70
+ },
71
+ ],
72
+ },
73
+ ]),
74
+ );
75
+ expect(result).toContain('<a href="https://example.com">here</a>');
76
+ });
77
+
78
+ test("renders heading with level 3 as <h3>", () => {
79
+ const result = renderHTML(
80
+ doc([
81
+ {
82
+ type: "section",
83
+ id: "s1",
84
+ content: [{ type: "heading", level: 3, value: "Sub-section" }],
85
+ },
86
+ ]),
87
+ );
88
+ expect(result).toContain("<h3>Sub-section</h3>");
89
+ });
90
+
91
+ test("renders ordered list as <ol>/<li>", () => {
92
+ const result = renderHTML(
93
+ doc([
94
+ {
95
+ type: "section",
96
+ id: "s1",
97
+ content: [
98
+ {
99
+ type: "list",
100
+ ordered: true,
101
+ items: [
102
+ {
103
+ type: "listItem",
104
+ children: [{ type: "text", value: "First" }],
105
+ },
106
+ {
107
+ type: "listItem",
108
+ children: [{ type: "text", value: "Second" }],
109
+ },
110
+ ],
111
+ },
112
+ ],
113
+ },
114
+ ]),
115
+ );
116
+ expect(result).toContain("<ol>");
117
+ expect(result).toContain("<li>First</li>");
118
+ expect(result).toContain("<li>Second</li>");
119
+ });
120
+
121
+ test("renders italic as <em>", () => {
122
+ const result = renderHTML(
123
+ doc([
124
+ {
125
+ type: "section",
126
+ id: "s1",
127
+ content: [
128
+ {
129
+ type: "paragraph",
130
+ children: [{ type: "italic", value: "emphasis" }],
131
+ },
132
+ ],
133
+ },
134
+ ]),
135
+ );
136
+ expect(result).toContain("<em>emphasis</em>");
137
+ });
138
+
139
+ test("renders list as <ul>/<li>", () => {
140
+ const result = renderHTML(
141
+ doc([
142
+ {
143
+ type: "section",
144
+ id: "s1",
145
+ content: [
146
+ {
147
+ type: "list",
148
+ items: [
149
+ {
150
+ type: "listItem",
151
+ children: [{ type: "text", value: "Alpha" }],
152
+ },
153
+ {
154
+ type: "listItem",
155
+ children: [{ type: "text", value: "Beta" }],
156
+ },
157
+ ],
158
+ },
159
+ ],
160
+ },
161
+ ]),
162
+ );
163
+ expect(result).toContain("<ul>");
164
+ expect(result).toContain("<li>Alpha</li>");
165
+ expect(result).toContain("<li>Beta</li>");
166
+ });
package/src/html.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type {
2
+ Document,
3
+ InlineNode,
4
+ ListItemNode,
5
+ ListNode,
6
+ } from "@openpolicy/core";
7
+
8
+ function escapeHtml(str: string): string {
9
+ return str
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;");
14
+ }
15
+
16
+ function renderInline(node: InlineNode): string {
17
+ switch (node.type) {
18
+ case "text":
19
+ return escapeHtml(node.value);
20
+ case "bold":
21
+ return `<strong>${escapeHtml(node.value)}</strong>`;
22
+ case "italic":
23
+ return `<em>${escapeHtml(node.value)}</em>`;
24
+ case "link":
25
+ return `<a href="${escapeHtml(node.href)}">${escapeHtml(node.value)}</a>`;
26
+ }
27
+ }
28
+
29
+ function renderListItem(item: ListItemNode): string {
30
+ const content = item.children
31
+ .map((child) =>
32
+ child.type === "list"
33
+ ? renderList(child)
34
+ : renderInline(child as InlineNode),
35
+ )
36
+ .join("");
37
+ return `<li>${content}</li>`;
38
+ }
39
+
40
+ function renderList(node: ListNode): string {
41
+ const tag = node.ordered ? "ol" : "ul";
42
+ const items = node.items.map(renderListItem).join("");
43
+ return `<${tag}>${items}</${tag}>`;
44
+ }
45
+
46
+ export function renderHTML(doc: Document): string {
47
+ return doc.sections
48
+ .map((section) => {
49
+ // biome-ignore lint/suspicious/useIterableCallbackReturn: typed
50
+ const blocks = section.content.map((node) => {
51
+ switch (node.type) {
52
+ case "heading": {
53
+ const level = node.level ?? 2;
54
+ return `<h${level}>${escapeHtml(node.value)}</h${level}>`;
55
+ }
56
+ case "paragraph":
57
+ return `<p>${node.children.map(renderInline).join("")}</p>`;
58
+ case "list":
59
+ return renderList(node);
60
+ }
61
+ });
62
+ return blocks.join("\n");
63
+ })
64
+ .join("\n");
65
+ }
@@ -0,0 +1,28 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { PolicyInput } from "@openpolicy/core";
3
+ import { compilePolicy } from "./index";
4
+
5
+ const input: PolicyInput = {
6
+ type: "privacy",
7
+ effectiveDate: "2026-01-01",
8
+ company: {
9
+ name: "Acme Inc.",
10
+ legalName: "Acme Corporation",
11
+ address: "123 Main St, Springfield, USA",
12
+ contact: "privacy@acme.com",
13
+ },
14
+ dataCollected: { "Account Information": ["Name", "Email"] },
15
+ legalBasis: "Legitimate interests",
16
+ retention: { "Account data": "Until deletion" },
17
+ cookies: { essential: true, analytics: false, marketing: false },
18
+ thirdParties: [],
19
+ userRights: ["access"],
20
+ jurisdictions: ["us"],
21
+ };
22
+
23
+ test("compilePolicy routes privacy input to markdown", async () => {
24
+ const results = await compilePolicy(input);
25
+ expect(results).toBeArray();
26
+ expect(results[0]?.format).toBe("markdown");
27
+ expect(results[0]?.content).toContain("Acme Inc.");
28
+ });
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ import type {
2
+ CompileOptions,
3
+ OutputFormat,
4
+ PolicyInput,
5
+ } from "@openpolicy/core";
6
+ import { compile } from "@openpolicy/core";
7
+
8
+ export { renderHTML } from "./html";
9
+ export { renderMarkdown } from "./markdown";
10
+ export { renderPDF } from "./pdf";
11
+
12
+ function filenameFor(type: PolicyInput["type"], ext: string): string {
13
+ switch (type) {
14
+ case "privacy":
15
+ return `privacy-policy.${ext}`;
16
+ case "terms":
17
+ return `terms-of-service.${ext}`;
18
+ case "cookie":
19
+ return `cookie-policy.${ext}`;
20
+ }
21
+ }
22
+
23
+ export async function compilePolicy(
24
+ input: PolicyInput,
25
+ options?: CompileOptions,
26
+ ): Promise<
27
+ { format: OutputFormat; filename: string; content: string | Buffer }[]
28
+ > {
29
+ const doc = compile(input);
30
+ const formats = options?.formats ?? ["markdown"];
31
+ return Promise.all(
32
+ formats.map(async (format) => {
33
+ switch (format) {
34
+ case "markdown": {
35
+ const { renderMarkdown } = await import("./markdown");
36
+ return {
37
+ format,
38
+ filename: filenameFor(input.type, "md"),
39
+ content: renderMarkdown(doc),
40
+ };
41
+ }
42
+ case "html": {
43
+ const { renderHTML } = await import("./html");
44
+ return {
45
+ format,
46
+ filename: filenameFor(input.type, "html"),
47
+ content: renderHTML(doc),
48
+ };
49
+ }
50
+ case "pdf": {
51
+ const { renderPDF } = await import("./pdf");
52
+ return {
53
+ format,
54
+ filename: filenameFor(input.type, "pdf"),
55
+ content: await renderPDF(doc),
56
+ };
57
+ }
58
+ default:
59
+ throw new Error(`Format not yet implemented: ${format}`);
60
+ }
61
+ }),
62
+ );
63
+ }
@@ -0,0 +1,230 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { Document } from "@openpolicy/core";
3
+ import { renderMarkdown } from "./markdown";
4
+
5
+ function doc(sections: Document["sections"]): Document {
6
+ return { type: "document", policyType: "privacy", sections };
7
+ }
8
+
9
+ test("renders a heading node", () => {
10
+ const result = renderMarkdown(
11
+ doc([
12
+ {
13
+ type: "section",
14
+ id: "s1",
15
+ content: [{ type: "heading", value: "Introduction" }],
16
+ },
17
+ ]),
18
+ );
19
+ expect(result).toBe("## Introduction");
20
+ });
21
+
22
+ test("renders a paragraph with text", () => {
23
+ const result = renderMarkdown(
24
+ doc([
25
+ {
26
+ type: "section",
27
+ id: "s1",
28
+ content: [
29
+ {
30
+ type: "paragraph",
31
+ children: [{ type: "text", value: "Hello world" }],
32
+ },
33
+ ],
34
+ },
35
+ ]),
36
+ );
37
+ expect(result).toBe("Hello world");
38
+ });
39
+
40
+ test("renders bold and link inline nodes", () => {
41
+ const result = renderMarkdown(
42
+ doc([
43
+ {
44
+ type: "section",
45
+ id: "s1",
46
+ content: [
47
+ {
48
+ type: "paragraph",
49
+ children: [
50
+ { type: "bold", value: "Important" },
51
+ { type: "text", value: ": see " },
52
+ { type: "link", href: "https://example.com", value: "here" },
53
+ ],
54
+ },
55
+ ],
56
+ },
57
+ ]),
58
+ );
59
+ expect(result).toBe("**Important**: see [here](https://example.com)");
60
+ });
61
+
62
+ test("renders a list with items", () => {
63
+ const result = renderMarkdown(
64
+ doc([
65
+ {
66
+ type: "section",
67
+ id: "s1",
68
+ content: [
69
+ {
70
+ type: "list",
71
+ items: [
72
+ {
73
+ type: "listItem",
74
+ children: [{ type: "text", value: "Alpha" }],
75
+ },
76
+ {
77
+ type: "listItem",
78
+ children: [{ type: "text", value: "Beta" }],
79
+ },
80
+ ],
81
+ },
82
+ ],
83
+ },
84
+ ]),
85
+ );
86
+ expect(result).toBe("- Alpha\n- Beta");
87
+ });
88
+
89
+ test("renders nested list with indented items", () => {
90
+ const result = renderMarkdown(
91
+ doc([
92
+ {
93
+ type: "section",
94
+ id: "s1",
95
+ content: [
96
+ {
97
+ type: "list",
98
+ items: [
99
+ {
100
+ type: "listItem",
101
+ children: [
102
+ { type: "text", value: "Parent" },
103
+ {
104
+ type: "list",
105
+ items: [
106
+ {
107
+ type: "listItem",
108
+ children: [{ type: "text", value: "Child" }],
109
+ },
110
+ ],
111
+ },
112
+ ],
113
+ },
114
+ ],
115
+ },
116
+ ],
117
+ },
118
+ ]),
119
+ );
120
+ expect(result).toBe("- Parent\n - Child");
121
+ });
122
+
123
+ test("joins multiple sections with dividers", () => {
124
+ const result = renderMarkdown(
125
+ doc([
126
+ {
127
+ type: "section",
128
+ id: "s1",
129
+ content: [{ type: "heading", value: "One" }],
130
+ },
131
+ {
132
+ type: "section",
133
+ id: "s2",
134
+ content: [{ type: "heading", value: "Two" }],
135
+ },
136
+ ]),
137
+ );
138
+ expect(result).toBe("## One\n\n---\n\n## Two");
139
+ });
140
+
141
+ test("renders heading with level 3", () => {
142
+ const result = renderMarkdown(
143
+ doc([
144
+ {
145
+ type: "section",
146
+ id: "s1",
147
+ content: [{ type: "heading", level: 3, value: "Sub-section" }],
148
+ },
149
+ ]),
150
+ );
151
+ expect(result).toBe("### Sub-section");
152
+ });
153
+
154
+ test("renders heading with level 1", () => {
155
+ const result = renderMarkdown(
156
+ doc([
157
+ {
158
+ type: "section",
159
+ id: "s1",
160
+ content: [{ type: "heading", level: 1, value: "Title" }],
161
+ },
162
+ ]),
163
+ );
164
+ expect(result).toBe("# Title");
165
+ });
166
+
167
+ test("renders ordered list with numbered items", () => {
168
+ const result = renderMarkdown(
169
+ doc([
170
+ {
171
+ type: "section",
172
+ id: "s1",
173
+ content: [
174
+ {
175
+ type: "list",
176
+ ordered: true,
177
+ items: [
178
+ {
179
+ type: "listItem",
180
+ children: [{ type: "text", value: "First" }],
181
+ },
182
+ {
183
+ type: "listItem",
184
+ children: [{ type: "text", value: "Second" }],
185
+ },
186
+ ],
187
+ },
188
+ ],
189
+ },
190
+ ]),
191
+ );
192
+ expect(result).toBe("1. First\n2. Second");
193
+ });
194
+
195
+ test("renders italic inline node", () => {
196
+ const result = renderMarkdown(
197
+ doc([
198
+ {
199
+ type: "section",
200
+ id: "s1",
201
+ content: [
202
+ {
203
+ type: "paragraph",
204
+ children: [{ type: "italic", value: "emphasis" }],
205
+ },
206
+ ],
207
+ },
208
+ ]),
209
+ );
210
+ expect(result).toBe("_emphasis_");
211
+ });
212
+
213
+ test("joins multiple content nodes within a section with blank lines", () => {
214
+ const result = renderMarkdown(
215
+ doc([
216
+ {
217
+ type: "section",
218
+ id: "s1",
219
+ content: [
220
+ { type: "heading", value: "Title" },
221
+ {
222
+ type: "paragraph",
223
+ children: [{ type: "text", value: "Body text." }],
224
+ },
225
+ ],
226
+ },
227
+ ]),
228
+ );
229
+ expect(result).toBe("## Title\n\nBody text.");
230
+ });
@@ -0,0 +1,70 @@
1
+ import type {
2
+ Document,
3
+ InlineNode,
4
+ ListItemNode,
5
+ ListNode,
6
+ } from "@openpolicy/core";
7
+
8
+ function renderInline(node: InlineNode): string {
9
+ switch (node.type) {
10
+ case "text":
11
+ return node.value;
12
+ case "bold":
13
+ return `**${node.value}**`;
14
+ case "italic":
15
+ return `_${node.value}_`;
16
+ case "link":
17
+ return `[${node.value}](${node.href})`;
18
+ }
19
+ }
20
+
21
+ function renderListItem(
22
+ item: ListItemNode,
23
+ indent = "",
24
+ ordered = false,
25
+ index = 0,
26
+ ): string {
27
+ const parts: string[] = [];
28
+ let nestedList: ListNode | null = null;
29
+ for (const child of item.children) {
30
+ if (child.type === "list") {
31
+ nestedList = child;
32
+ } else {
33
+ parts.push(renderInline(child));
34
+ }
35
+ }
36
+ const bullet = ordered ? `${index + 1}.` : "-";
37
+ const line = `${indent}${bullet} ${parts.join("")}`;
38
+ if (nestedList) {
39
+ const nested = nestedList.items
40
+ .map((i, idx) =>
41
+ renderListItem(i, `${indent} `, nestedList!.ordered, idx),
42
+ )
43
+ .join("\n");
44
+ return `${line}\n${nested}`;
45
+ }
46
+ return line;
47
+ }
48
+
49
+ export function renderMarkdown(doc: Document): string {
50
+ return doc.sections
51
+ .map((section) => {
52
+ // biome-ignore lint/suspicious/useIterableCallbackReturn: typed
53
+ const blocks = section.content.map((node) => {
54
+ switch (node.type) {
55
+ case "heading": {
56
+ const hashes = "#".repeat(node.level ?? 2);
57
+ return `${hashes} ${node.value}`;
58
+ }
59
+ case "paragraph":
60
+ return node.children.map(renderInline).join("");
61
+ case "list":
62
+ return node.items
63
+ .map((item, idx) => renderListItem(item, "", node.ordered, idx))
64
+ .join("\n");
65
+ }
66
+ });
67
+ return blocks.join("\n\n");
68
+ })
69
+ .join("\n\n---\n\n");
70
+ }
@@ -0,0 +1,34 @@
1
+ import { expect, test } from "bun:test";
2
+ import { compile } from "@openpolicy/core";
3
+ import { renderPDF } from "./pdf";
4
+
5
+ const input = {
6
+ type: "privacy" as const,
7
+ effectiveDate: "2026-01-01",
8
+ company: {
9
+ name: "Acme Inc.",
10
+ legalName: "Acme Corporation",
11
+ address: "123 Main St",
12
+ contact: "privacy@acme.com",
13
+ },
14
+ dataCollected: { "Account Information": ["Name", "Email"] },
15
+ legalBasis: "Legitimate interests",
16
+ retention: { "Account data": "Until deletion" },
17
+ cookies: { essential: true, analytics: false, marketing: false },
18
+ thirdParties: [],
19
+ userRights: ["access" as const],
20
+ jurisdictions: ["us" as const],
21
+ };
22
+
23
+ test("renderPDF returns a Buffer", async () => {
24
+ const doc = compile(input);
25
+ const result = await renderPDF(doc);
26
+ expect(result).toBeInstanceOf(Buffer);
27
+ expect(result.length).toBeGreaterThan(100);
28
+ });
29
+
30
+ test("renderPDF output begins with PDF magic bytes", async () => {
31
+ const doc = compile(input);
32
+ const result = await renderPDF(doc);
33
+ expect(result.slice(0, 5).toString("ascii")).toBe("%PDF-");
34
+ });
package/src/pdf.ts ADDED
@@ -0,0 +1,158 @@
1
+ import type {
2
+ Document,
3
+ DocumentSection,
4
+ InlineNode,
5
+ ListItemNode,
6
+ ListNode,
7
+ } from "@openpolicy/core";
8
+ import PDFDocument from "pdfkit";
9
+
10
+ const FONT_REGULAR = "Helvetica";
11
+ const FONT_BOLD = "Helvetica-Bold";
12
+ const FONT_ITALIC = "Helvetica-Oblique";
13
+ const SIZE_HEADING_BASE = 16;
14
+ const SIZE_BODY = 11;
15
+ const COLOR_BODY = "#374151";
16
+ const COLOR_HEADING = "#111827";
17
+ const COLOR_LINK = "#2563eb";
18
+
19
+ function headingSize(level: number): number {
20
+ // H1=16, H2=14, H3=13, H4=12, H5=11, H6=10
21
+ return Math.max(10, SIZE_HEADING_BASE - (level - 1) * 1.5);
22
+ }
23
+
24
+ function renderInlineNodes(
25
+ doc: InstanceType<typeof PDFDocument>,
26
+ nodes: InlineNode[],
27
+ ): void {
28
+ for (let i = 0; i < nodes.length; i++) {
29
+ const node = nodes[i];
30
+ const continued = i < nodes.length - 1;
31
+ switch (node!.type) {
32
+ case "text":
33
+ doc
34
+ .font(FONT_REGULAR)
35
+ .fillColor(COLOR_BODY)
36
+ .text(node!.value, { continued });
37
+ break;
38
+ case "bold":
39
+ doc
40
+ .font(FONT_BOLD)
41
+ .fillColor(COLOR_HEADING)
42
+ .text(node!.value, { continued });
43
+ break;
44
+ case "italic":
45
+ doc
46
+ .font(FONT_ITALIC)
47
+ .fillColor(COLOR_BODY)
48
+ .text(node!.value, { continued });
49
+ break;
50
+ case "link":
51
+ doc
52
+ .font(FONT_REGULAR)
53
+ .fillColor(COLOR_LINK)
54
+ .text(node!.value, { continued, link: node!.href, underline: true });
55
+ doc.fillColor(COLOR_BODY);
56
+ break;
57
+ }
58
+ }
59
+ }
60
+
61
+ function renderListItem(
62
+ doc: InstanceType<typeof PDFDocument>,
63
+ item: ListItemNode,
64
+ depth: number,
65
+ ordered: boolean,
66
+ index: number,
67
+ ): void {
68
+ const indent = doc.page.margins.left + 10 + depth * 15;
69
+ const bullet = ordered ? `${index + 1}.` : depth === 0 ? "•" : "◦";
70
+
71
+ const inlineNodes = item.children.filter(
72
+ (c): c is InlineNode => c.type !== "list",
73
+ );
74
+ const nested =
75
+ item.children.find((c): c is ListNode => c.type === "list") ?? null;
76
+
77
+ doc
78
+ .font(FONT_REGULAR)
79
+ .fontSize(SIZE_BODY)
80
+ .fillColor(COLOR_BODY)
81
+ .text(`${bullet} `, { continued: true, indent });
82
+
83
+ if (inlineNodes.length > 0) {
84
+ renderInlineNodes(doc, inlineNodes);
85
+ } else {
86
+ doc.text("");
87
+ }
88
+
89
+ if (nested) {
90
+ for (let i = 0; i < nested.items.length; i++) {
91
+ renderListItem(
92
+ doc,
93
+ nested.items[i]!,
94
+ depth + 1,
95
+ nested.ordered ?? false,
96
+ i,
97
+ );
98
+ }
99
+ }
100
+ }
101
+
102
+ function renderSection(
103
+ doc: InstanceType<typeof PDFDocument>,
104
+ section: DocumentSection,
105
+ isFirst: boolean,
106
+ ): void {
107
+ if (!isFirst) {
108
+ doc.moveDown(0.5);
109
+ doc
110
+ .moveTo(doc.page.margins.left, doc.y)
111
+ .lineTo(doc.page.width - doc.page.margins.right, doc.y)
112
+ .strokeColor("#e5e7eb")
113
+ .lineWidth(0.5)
114
+ .stroke();
115
+ doc.moveDown(0.5);
116
+ }
117
+
118
+ for (const node of section.content) {
119
+ switch (node.type) {
120
+ case "heading":
121
+ doc
122
+ .font(FONT_BOLD)
123
+ .fontSize(headingSize(node.level ?? 2))
124
+ .fillColor(COLOR_HEADING)
125
+ .text(node.value)
126
+ .moveDown(0.3);
127
+ break;
128
+ case "paragraph":
129
+ doc.font(FONT_REGULAR).fontSize(SIZE_BODY);
130
+ renderInlineNodes(doc, node.children);
131
+ doc.moveDown(0.3);
132
+ break;
133
+ case "list":
134
+ doc.fontSize(SIZE_BODY);
135
+ for (let i = 0; i < node.items.length; i++) {
136
+ renderListItem(doc, node.items[i]!, 0, node.ordered ?? false, i);
137
+ }
138
+ doc.moveDown(0.3);
139
+ break;
140
+ }
141
+ }
142
+ }
143
+
144
+ export function renderPDF(document: Document): Promise<Buffer> {
145
+ return new Promise<Buffer>((resolve, reject) => {
146
+ const pdf = new PDFDocument({ margin: 50, size: "A4" });
147
+ const chunks: Buffer[] = [];
148
+ pdf.on("data", (chunk: Buffer) => chunks.push(chunk));
149
+ pdf.on("end", () => resolve(Buffer.concat(chunks)));
150
+ pdf.on("error", reject);
151
+
152
+ for (let i = 0; i < document.sections.length; i++) {
153
+ renderSection(pdf, document.sections[i]!, i === 0);
154
+ }
155
+
156
+ pdf.end();
157
+ });
158
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@openpolicy/tooling/base",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"]
8
+ }