DOM Parsing & Scraping

IC Insight reads grade data directly from the Infinite Campus page DOM. It does not use any API. This document explains the parsing pipeline in detail.


Entry: parseModel()

The top-level parser is parseModel() in content.js. It:

  1. Locates the grades panel via getGradesPanel() (looks for visible [role="tabpanel"] elements).
  2. Determines whether to use the multi-quarter path or the legacy flat path.
  3. Calls parseModelWithQuarters().

Returns a model object:

{
  courseId:   "course:12345",
  courseName: "AP CALCULUS BC",
  quarters:   [{ id, name, startDate, endDate, categories: [...] }]
}

parseModelWithQuarters()

The primary parsing path. Steps:

  1. Calls parseQuarterInfo() to detect which quarter(s) are currently visible (see Quarter Detection).
  2. For each detected quarter, calls parseCategoriesInSection(panel, quarter) to extract all categories and assignments.
  3. Returns the model with all quarters populated.

Quarter Detection

Three strategies tried in order (see parseQuarterInfo()):

Strategy DOM target Pattern
1 tl-app-term-picker .header-text "Term Q1" → extract Q1; sub-header for date range MM/DD/YYYY – MM/DD/YYYY
2 document.body full text scan /Term\s+(Q[1-4])\s*\(?\s*(date)\s*[-– - ]\s*(date)/gi
3 document.body fallback /Term\s+(Q[1-4])/gi (no dates)

Quarter IDs are always uppercase Q1Q4. Dates are stored as "MM/DD/YYYY" strings.


parseCategoriesInSection(panel, quarter)

The core assignment-parsing function. It walks the grades panel DOM looking for category and assignment elements.

Category Detection

Categories are identified by elements matching selectors like tl-grading-detail or .grade-category. Each category provides:

  • id - usually a short string like "c0", "c1", from the data-category-id attribute.
  • name - from the category header text.
  • weight - extracted from text matching "Weight: N" or "(N%)" near the category header. null if not found.

Assignment Row Detection

Within each category, assignment rows are found via selectors like tl-student-assignment-row. For each row, three helper functions extract data:

extractScore(row)

Looks for score elements in this order:

  1. .assignment-score__score inside tl-student-assignment-score
  2. .assignment-score__score anywhere in the row

Score text formats recognized:

  • Fraction: "88/100"earned=88, possible=100
  • Percent: "88%"earned=88, possible=100
  • Letter grade: text matching LETTER_GRADE_RE = /^([A-D][+-]?|F)$/iletterGrade="A+", earned=null

Returns null if no score element is found (assignment is ungraded).

extractMultiplier(row)

Searches the row's full text for the pattern /Multiplier\s*:\s*(\d+(?:\.\d+)?)/i. Returns the numeric value, or null if not present. The multiplier is only read from this explicit "Multiplier: X" label - never inferred from the assignment name.

extractDroppedFlag(row)

Checks for a tl-curriculum-flags element whose text matches /\bDropped\b/i. Returns true if found, false otherwise.

Assignment Object

Each parsed assignment is stored as:

{
  name:         string,
  earned:       number | null,
  possible:     number | null,
  ungraded:     boolean,        // true when no score found
  letterGrade:  string | null,  // e.g. 'A+'
  dropped:      boolean,        // true when gradebook-dropped
  multiplier:   number | null,  // only present when != 1
  dueDate:      string | null,  // ISO date string
  dueText:      string | null   // human-readable due text
}

getWorkspaceDocument() and getWorkspaceURL()

Infinite Campus embeds the grades view in an <iframe> on some configurations. When running in the parent frame, getWorkspaceDocument() returns iframe.contentDocument if a grades iframe is found. This ensures selectors query the correct document regardless of embedding.

getWorkspaceURL() similarly returns the src of the grades iframe (or location.href if not in an iframe context), ensuring getCourseKeyFromLocation() extracts the correct classroomSectionID.


Course Key Format

The course key uniquely identifies a course in storage:

function getCourseKeyFromLocation() {
  const url = new URL(getWorkspaceURL() || location.href);
  const section = url.searchParams.get("classroomSectionID")
               || url.searchParams.get("sectionID")
               || "unknown";
  return `course:${section}`;
}

The key uses only the classroomSectionID (not the selectedTermID), so the same course's data from all quarters shares one storage key. This is what enables sequential quarter merging.


initializeICI() - Activation Conditions

The content script initializes on a page only if one of three conditions is true:

  1. In a parent frame on a grades page: location.pathname matches /\/student\/classroom\/grades/i.
  2. In a parent frame containing a grades iframe: A child iframe whose src contains student-grades is present.
  3. Inside the grades iframe itself: window.self !== window.top AND the iframe's URL contains student-grades.

If none of these match, the script silently exits without injecting any UI.


MutationObserver - SPA Navigation Detection

Infinite Campus is a single-page application. After initialization, a MutationObserver watches the document for:

  • Changes to the grades panel content (new assignments loaded)
  • URL changes (navigation to a different course)

On detecting a relevant change, refresh() is called to re-parse and re-render the overlay. A debounce prevents excessive re-renders during rapid DOM mutations.


detectCourseName()

Falls back to reading the page's <h1> or <h2> element for the course name when the model doesn't provide one:

function detectCourseName() {
  const doc = getWorkspaceDocument() || document;
  const h = doc.querySelector("h1, h2, [role='heading'] h1, [role='heading'] h2");
  return (h?.textContent || "Course").trim();
}