meisolates the numeric set ID. 2. **Paginated API Fetching**: Iterativefetch()calls usingperPage=200and dynamicpagingTokenresolution untiltotalcount matches or payload shrinks. 3. **Data Normalization**: ExtractsplainTextfromcardSidesmedia arrays, strips whitespace, and builds a bidirectional term↔definition map. 4. **Multi-Format Export Pipeline**: Normalized JSON is piped through format-specific serializers (CSV, TXT, PDF viajsPDF, Anki via ankipack). 5. **Cross-Tab Merge Engine**: Uses chrome.storage.local` and tab messaging to aggregate multiple open sets before export.
6. Knowt Direct Import: Bypasses the buggy extension by submitting normalized data directly to Knowt's import endpoint via background fetch.
Anki Integration (ankipack)
Anki's schema evolution (V18 with protobuf-encoded deck configs) broke existing TypeScript generators. ankipack was built to target Anki 24.x+ specifically, generating calendar-aware presets that align review intervals with exam dates.
Automation Utility (Console Script)
The following script demonstrates low-level DOM event simulation and API consumption for Quizlet's matching game. It uses precise coordinate-based PointerEvent/MouseEvent dispatching to bypass framework-level click handlers.
(async () => {
// ===== config =====
const CLICK_DELAY_MS = 120; // delay between the two clicks of a pair
const PAIR_DELAY_MS = 250; // delay after a completed pair
const START_WAIT_MS = 1500; // wait for board to appear after clicking start
const MAX_ITERS = 500; // safety stop
// ==================
const C = 'color:#7F77DD;font-weight:bold';
const log = (m, ...a) => console.log(`%c[Match]%c ${m}`, C, '', ...a);
const warn = (m, ...a) => console.warn(`%c[Match]%c ${m}`, C, '', ...a);
const err = (m, ...a) => console.error(`%c[Match]%c ${m}`, C, '', ...a);
const sleep = ms => new Promise(r => setTimeout(r, ms));
const norm = s => (s || '').trim().toLowerCase().replace(/\s+/g, ' ');
const realClick = (el) => {
const r = el.getBoundingClientRect();
const o = { bubbles: true, cancelable: true, view: window,
clientX: r.left + r.width/2, clientY: r.top + r.height/2 };
el.dispatchEvent(new PointerEvent('pointerdown', { ...o, pointerType: 'mouse' }));
el.dispatchEvent(new MouseEvent('mousedown', o));
el.dispatchEvent(new PointerEvent('pointerup', { ...o, pointerType: 'mouse' }));
el.dispatchEvent(new MouseEvent('mouseup', o));
el.dispatchEvent(new MouseEvent('click', o));
};
const isVisible = (el) => {
if (!el?.isConnected) return false;
const r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) return false;
const cs = getComputedStyle(el);
return cs.visibility !== 'hidden' && cs.display !== 'none' && parseFloat(cs.opacity) > 0.1;
};
// 1. extract set id
const m = location.pathname.match(/^\/(?:[a-z]{2}\/)?(\d+)(?:\/|$)/);
if (!m) { err('Could not parse set ID from URL'); return; }
const setId = m[1];
log(`Set ID: ${setId}`);
// 2. fetch cards (paginated)
log('Fetching cards...');
const cards = [];
let page = 1, pagingToken = '';
const perPage = 200;
while (true) {
const qs = new URLSearchParams({
'filters[studiableContainerId]': setId,
'filters[studiableContainerType]': '1',
perPage: String(perPage),
page: String(page),
});
if (pagingToken) qs.set('pagingToken', pagingToken);
const res = await fetch(`https://quizlet.com/webapi/3.4/studiable-item-documents?${qs}`, { credentials: 'include' });
if (!res.ok) { err(`API returned ${res.status}`); return; }
const data = await res.json();
const resp = data?.responses?.[0];
const items = resp?.models?.studiableItem || [];
for (const it of items) {
const term = it.cardSides?.[0]?.media?.find(x => x.plainText)?.plainText;
const def = it.cardSides?.[1]?.media?.find(x => x.plainText)?.plainText;
if (term && def) cards.push({ term, def });
}
const total = resp?.paging?.total;
pagingToken = resp?.paging?.token || '';
if (items.length < perPage || (total != null && cards.length >= total)) break;
page++;
}
log(`Got ${cards.length} cards`);
if (!cards.length) { err('No cards fetched'); return; }
// build pair lookup: each side knows its partner
const pair = new Map();
for (const { term, def } of cards) {
pair.set(norm(term), norm(def));
pair.set(norm(def), norm(term));
}
// 3. click start button inside #__next
const root = document.getElementById('__next');
if (!root) { err('#__next not found'); return; }
const startRx = /spiel beginnen|start game|jouer|commencer|begin game|empezar|iniciar|gioca/i;
let startBtn = [...root.querySelectorAll('button')].find(b => {
const lab = (b.getAttribute('aria-label') || '') + ' ' + (b.textContent || '');
return startRx.test(lab);
});
if (!startBtn) startBtn = root.querySelector('button[data-testid="assembly-button-primary"]');
if (startBtn) {
log(`Clicking start: "${(startBtn.getAttribute('aria-label') || startBtn.textContent || '').trim()}"`);
realClick(startBtn);
await sleep(START_WAIT_MS);
} else {
warn('No start button found (assuming game already running)');
}
// 4. matching loop
const getTiles = () => {
const nodes = root.querySelectorAll('.FormattedText[aria-label]');
const out = [];
for (const n of nodes) {
// FormattedText -> .t1s3w3lt -> .c13hkcga -> tile container
const tile = n.parentElement?.parentElement?.parentElement;
if (!tile || !isVisible(tile)) continue;
out.push({ tile, text: n.getAttribute('aria-label') || '' });
}
return out;
};
log('Matching started');
let matched = 0, stuck = 0, prevCount = -1;
for (let i = 0; i < MAX_ITERS; i++) {
const tiles = getTiles();
if (tiles.length === 0) {
log(`%c✓ Done. Matched ${matched} pairs.`, 'color:#1D9E75;font-weight:bold');
return;
}
if (tiles.length === prevCount) {
if (++stuck > 3) { err('Stuck. Remaining:', tiles.map(t => t.text)); return; }
} else { stuck = 0; prevCount = tiles.length; }
// map normalized text -> DOM tile (for currently visible tiles)
const visible = new Map();
for (const t of tiles) {
const k = norm(t.text);
if (!visible.has(k)) visible.set(k, t.tile);
}
// find first tile whose partner is also visible
let A, B, aTxt, bTxt;
for (const t of tiles) {
const partner = pair.get(norm(t.text));
if (partner && visible.has(partner) && visib
Pitfall Guide
- DOM Scraping Over API Consumption: Relying on HTML parsing fails with virtualized lists, lazy loading, and framework-rendered components. Always reverse-engineer the internal REST/GraphQL API used by the frontend.
- Ignoring Pagination Tokens: Assuming
page increments linearly breaks when platforms use cursor-based or token-based pagination. Always track pagingToken and validate against total counts.
- Framework-Blind Click Simulation: Modern React/Next.js apps ignore native
.click() or dispatchEvent('click'). You must dispatch full PointerEvent/MouseEvent chains with precise bounding box coordinates to trigger framework event listeners.
- Hardcoding Anki Schema Versions: Anki frequently updates its internal deck format (e.g., V18 with protobuf). Generators must dynamically detect schema versions and abstract config serialization to avoid silent corruption.
- Cross-Tab State Race Conditions: Merging multiple sets requires synchronized tab communication. Use
chrome.storage.local with atomic updates and debounce mechanisms to prevent overwrites during concurrent exports.
- Over-Requesting Extension Permissions: Requesting broad
host_permissions triggers Chrome Web Store rejection and user distrust. Scope permissions to activeTab and specific API endpoints, requesting additional access only when triggered by user action.
Deliverables
- 📘 Extension Architecture Blueprint: Complete flowchart covering URL detection → API pagination → data normalization → multi-format serialization → cross-tab merge logic. Includes endpoint mapping for
/webapi/3.4/studiable-item-documents and Knowt import routing.
- ✅ Implementation Checklist: Step-by-step validation protocol for manifest configuration, permission scoping, API response parsing, pagination token handling, Anki V18 schema compliance, and export pipeline testing.
- ⚙️ Configuration Templates:
manifest.json (MV3 compliant, minimal host permissions)
ankipack preset generator config (calendar-driven review intervals, V18 protobuf schema mapping)
- Export format serializers (CSV/TXT/JSON/PDF/Anki/Knowt) with normalization rules and delimiter handling