Skip to content

Unity-Environmental-University/canvas-injector

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@hale9000/canvas-injector

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.

Features

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

Installation

npm install @hale9000/canvas-injector
# or
yarn add @hale9000/canvas-injector
# or
pnpm add @hale9000/canvas-injector

Quick Start

Transform Markdown to HTML

import { 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>..."

Reverse-Extract Canvas HTML

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
}

Validate Course Structure

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 warnings

Use Canvas API Client

import { 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 }

With Progress Tracking

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)"
  },
);

Architecture

Types (./types)

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.

Transforms (./transforms)

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.

Validators (./validators)

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.

API Client (./api)

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.

Plugin Contexts

Chrome Extension

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 });
  }
});

Obsidian Plugin

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
      },
    });
  }
}

VS Code Extension

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();
  });
}

Node.js CLI

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})`);
  });
}

The Prototype IS The Spec

Instead of separate design documents, the library documents the specification via code:

Reading the Spec

  1. 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")
  2. 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")
  3. 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
  4. 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

Why This Approach?

  • 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

Validation Examples

Detect Vague Rubrics

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: true

Points Validation

import { 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, ... }

Error Handling

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);
}

Performance

  • 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

TypeScript Support

Full TypeScript support with:

  • Strict null checking enabled
  • No implicit any
  • Complete type coverage
  • JSDoc comments on all exports

Browser Support

Works in:

  • ✅ Chrome 90+
  • ✅ Firefox 88+
  • ✅ Safari 14+
  • ✅ Edge 90+
  • ✅ Node.js 16+

Uses fetch API (polyfill in older browsers).

License

MIT

Contributing

Read the source code to understand the spec. Submit PRs that:

  1. Don't break existing types
  2. Include new test cases
  3. Update docstrings if adding features

The code IS the documentation.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published