Skip to content
This repository was archived by the owner on Jan 16, 2026. It is now read-only.

Commit fae19fb

Browse files
committed
Copy as markdown
1 parent bc02da4 commit fae19fb

3 files changed

Lines changed: 203 additions & 2 deletions

File tree

src/models/worksheet.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { OutcomeFinder } from "@utils/outcome-finder";
2+
13
export enum WorksheetBlockType {
24
Question = "question",
35
Paragraph = "paragraph",
@@ -100,4 +102,179 @@ export class Worksheet implements IWorksheet {
100102
.filter(b => b.currentType === WorksheetBlockType.Question)
101103
.reduce((sum, b) => sum + (b.points ?? 0), 0);
102104
}
105+
106+
/**
107+
* Returns a full worksheet export as Markdown.
108+
* (LaTeX is preserved as-is inside markdown strings.)
109+
*/
110+
async copyAsMarkdown(): Promise<string> {
111+
const lines: string[] = [];
112+
113+
const pushIf = (label: string, value: string | undefined) => {
114+
const v = (value ?? "").trim();
115+
if (v) lines.push(`- **${label}:** ${v}`);
116+
};
117+
118+
const ensureBlankLine = () => {
119+
if (lines.length === 0) return;
120+
if (lines[lines.length - 1]?.trim() !== "") lines.push("");
121+
};
122+
123+
const md = (s?: string) => (s ?? "").trim();
124+
const escapeForHeading = (s?: string) => (s ?? "").trim();
125+
126+
// ---------- Header / Metadata ----------
127+
const title = escapeForHeading(this.name) || "Worksheet";
128+
lines.push(`# ${title}`);
129+
ensureBlankLine();
130+
131+
// Metadata list
132+
pushIf("Topic", this.topic);
133+
pushIf("Grade", this.gradeLevel);
134+
pushIf("Author", this.author);
135+
pushIf("Date", this.date);
136+
137+
const totalPoints = this.getTotalPoints();
138+
if (totalPoints > 0) lines.push(`- **Total Points:** ${totalPoints}`);
139+
140+
ensureBlankLine();
141+
142+
// Outcomes
143+
if ((this.curricularOutcomes?.length ?? 0) > 0) {
144+
lines.push(`## Curricular Outcomes`);
145+
ensureBlankLine();
146+
for (const outcomeId of this.curricularOutcomes) {
147+
const outcome = await OutcomeFinder.getById(outcomeId);
148+
if (outcome) lines.push(outcome.toString());
149+
}
150+
ensureBlankLine();
151+
}
152+
153+
// Teacher notes
154+
if (md(this.teacherNotes)) {
155+
lines.push(`## Teacher Notes`);
156+
ensureBlankLine();
157+
lines.push(md(this.teacherNotes));
158+
ensureBlankLine();
159+
}
160+
161+
// ---------- Blocks ----------
162+
if ((this.blocks?.length ?? 0) > 0) {
163+
lines.push(`---`);
164+
ensureBlankLine();
165+
166+
for (const block of this.blocks) {
167+
const b = this.blockToMarkdown(block);
168+
169+
if (b) {
170+
lines.push(b.trimEnd());
171+
ensureBlankLine();
172+
}
173+
}
174+
}
175+
176+
// Trim trailing blank lines
177+
// while (lines.length > 1 && lines[lines.length - 1].trim() === "") {
178+
// lines.pop();
179+
// }
180+
181+
return lines.join("\n");
182+
}
183+
184+
private blockToMarkdown(
185+
block: WorksheetBlock
186+
): string {
187+
188+
const md = (s?: string) => (s ?? "").trim();
189+
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
190+
191+
switch (block.currentType) {
192+
case WorksheetBlockType.SectionHeader: {
193+
const headerText = md(block.title) || "";
194+
if (!headerText) return "";
195+
196+
// headerType could be "h1" | "h2" | "h3" ... or something custom; map safely
197+
const ht = (block.headerType ?? "h2").toLowerCase();
198+
const level =
199+
ht === "h1" ? 1 :
200+
ht === "h2" ? 2 :
201+
ht === "h3" ? 3 :
202+
ht === "h4" ? 4 :
203+
ht === "h5" ? 5 :
204+
ht === "h6" ? 6 : 2;
205+
206+
return `${"#".repeat(clamp(level, 1, 6))} ${headerText}`;
207+
}
208+
209+
case WorksheetBlockType.Paragraph: {
210+
// Your interface includes paragraphMarkdown (even though it’s under QUESTION FIELDS)
211+
const text = md(block.paragraphMarkdown);
212+
return text ? text : "";
213+
}
214+
215+
case WorksheetBlockType.Question: {
216+
const q = md(block.questionMarkdown);
217+
const a = md(block.answerMarkdown);
218+
219+
const points = block.points ?? 0;
220+
const ptsSuffix = points > 0 ? ` (${points} pt${points === 1 ? "" : "s"})` : "";
221+
222+
const parts: string[] = [];
223+
if (q) {
224+
parts.push(`### Question${ptsSuffix}`);
225+
parts.push(q);
226+
} else {
227+
// still emit a heading if empty question but points exist
228+
if (points > 0) parts.push(`### Question${ptsSuffix}`);
229+
}
230+
231+
const showSolutionFlag = block.showSolution ?? false;
232+
233+
if (showSolutionFlag && a) {
234+
parts.push("");
235+
parts.push(`<details>`);
236+
parts.push(`<summary>Solution</summary>`);
237+
parts.push("");
238+
parts.push(a);
239+
parts.push("");
240+
parts.push(`</details>`);
241+
}
242+
243+
// Optional: add a space indicator based on questionSpaceSize (if you want)
244+
// (Not required; but you can uncomment if you want a visible cue)
245+
//
246+
// const space = block.questionSpaceSize ?? 0;
247+
// if (space > 0) {
248+
// parts.push("");
249+
// parts.push(`_Space: ${space}_`);
250+
// }
251+
252+
return parts.join("\n").trim();
253+
}
254+
255+
case WorksheetBlockType.Divider: {
256+
return `---`;
257+
}
258+
259+
case WorksheetBlockType.PageBreak: {
260+
// Markdown has no official page break; HTML works in many renderers/print pipelines
261+
return `\n<div style="page-break-after: always;"></div>\n`;
262+
}
263+
264+
case WorksheetBlockType.BlankSpace: {
265+
const size = block.size ?? 0;
266+
267+
// Represent as a “blank lines” area in Markdown.
268+
// You can change the visual to underscores or an HTML spacer if preferred.
269+
const n = clamp(Math.round(size), 1, 20); // cap so it doesn't explode
270+
const lines: string[] = [];
271+
lines.push(`_Blank space:_`);
272+
for (let i = 0; i < n; i++) lines.push(`\n`);
273+
return lines.join("").trimEnd();
274+
}
275+
276+
default:
277+
return "";
278+
}
279+
}
103280
}

