Skip to content
Merged
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
26 changes: 26 additions & 0 deletions app/api/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,20 @@ import {
type PdfRejectReason,
type PdfQualityStats,
} from "@/lib/pdf-extract";
import {
markdownToPdf,
MarkdownEmptyError,
MarkdownTooLargeError,
} from "@/lib/md-to-pdf";
import { ensureDocDir, pdfPath } from "@/lib/paths";
import { getDoc, newDocId, saveDoc } from "@/lib/store";

export const runtime = "nodejs";
export const maxDuration = 60;

/** Extensions we accept as Markdown and render to PDF before ingesting. */
const MARKDOWN_EXT = /\.(md|markdown|mdown|mkd|mdwn)$/i;

const SAMPLE_NAME_TO_DOC_ID: Record<string, string> = {
anatomy: "sample-anatomy",
physics: "sample-physics",
Expand Down Expand Up @@ -105,6 +113,24 @@ export async function POST(req: Request) {
buffer = Buffer.from(await file.arrayBuffer());
const fname = (file as unknown as { name?: string }).name;
if (fname) filename = fname.replace(/[^a-z0-9._-]/gi, "_");

// Markdown uploads: render to a clean, text-bearing PDF up front, then
// fall through to the exact same extract → gate → store pipeline as any
// other PDF. The %PDF- sanity check below then validates the rendered
// bytes (pdfkit emits a 1.7 header).
if (MARKDOWN_EXT.test(filename)) {
try {
buffer = await markdownToPdf(buffer.toString("utf-8"));
} catch (e) {
if (e instanceof MarkdownEmptyError || e instanceof MarkdownTooLargeError) {
return NextResponse.json({ error: e.message }, { status: 422 });
}
return NextResponse.json(
{ error: "This Markdown file couldn't be converted to a document." },
{ status: 422 },
);
}
}
}
} else {
return NextResponse.json({ error: "expected multipart/form-data" }, { status: 400 });
Expand Down
2 changes: 1 addition & 1 deletion app/library/library-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const FILENAME_TO_TITLE: Record<string, string> = {
};

function titleOf(filename: string): string {
return FILENAME_TO_TITLE[filename] ?? filename.replace(/\.pdf$/i, "");
return FILENAME_TO_TITLE[filename] ?? filename.replace(/\.(pdf|md|markdown|mdown|mkd|mdwn)$/i, "");
}

function humaniseAgo(ts: number): string {
Expand Down
2 changes: 1 addition & 1 deletion app/viewer/[docId]/viewer-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export default function ViewerClient({ docId }: { docId: string }) {

const docTitle = useMemo(
() =>
meta && (FILENAME_TO_TITLE[meta.filename] || meta.filename.replace(/\.pdf$/i, "")),
meta && (FILENAME_TO_TITLE[meta.filename] || meta.filename.replace(/\.(pdf|md|markdown|mdown|mkd|mdwn)$/i, "")),
[meta],
);

Expand Down
16 changes: 10 additions & 6 deletions components/UploadCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ const FILENAME_TO_TITLE: Record<string, string> = {
"chemistry.pdf": "Organic Chemistry",
};

/** A PDF, or a Markdown file we render to PDF on upload. Kept in sync with
* MARKDOWN_EXT in app/api/upload/route.ts. */
const ACCEPTED_FILE = /\.(pdf|md|markdown|mdown|mkd|mdwn)$/i;

function humaniseAgo(ts: number): string {
const dt = Date.now() - ts;
if (dt < 5_000) return "just now";
Expand Down Expand Up @@ -137,8 +141,8 @@ export default function UploadCard() {
const startUpload = useCallback(
async (file: File) => {
setError(null);
if (!file.name.toLowerCase().endsWith(".pdf")) {
setError("Please pick a PDF file");
if (!ACCEPTED_FILE.test(file.name)) {
setError("Please pick a PDF or Markdown (.md) file");
return;
}
setBusy("upload");
Expand Down Expand Up @@ -203,7 +207,7 @@ export default function UploadCard() {
<input
ref={inputRef}
type="file"
Comment on lines 207 to 209

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 accept attribute is missing three of the five accepted extensions

ACCEPTED_FILE (and the server-side MARKDOWN_EXT) accept .mdown, .mkd, and .mdwn, but the accept string only lists .md and .markdown. Files with those three extensions will be hidden in the OS file picker unless the user chooses "All Files". They do pass client-side validation when picked by other means, so this is purely a discoverability gap rather than a blocking error.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

accept="application/pdf,.pdf"
accept="application/pdf,.pdf,text/markdown,.md,.markdown"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
Expand Down Expand Up @@ -237,7 +241,7 @@ export default function UploadCard() {
</>
) : (
<>
<span>Drop your PDF here, or</span>
<span>Drop your PDF or Markdown here, or</span>
<span className="inline-flex items-center gap-1.5 rounded-md bg-[var(--accent-600)] px-3 py-1 text-[12.5px] font-semibold text-white shadow-sm transition hover:bg-[var(--accent-700)]">
<Upload className="h-3.5 w-3.5" />
Select the file
Expand All @@ -246,7 +250,7 @@ export default function UploadCard() {
)}
</p>
<p className="mt-3 text-[11.5px] text-[var(--ink-400)]">
Text-tagged PDFs work best. No OCR.
Text-based PDFs and Markdown (.md) work best. No OCR.
</p>
</div>

Expand Down Expand Up @@ -301,7 +305,7 @@ export default function UploadCard() {
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
{libraryPreview.map((d) => {
const title =
FILENAME_TO_TITLE[d.filename] ?? d.filename.replace(/\.pdf$/i, "");
FILENAME_TO_TITLE[d.filename] ?? d.filename.replace(/\.(pdf|md|markdown|mdown|mkd|mdwn)$/i, "");
return (
<Link
key={d.id}
Expand Down
2 changes: 1 addition & 1 deletion lib/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function docTitleFromFilename(filename: string): string {
"calculus.pdf": "Differential & Integral Calculus",
"chemistry.pdf": "Organic Chemistry",
};
return FILENAME_TO_TITLE[filename] ?? filename.replace(/\.pdf$/i, "");
return FILENAME_TO_TITLE[filename] ?? filename.replace(/\.(pdf|md|markdown|mdown|mkd|mdwn)$/i, "");
}

function mergeTagsFile(
Expand Down
Loading