+ {t("workspaces.portability", "Import, reuse & export")}
+
+
+
{t("workspaces.privateWriting", "Private snippets and templates")}
+ {library.data?.map((item) => (
+
+ {item.kind} · {item.title}
+
+ {item.kind === "template" && }
+
+
+ ))}
+
+
+
+
+
{
+ const file = event.target.files?.[0];
+ if (!file) return;
+ void run(async () => {
+ const imported = await previewWorkspaceImport(file);
+ setPreview({
+ ...imported,
+ name: file.name,
+ provenance: { fileName: file.name, fileSize: file.size, importedAt: new Date().toISOString() },
+ sourceKind: file.name.toLowerCase().endsWith(".docx") ? "docx" : "markdown",
+ });
+ });
+ }} />
+ {preview && (
+
+
{preview.name}
+ {preview.warnings.map((warning) =>
{warning}
)}
+
+
+
+ )}
+
+
+
+
{t("workspaces.exportDocument", "Export this document")}
+
+
+
+
+
{t("workspaces.packetArtifacts", "Practice feedback to include")}
+ {practice.data?.artifacts.filter((item) => item.status === "complete").map((artifact) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
{t("workspaces.googleAccounts", "Google Drive accounts")}
+ {connections.data?.length ? (
+ <>
+
+
+
+
+
+
+ >
+ ) :
{t("workspaces.noGoogleAccounts", "Connect a Google account to import or export Docs without changing your DebateLab login.")}
}
+
+
+ {error && {error}
}
+
+ );
+}
diff --git a/src/lib/workspace/document-conversion.ts b/src/lib/workspace/document-conversion.ts
index cd838d4f8..4735fd4d8 100644
--- a/src/lib/workspace/document-conversion.ts
+++ b/src/lib/workspace/document-conversion.ts
@@ -47,12 +47,13 @@ function inlineMarkdown(content: JSONContent[] = []) {
function serializeList(node: JSONContent, depth = 0): string {
const lines: string[] = [];
- for (const item of node.content ?? []) {
+ for (const [index, item] of (node.content ?? []).entries()) {
const blocks = item.content ?? [];
const paragraph = blocks.find((block) => block.type === "paragraph");
- lines.push(`${" ".repeat(depth)}- ${inlineMarkdown(paragraph?.content)}`);
+ const marker = node.type === "orderedList" ? `${index + Number(node.attrs?.start ?? 1)}.` : "-";
+ lines.push(`${" ".repeat(depth)}${marker} ${inlineMarkdown(paragraph?.content)}`);
for (const child of blocks) {
- if (child.type === "bulletList") lines.push(serializeList(child, depth + 1));
+ if (child.type === "bulletList" || child.type === "orderedList") lines.push(serializeList(child, depth + 1));
}
}
return lines.join("\n");
@@ -79,7 +80,7 @@ export function toWorkspaceMarkdown(document: JSONContent): string {
return `${"#".repeat(Number(node.attrs?.level) || 1)} ${inlineMarkdown(node.content)}`;
}
if (node.type === "paragraph") return inlineMarkdown(node.content);
- if (node.type === "bulletList") return serializeList(node);
+ if (node.type === "bulletList" || node.type === "orderedList") return serializeList(node);
if (node.type === "table") return serializeTable(node);
return "";
}).filter(Boolean).join("\n\n");
@@ -111,17 +112,19 @@ function parseCitation(href: string): JSONContent | null {
function parseMarkedText(text: string, extraMarks: JSONContent["marks"] = []): JSONContent[] {
const nodes: JSONContent[] = [];
- const pattern = /(\*\*([^*]+)\*\*|\*([^*]+)\*|~~([^~]+)~~)/g;
+ const pattern = /(\*\*\*([^*]+)\*\*\*|\*\*([^*]+)\*\*|\*([^*]+)\*|~~([^~]+)~~)/g;
let start = 0;
for (const match of text.matchAll(pattern)) {
if (match.index > start) {
nodes.push({ type: "text", text: unescapeMarkdown(text.slice(start, match.index)), marks: extraMarks });
}
- const mark = match[2] ? "bold" : match[3] ? "italic" : "strike";
+ const marks = match[2]
+ ? [{ type: "bold" }, { type: "italic" }]
+ : [{ type: match[3] ? "bold" : match[4] ? "italic" : "strike" }];
nodes.push({
type: "text",
- text: unescapeMarkdown(match[2] ?? match[3] ?? match[4]),
- marks: [...extraMarks, { type: mark }],
+ text: unescapeMarkdown(match[2] ?? match[3] ?? match[4] ?? match[5]),
+ marks: [...extraMarks, ...marks],
});
start = match.index + match[0].length;
}
@@ -190,9 +193,12 @@ function parseTable(lines: string[], start: number) {
function parseList(lines: string[], start: number, indent: number) {
const items: JSONContent[] = [];
+ const first = /^(\s*)(?:(\d+)\.|[-+*])\s+(.*)$/.exec(lines[start]);
+ const ordered = Boolean(first?.[2]);
+ const order = Number(first?.[2] ?? 1);
let index = start;
while (index < lines.length) {
- const match = /^(\s*)-\s+(.*)$/.exec(lines[index]);
+ const match = /^(\s*)(?:(\d+)\.|[-+*])\s+(.*)$/.exec(lines[index]);
if (!match || match[1].length < indent) break;
if (match[1].length > indent) {
const nested = parseList(lines, index, match[1].length);
@@ -202,11 +208,14 @@ function parseList(lines: string[], start: number, indent: number) {
}
items.push({
type: "listItem",
- content: [{ type: "paragraph", content: parseInline(match[2]) }],
+ content: [{ type: "paragraph", content: parseInline(match[3]) }],
});
index += 1;
}
- return { node: { type: "bulletList", content: items } satisfies JSONContent, index };
+ return {
+ node: { type: ordered ? "orderedList" : "bulletList", ...(ordered ? { attrs: { start: order } } : {}), content: items } satisfies JSONContent,
+ index,
+ };
}
export function fromWorkspaceMarkdown(markdown: string): JSONContent {
@@ -229,7 +238,7 @@ export function fromWorkspaceMarkdown(markdown: string): JSONContent {
index += 1;
continue;
}
- if (/^\s*-\s+/.test(line)) {
+ if (/^\s*(?:(?:\d+)\.|[-+*])\s+/.test(line)) {
const list = parseList(lines, index, /^\s*/.exec(line)?.[0].length ?? 0);
content.push(list.node);
index = list.index;
@@ -283,10 +292,12 @@ function docxList(node: JSONContent, docx: typeof import("docx"), depth = 0): im
const paragraph = blocks.find((block) => block.type === "paragraph");
paragraphs.push(new docx.Paragraph({
children: docxInline(paragraph?.content, docx),
- bullet: { level: depth },
+ ...(node.type === "orderedList"
+ ? { numbering: { reference: "workspace-numbering", level: depth } }
+ : { bullet: { level: depth } }),
}));
for (const child of blocks) {
- if (child.type === "bulletList") paragraphs.push(...docxList(child, docx, depth + 1));
+ if (child.type === "bulletList" || child.type === "orderedList") paragraphs.push(...docxList(child, docx, depth + 1));
}
}
return paragraphs;
@@ -305,10 +316,8 @@ function docxTable(node: JSONContent, docx: typeof import("docx")) {
});
}
-export async function toWorkspaceDocx(document: JSONContent): Promise