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:
- Locates the grades panel via
getGradesPanel()(looks for visible[role="tabpanel"]elements). - Determines whether to use the multi-quarter path or the legacy flat path.
- 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:
- Calls
parseQuarterInfo()to detect which quarter(s) are currently visible (see Quarter Detection). - For each detected quarter, calls
parseCategoriesInSection(panel, quarter)to extract all categories and assignments. - 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 Q1–Q4. 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 thedata-category-idattribute.name- from the category header text.weight- extracted from text matching"Weight: N"or"(N%)"near the category header.nullif 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:
.assignment-score__scoreinsidetl-student-assignment-score.assignment-score__scoreanywhere 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)$/i→letterGrade="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:
- In a parent frame on a grades page:
location.pathnamematches/\/student\/classroom\/grades/i. - In a parent frame containing a grades iframe: A child
iframewhosesrccontainsstudent-gradesis present. - Inside the grades iframe itself:
window.self !== window.topAND the iframe's URL containsstudent-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();
}