"ChromeEmbed 의 content-script.js 가 DOM walk, viewport 안 텍스트 picking, form control 과 ARIA label layer, 매 의미 있는 event 마다 SW 로 ship. Lesson 3 가 일곱 track 의 foundation 후 fresh eye 로 그 220-줄 file read 하기."
Walker 패턴
Core 추출 loop:
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
if (!inlineText(node.textContent)) return NodeFilter.FILTER_REJECT;
if (!isVisibleElement(node.parentElement)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
}
);
TreeWalker 가 text node 만 visit. 각자에 acceptNode filter 가 parent 가 hidden (display:none, visibility:hidden 등) 인 node 와 blank 인 node drop. Walker 가 budget 으로 wrap: MAX_VISIBLE_TEXT_NODES = 900, MAX_TEXT_NODE_VISITS = 20000. Cap 이 병적 page (10MB 텍스트) 가 content script freeze 안 시키게 방지.
Visibility-rect 확인
Walker 가 node accept 후 script 가 bounding rect 가 실제로 viewport 와 intersect 하는지 확인:
function visibleTextRect(textNode) {
const range = document.createRange();
try {
range.selectNodeContents(textNode);
for (const rect of range.getClientRects()) {
if (rectIntersectsViewport(rect)) return rect;
}
} finally {
range.detach?.();
}
return null;
}
이게 Track 7 Lesson 3 의 getClientRects() 가 filter 로 적용된 것. Parent 가 display:block 이지만 화면 밖으로 scroll 된 text node 가 intersecting rect 없음; skip 됨. 결과: user 가 지금 실제로 볼 수 있는 텍스트만.
Control layer
텍스트 content 너머, script 가 form control 과 ARIA-label 된 element capture:
const selector = 'input, textarea, select, img[alt], [role="button"], [aria-label]';
for (const element of document.querySelectorAll(selector)) {
if (!isVisibleElement(element)) continue;
const rect = element.getBoundingClientRect();
if (!rectIntersectsViewport(rect)) continue;
const text = controlLabel(element);
if (!text) continue;
entries.push({ top: rect.top, left: rect.left, text });
}
이게 Pippa 가 user 가 page 에서 read 할 수 있는 것만 아닌 do 할 수 있는 것도 aware 하게. 이미지의 alt 텍스트, 버튼의 aria-label, input 의 placeholder — 모두 context 일부 됨. Element type 별 label-추출 logic 이 controlLabel() 에.
Spatial sort
Entry 가 모인 후 위에서 아래, 왼쪽에서 오른쪽으로 sort:
entries.sort((a, b) => (a.top - b.top) || (a.left - b.left));
이게 flat 한 텍스트 fragment 모음에서 읽기 순서 회복. 현대 page 가 source 순서 wildly 벗어난 layout (CSS Grid, reverse 가진 Flexbox) 가지지만, visual 위-아래 순서가 user 가 perceive 하는 것. Rect 좌표로 sort 가 DOM 순서 아닌 visual 순서 존중.
Throttle
세 event listener 가 context update trigger:
scroll— 350 ms quiet period 가진scheduleContext통해 throttled. 무거운 event; batching 중요.selectionchange,mouseup,pointerup,keyup,touchend— selection 관련;scheduleSelectionContext가 0-ms setTimeout (microtask) 으로 즉시 fire, selection 이 panel 에 가능한 한 real-time 가깝게 도착.focus— window 가 focus 되찾음; re-snapshot.- 초기 호출 — file 끝의
sendContext(). Injection 순간 상태 capture.
Scroll 에 throttle vs selection 에 즉시 가 맞는 거래. Scroll 이 single drag 동안 수백 번 fire; selection 이 user 가 실제로 highlight 할 때만 fire. 다른 cadence, 다른 handler.
Selection cache
미묘한 UX detail: user 가 side panel 열기 위해 click 하면서 selection 종종 사라짐. Cache:
function currentSelectionText() {
const current = truncate(window.getSelection?.().toString() || '', MAX_SELECTION_CHARS);
if (current) {
lastSelection = current;
lastSelectionAt = Date.now();
return current;
}
if (lastSelection && Date.now() - lastSelectionAt <= SELECTION_CACHE_MS) {
return lastSelection;
}
return '';
}
Live selection 이 비었지만 지난 5 분 안에 capture 된 거 있으면 cached version 반환. User 가 '그거 highlight 하고 Pippa 한테 물었음' 생각 — 'oh, panel 여는 click 이 selection clear 함' 안 생각. Cache 가 경험을 intent 와 매칭.
SW 가 받는 것
sendContext() 호출 당 payload 하나, Lesson 6 에서 자세히 설명된 shape. Content script 가 sensor; SW 가 bus; panel 이 consumer. 각 layer 가 한 job; 함께 cwkPippa 를 user 가 지금 읽고 있는 것으로 먹임.
isTopFrame: false 와 자체 context 보냄. SW 의 merge logic (Lesson 2) 가 sub-frame selection merge 하면서 top-frame 의 bulk 텍스트 보존. 이렇게 YouTube embed 안 텍스트 highlight 하는 user 가 여전히 Pippa 에 올바르게 닿음.