Self-documenting Canvas course transformation library for Node.js and browsers.
The prototype IS the specification. The TypeScript code documents every transformation rule, API endpoint, and validation criterion.
✅ Markdown ↔ Canvas HTML - Round-trip conversion with template preservation ✅ Canvas API Client - Full REST API support (create/update courses, modules, assignments, pages, rubrics) ✅ Comprehensive Validators - Check points, rubrics, course structure ✅ Context-Agnostic - Works in Node.js, Chrome, Obsidian, VS Code, browsers ✅ Self-Documenting - Types and functions document the spec via their signatures ✅ TypeScript First - Full type safety, tree-shakeable, zero dependencies
npm install @hale9000/canvas-injector
# or
yarn add @hale9000/canvas-injector
# or
pnpm add @hale9000/canvas-injectorimport { parseMarkdown, markdownToHtml } from '@hale9000/canvas-injector/transforms';
const markdown = `
---
title: "My Assignment"
points: 100
rubric:
- criterion: "Theoretical Grounding"
points: 50
description: "Student demonstrates understanding of core concepts"
---
# Assignment Instructions
Write a 5-page essay...
`;
const parsed = parseMarkdown(markdown);
console.log(parsed.data.title); // "My Assignment"
console.log(parsed.data.points); // 100
console.log(parsed.data.rubric); // Array of criteria
const html = markdownToHtml(parsed.body);
console.log(html); // "<h1>Assignment Instructions</h1>..."import { reverseExtractFromCanvasHtml } from '@hale9000/canvas-injector/transforms';
const canvasHtml = `
<link rel="stylesheet" href="...UTC_Template2023_Mobile.css">
<div class="cbt-banner ...">
<h1>Assignment Title</h1>
</div>
<div class="cbt-content">
<p>Assignment instructions...</p>
</div>
`;
const result = reverseExtractFromCanvasHtml(canvasHtml);
if (result.success) {
console.log(result.data?.markdown); // YAML frontmatter + markdown body
console.log(result.data?.analysis); // Theme, CSS/JS URLs, metadata
}import { validateCourseStructure } from '@hale9000/canvas-injector/validators';
const assignments = [
{
id: 'assign_1',
name: 'Assignment 1',
points_possible: 100,
rubric: { /* ... */ },
// ...
},
// ... more assignments
];
const result = validateCourseStructure(assignments, 800);
console.log(result.isValid); // true/false
console.log(result.errors); // Array of errors
console.log(result.warnings); // Array of warningsimport { CanvasApiClient } from '@hale9000/canvas-injector/api';
const client = new CanvasApiClient({
baseUrl: 'https://university.instructure.com',
token: 'your-api-token',
});
// Get course
const course = await client.getCourse(123);
// Create module
const module = await client.createModule(123, {
name: 'Module 1',
position: 1,
});
// Create assignment
const assignment = await client.createAssignment(123, {
name: 'Assignment 1',
points_possible: 100,
description: '<p>Assignment description</p>',
due_at: '2025-12-31T23:59:59Z',
submission_types: ['online_text_entry', 'online_upload'],
grading_type: 'points',
});
// Upload complete course
const result = await client.uploadCourseData(123, courseData);
console.log(result.created); // { modules: 5, assignments: 13, pages: 2 }import { uploadCourseWithProgress } from '@hale9000/canvas-injector/api';
await uploadCourseWithProgress(
client,
123,
courseData,
(progress) => {
console.log(`${progress.status} (${progress.current}/${progress.total})`);
// Output: "Creating module: Module 1 (1/20)"
// "Creating assignment: Assignment 1 (5/20)"
// "Creating page: Syllabus (19/20)"
},
);Complete TypeScript definitions for:
- Frontmatter & Metadata - YAML frontmatter structure, parsed markdown files
- Rubric - Criterion definition, Canvas rubric objects
- HTML Templates - Template formats, theme analysis
- Canvas Objects - Courses, modules, assignments, pages, rubrics
- Validation - Result types, point validation, rubric validation
- API Client - Configuration, request/response types, context types
Read types as documentation: src/types/index.ts is the API contract.
Transformation functions:
- Parsing -
parseMarkdown(),extractHtmlTemplate(),extractRubric() - Conversion -
markdownToHtml(),htmlToMarkdown(),applyHtmlTemplate() - Analysis -
analyzeCanvasHtml(),extractRubricFromHtml() - Reverse -
reverseExtractFromCanvasHtml()(Canvas HTML → markdown)
Read functions as spec: Each function signature documents exactly what it transforms.
Validation functions:
- Points -
validatePointsTotal(),validateAssignmentRubric() - Rubrics -
validateRubric(),validateRubricCriterion() - Course -
validateCourseMetadata(),validateModule(),validateCourseStructure() - Reports -
generateValidationReport()
Read validators as spec: Each validator documents what it checks and why.
Canvas REST API client:
- Courses -
getCourse(),updateCourse() - Modules -
getModules(),createModule() - Assignments -
getAssignments(),createAssignment(),updateAssignment() - Pages -
getPages(),createPage(),updatePage() - Rubrics -
createRubric(),associateRubricWithAssignment() - Batch -
uploadCourseData(),uploadCourseWithProgress(),exportCourseData()
Read API client as spec: Each method documents the Canvas REST API endpoint and data format.
import { parseMarkdown, reverseExtractFromCanvasHtml } from '@hale9000/canvas-injector/transforms';
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'extractCanvasHtml') {
const result = reverseExtractFromCanvasHtml(request.html);
sendResponse({ success: result.success, markdown: result.data?.markdown });
}
});import { CanvasApiClient } from '@hale9000/canvas-injector/api';
import { parseMarkdown } from '@hale9000/canvas-injector/transforms';
export default class CanvasInjectorPlugin extends Plugin {
async onload() {
this.addCommand({
id: 'inject-to-canvas',
name: 'Inject current note to Canvas',
callback: async () => {
const file = this.app.workspace.getActiveFile();
const content = await this.app.vault.read(file);
const parsed = parseMarkdown(content);
const client = new CanvasApiClient({
baseUrl: this.settings.canvasUrl,
token: this.settings.canvasToken,
});
// Use parsed.data for metadata, parsed.body for content
},
});
}
}import * as vscode from 'vscode';
import { validateCourseStructure } from '@hale9000/canvas-injector/validators';
export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('canvas-injector.validate', async () => {
const editor = vscode.window.activeTextEditor;
const content = editor?.document.getText();
// Validate course structure
const result = validateCourseStructure(assignments, 800);
// Show in output channel
const channel = vscode.window.createOutputChannel('Canvas Injector');
channel.appendLine(generateValidationReport(result));
channel.show();
});
}import { CanvasApiClient, uploadCourseWithProgress } from '@hale9000/canvas-injector/api';
import { parseMarkdown } from '@hale9000/canvas-injector/transforms';
import * as fs from 'fs';
async function main() {
const client = new CanvasApiClient({
baseUrl: process.env.CANVAS_URL!,
token: process.env.CANVAS_TOKEN!,
});
const courseJson = JSON.parse(fs.readFileSync('.course.json', 'utf-8'));
const result = await uploadCourseWithProgress(client, courseJson.canvas_id, courseData, (p) => {
console.log(`${p.status} (${p.current}/${p.total})`);
});
}Instead of separate design documents, the library documents the specification via code:
-
API Contract - Read
src/types/index.ts- Every exported interface is a data structure
- Field names and types document what data is required
- Comments explain context (e.g., "NOT: vague / YES: specific")
-
Transformation Rules - Read
src/transforms/index.ts- Function signatures document input/output
- Implementation shows exact transformation logic
- Comments explain the reasoning (e.g., "Canvas Banner Template detection")
-
Validation Rules - Read
src/validators/index.ts- Function names document what is validated
- Error/warning arrays document what can fail
- Implementation shows exact validation criteria
-
API Endpoints - Read
src/api/index.ts- Method names document Canvas REST API operations
- Parameters document Canvas API data format
- Comments reference Canvas API documentation
- Single Source of Truth - Code and spec are the same
- Always Up-to-Date - No separate docs to maintain
- Executable Spec - Code is tested and working
- Clear Contracts - TypeScript enforces the spec at compile time
- Self-Documenting - Function/type names clearly state intent
import { validateRubricCriterion } from '@hale9000/canvas-injector/validators';
const criterion = {
criterion: 'Clarity',
points: 20,
description: 'Student writes clearly', // ❌ Vague
};
const result = validateRubricCriterion(criterion);
// warnings: ["Criterion 'Clarity' uses vague language. Recommend NOT/YES examples instead."]
// Fixed:
const fixed = {
criterion: 'Clarity',
points: 20,
description: `NOT: "text is hard to follow" | YES: "text flows logically with clear transitions"`,
};
const result2 = validateRubricCriterion(fixed);
// isValid: trueimport { validatePointsTotal } from '@hale9000/canvas-injector/validators';
const assignments = [
{ name: 'Module 1', points_possible: 150 },
{ name: 'Module 2', points_possible: 150 },
{ name: 'Module 3', points_possible: 150 },
{ name: 'Module 4', points_possible: 150 },
{ name: 'Module 5', points_possible: 200 },
];
const result = validatePointsTotal(assignments, 800);
// isValid: true
// total: 800
// byModule: { module_1: 150, module_2: 150, ... }All functions return result objects with .success, .data, .error, and .warnings:
const result = reverseExtractFromCanvasHtml(htmlContent);
if (!result.success) {
console.error(`Failed: ${result.error}`);
console.warn(result.warnings);
} else {
console.log(result.data?.markdown);
}- No Runtime Dependencies - Only gray-matter and markdown-it for parsing
- Tree-Shakeable - Import only what you need
- Async/Await - All API calls are asynchronous
- Streaming Ready - Can be extended for large file handling
Full TypeScript support with:
- Strict null checking enabled
- No implicit any
- Complete type coverage
- JSDoc comments on all exports
Works in:
- ✅ Chrome 90+
- ✅ Firefox 88+
- ✅ Safari 14+
- ✅ Edge 90+
- ✅ Node.js 16+
Uses fetch API (polyfill in older browsers).
MIT
Read the source code to understand the spec. Submit PRs that:
- Don't break existing types
- Include new test cases
- Update docstrings if adding features
The code IS the documentation.