Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ RUN dnf install -y \
google-noto-sans-cjk-ttc-fonts \
&& fc-cache -fv

# Copy the LaTeX template
COPY ./src/template.latex template.latex
# Copy the LaTeX templates
COPY ./src/templates ./templates

# Copy built files from the previous stage
COPY --from=builder /app/dist/* ./
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,37 @@ body:
]
```

### Request fields

| Field | Required | Description |
|---|---|---|
| `userId` | Yes | Used to namespace the generated file in S3 |
| `fileName` | Yes | Output file name, without extension |
| `typeOfFile` | Yes | `"PDF"` or `"TEX"` |
| `markdown` | Yes | Source document body |
| `implicitFigures` | No | Enables Pandoc's `implicit_figures` extension |
| `variables` | No | Map of Pandoc template variables, passed as `--variable=key:value` |
| `template` | No | `"default"` or `"cjk"` (see below). Defaults to `"default"` |

### Templates

Two LaTeX templates are available via the `template` field:

- `"default"` (default) — the standard document template, sans-serif `lmodern` font.
- `"cjk"` — adds `xeCJK` support for Chinese/Japanese/Korean typesetting, with `Noto Sans` / `Noto Sans CJK KR` as the default fonts. Use this when the document contains CJK text. `mainfont`/`CJKmainfont` can still be overridden via `variables`.

```json
[
{
"userId":"c82da7d4-3295-4c4a-921b-7000d65224b6",
"fileName": "korean_doc",
"typeOfFile":"PDF",
"template":"cjk",
"markdown":"# 수학 문서\n\n이차 방정식의 판별식은 $\\Delta = b^2 - 4ac$ 입니다."
}
]
```


## Testing

Expand Down
37 changes: 37 additions & 0 deletions index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ describe("schema", () => {
const data = [{ userId: "u1", fileName: "doc", typeOfFile: "PDF", markdown: "text", variables: { lang: 42 } }];
expect(schema.safeParse(data).success).toBe(false);
});

it("validates a request with template \"default\"", () => {
const data = [{ userId: "u1", fileName: "doc", typeOfFile: "PDF", markdown: "text", template: "default" }];
expect(schema.safeParse(data).success).toBe(true);
});

it("validates a request with template \"cjk\"", () => {
const data = [{ userId: "u1", fileName: "doc", typeOfFile: "PDF", markdown: "text", template: "cjk" }];
expect(schema.safeParse(data).success).toBe(true);
});

it("rejects an unknown template value", () => {
const data = [{ userId: "u1", fileName: "doc", typeOfFile: "PDF", markdown: "text", template: "korean" }];
expect(schema.safeParse(data).success).toBe(false);
});
});

describe("handler", () => {
Expand Down Expand Up @@ -117,6 +132,28 @@ describe("handler", () => {
expect(calledArgs).toContain("--variable=CJKmainfont:Noto Sans CJK KR");
});

it("uses the default template when template is omitted", async () => {
const executeMock = vi.fn().mockResolvedValue("");
vi.mocked(PdcTs).mockImplementationOnce(() => ({ Execute: executeMock }) as any);

const event = [{ userId: "user1", fileName: "test-doc", typeOfFile: "TEX", markdown: "# Hello" }];
await handler(event as any);

const calledArgs: string[] = executeMock.mock.calls[0]?.[0]?.pandocArgs ?? [];
expect(calledArgs).toContain("--template=./templates/default.latex");
});

it("uses the cjk template when template is \"cjk\"", async () => {
const executeMock = vi.fn().mockResolvedValue("");
vi.mocked(PdcTs).mockImplementationOnce(() => ({ Execute: executeMock }) as any);

const event = [{ userId: "user1", fileName: "test-doc", typeOfFile: "TEX", markdown: "# Hello", template: "cjk" }];
await handler(event as any);

const calledArgs: string[] = executeMock.mock.calls[0]?.[0]?.pandocArgs ?? [];
expect(calledArgs).toContain("--template=./templates/cjk.latex");
});

it("returns 500 when Pandoc execution fails", async () => {
vi.mocked(PdcTs).mockImplementationOnce(() => ({
Execute: vi.fn()
Expand Down
12 changes: 10 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { deleteFile, errorRefiner } from "./src/utils";
import { z } from "zod";

const TypeOfFileSchema = z.enum(["PDF", "TEX"]);
const TemplateSchema = z.enum(["default", "cjk"]);

const templatePaths: Record<z.infer<typeof TemplateSchema>, string> = {
default: "./templates/default.latex",
cjk: "./templates/cjk.latex",
};

export const schema = z.array(
z.object({
Expand All @@ -15,6 +21,7 @@ export const schema = z.array(
markdown: z.string(),
implicitFigures: z.boolean().optional(),
variables: z.record(z.string(), z.string()).optional(),
template: TemplateSchema.optional(),
})
);

Expand Down Expand Up @@ -150,13 +157,14 @@ export const handler = async function (
const implicitFigures = eachRequestData.implicitFigures;
const variableArgs = Object.entries(eachRequestData.variables ?? {})
.map(([k, v]) => `--variable=${k}:${v}`);
const templatePath = templatePaths[eachRequestData.template ?? "default"];

switch (eachRequestData.typeOfFile) {
case "PDF":
const filenamePDF = `${eachRequestData.fileName}.pdf`;
const localPathPDF = `/tmp/${filenamePDF}`;
const generatePDFResult = await generateFile(
["--pdf-engine=xelatex", `--template=./template.latex`, ...variableArgs],
["--pdf-engine=xelatex", `--template=${templatePath}`, ...variableArgs],
localPathPDF,
markdown,
implicitFigures
Expand All @@ -173,7 +181,7 @@ export const handler = async function (
const filenameTEX = `${eachRequestData.fileName}.tex`;
const localPathTEX = `/tmp/${filenameTEX}`;
await generateFile(
[`--template=./template.latex`, ...variableArgs],
[`--template=${templatePath}`, ...variableArgs],
localPathTEX,
markdown,
implicitFigures
Expand Down
27 changes: 23 additions & 4 deletions src/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@ import { PdcTs } from "pdc-ts";
// These run the full production pipeline: markdown → Pandoc + template.latex → xelatex → PDF.
// They are intentionally slow (~5-15s per compile).

// Absolute path so Pandoc can locate the template regardless of its working directory
const TEMPLATE = path.resolve(__dirname, "template.latex");
// Absolute paths so Pandoc can locate the templates regardless of its working directory
const DEFAULT_TEMPLATE = path.resolve(__dirname, "templates/default.latex");
const CJK_TEMPLATE = path.resolve(__dirname, "templates/cjk.latex");

const pendingPdfs: string[] = [];

const compileToPdf = async (markdown: string, id: string) => {
const compileToPdf = async (markdown: string, id: string, template: string = DEFAULT_TEMPLATE) => {
const tmpPath = `/tmp/compile-test-${id}.pdf`;
pendingPdfs.push(tmpPath);
await new PdcTs().Execute({
from: "markdown",
to: "latex",
pandocArgs: ["--pdf-engine=xelatex", `--template=${TEMPLATE}`],
pandocArgs: ["--pdf-engine=xelatex", `--template=${template}`],
spawnOpts: { argv0: "+RTS -M512M -RTS" },
outputToFile: true,
sourceText: markdown,
Expand Down Expand Up @@ -84,4 +85,22 @@ describe("PDF compile (end-to-end pipeline)", () => {
},
{ timeout: 60_000 }
);

it(
"renders Korean text via the cjk template",
async () => {
const pdf = await compileToPdf(
"# 수학 문서\n\n이차 방정식의 판별식은 $\\Delta = b^2 - 4ac$ 입니다.",
"korean",
CJK_TEMPLATE
);
const text = extractText(pdf);
// pdftotext collapses inter-word spacing between Hangul syllable blocks,
// so match the individual words rather than the spaced phrase
expect(text).toContain("수학");
expect(text).toContain("문서");
expect(text).toContain("판별식");
},
{ timeout: 60_000 }
);
});
File renamed without changes.
Loading
Loading