src/pages/worksheet/worksheet.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ <h6 class="left-align m l">Worksheet</h6>
6464
<i>print</i>
6565
<div>Print</div>
6666
</a>
67+
<a class="l" id="copy-content">
68+
<i>content_copy</i>
69+
<div>Copy Content</div>
70+
<div class="badge border"><i class="tiny">robot_2</i></div>
71+
<div class="tooltip max top">
72+
Copies this lesson plan in Markdown format so you can paste it into ChatGPT, or other AI tools to analyze, adapt, or extend your lesson.
73+
</div>
74+
</a>
6775
<a class="l m" id="share-lesson">
6876
<i>share</i>
6977
<div>Share</div>
@@ -80,6 +88,14 @@ <h6 class="left-align m l">Worksheet</h6>
8088
<i>print</i>
8189
<div>Print</div>
8290
</a>
91+
<a class="m" id="copy-content">
92+
<i>content_copy</i>
93+
<div>Copy Content</div>
94+
<div class="badge border"><i class="tiny">robot_2</i></div>
95+
<div class="tooltip max top">
96+
Copies this lesson plan in Markdown format so you can paste it into ChatGPT, or other AI tools to analyze, adapt, or extend your lesson.
97+
</div>
98+
</a>
8399
<a id="appearance-button">
84100
<i>palette</i>
85101
<span>Appearance</span>
@@ -91,6 +107,14 @@ <h6 class="left-align m l">Worksheet</h6>
91107
<i>print</i>
92108
<div>Print</div>
93109
</a>
110+
<a class="s" id="copy-content">
111+
<i>content_copy</i>
112+
<div>Copy Content</div>
113+
<div class="badge border"><i class="tiny">robot_2</i></div>
114+
<div class="tooltip max top">
115+
Copies this lesson plan in Markdown format so you can paste it into ChatGPT, or other AI tools to analyze, adapt, or extend your lesson.
116+
</div>
117+
</a>
94118
<a class="s" id="share-lesson">
95119
<i>share</i>
96120
<div>Share</div>

src/pages/worksheet/worksheet.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,8 @@ document.addEventListener("DOMContentLoaded", async () => {
482482
// Copy preview markdown
483483
// -----------------------------
484484
bindAll("#copy-content", (el) => {
485-
el.addEventListener("click", () => {
486-
const md = (window as any).preview?.getMarkdown() || "";
485+
el.addEventListener("click", async () => {
486+
const md = await worksheet.copyAsMarkdown();
487487
navigator.clipboard.writeText(md);
488488
new ContentCopiedSnackbar();
489489
});

0 commit comments

Comments
 (0)