Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions injected/src/features/page-context.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ContentFeature from '../content-feature.js';
import { getFaviconList } from './favicon.js';
import { isDuckAi, isBeingFramed, getTabUrl } from '../utils.js';
import { getActiveSelectionLanguage } from './page-context/utils.js';
const MSG_PAGE_CONTEXT_RESPONSE = 'collectionResult';

function collapseWhitespace(str) {
Expand Down Expand Up @@ -76,12 +77,24 @@ export default class PageContext extends ContentFeature {
listenForUrlChanges = true;

init() {
this.setupActiveSelectionLanguageListener();
if (!this.shouldActivate()) {
return;
}
this.setupListeners();
}

setupActiveSelectionLanguageListener() {
if (!this.getFeatureSettingEnabled('subscribeToActiveSelectionLanguage', 'enabled')) {
return;
}
this.messaging.subscribe('getActiveSelectionLanguage', () => {
const activeSelectionLanguage = getActiveSelectionLanguage();
this.log.info('Active selection language', activeSelectionLanguage);
this.messaging.notify('getActiveSelectionLanguageResult', { activeSelectionLanguage });
});
}

setupListeners() {
this.observeContentChanges();
if (this.getFeatureSettingEnabled('subscribeToCollect', 'enabled')) {
Expand Down
40 changes: 40 additions & 0 deletions injected/src/features/page-context/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export function getActiveSelectionLanguage() {
// BCP 47 language tag regex
const bcp47 =
/^(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE|art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang|(?<langtag>(?<language>[A-Za-z]{2,3}(?<extlang>-[A-Za-z]{3}(-[A-Za-z]{3}){0,2})?|[A-Za-z]{4,8})(?:-(?<script>[A-Za-z]{4}))?(?:-(?<region>[A-Za-z]{2}|[0-9]{3}))?(?<variants>(?:-(?:[0-9A-Za-z]{5,8}|[0-9][0-9A-Za-z]{3}))*)(?<extensions>(?:-(?:[0-9A-WY-Za-wy-z](?:-[0-9A-Za-z]{2,8})+))*)(?:-(?<privateuse>x(?:-[0-9A-Za-z]{1,8})+))?)|(?<privateuse>x(?:-[0-9A-Za-z]{1,8})+))$/gim;

// Get the Selection from the currently focused document (top or same-origin iframe)
function getActiveSelection() {
const activeElement = document.activeElement;
if (activeElement && activeElement.tagName === 'IFRAME') {
try {
// Type assertion to access contentWindow safely
const iframe = /** @type {HTMLIFrameElement} */ (activeElement);
return iframe.contentWindow?.getSelection() || null;
} catch {
// Cross-origin iframe: cannot access its selection
}
}
return window.getSelection?.() || null;
}

const selection = getActiveSelection();
const startContainer = selection?.rangeCount ? selection.getRangeAt(0).startContainer : null;

// If no selection, use the document's lang (or null if absent)
if (!startContainer) {
return document.documentElement.getAttribute('lang') || null;
}

// If the start is a text node, step up to its parent element
const startElement =
startContainer.nodeType === Node.ELEMENT_NODE ? /** @type {Element} */ (startContainer) : startContainer.parentElement;

// Look up the DOM tree for a lang attribute; fallback to the document's lang
const lang = startElement?.closest('[lang]')?.getAttribute('lang') || document.documentElement.getAttribute('lang');

// Validate the lang attribute against the BCP 47 regex
if (lang && bcp47.test(lang)) return lang;

return null;
}
8 changes: 8 additions & 0 deletions injected/src/messages/page-context/collect.subscribe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Collect",
"description": "Subscription to collect page content data",
"properties": {},
"additionalProperties": false
}
91 changes: 91 additions & 0 deletions injected/src/messages/page-context/collectionResult.notify.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "CollectionResult",
"description": "Result of page content collection, either success or error",
"oneOf": [
{
"type": "object",
"title": "Collection Success",
"required": ["success", "data"],
"properties": {
"success": {
"const": true
},
"data": {
"type": "object",
"description": "Collected page data",
"required": ["favicon", "title", "content", "truncated", "timestamp", "url"],
"properties": {
"favicon": {
"type": "array",
"description": "List of favicon URLs",
"items": {
"type": "string"
}
},
"title": {
"type": ["string", "null"],
"description": "Page title"
},
"metaDescription": {
"type": ["string", "null"],
"description": "Page meta description"
},
"content": {
"type": "string",
"description": "Main page content in markdown format"
},
"truncated": {
"type": "boolean",
"description": "Whether content was truncated due to size limits"
},
"headings": {
"type": "array",
"description": "List of page headings",
"items": {
"type": "string"
}
},
"links": {
"type": "array",
"description": "List of page links",
"items": {
"type": "string"
}
},
"images": {
"type": "array",
"description": "List of image URLs",
"items": {
"type": "string"
}
},
"timestamp": {
"type": "number",
"description": "Unix timestamp when data was collected"
},
"url": {
"type": "string",
"description": "Page URL"
}
}
}
}
},
{
"type": "object",
"title": "Collection Error",
"required": ["success", "error"],
"properties": {
"success": {
"const": false
},
"error": {
"type": "string",
"description": "Error message describing what went wrong"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GetActiveSelectionLanguage",
"description": "Subscription to get the active selection's language",
"properties": {},
"additionalProperties": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GetActiveSelectionLanguageResult",
"description": "Result of getting the active selection's language as BCP 47 language tag",
"required": ["activeSelectionLanguage"],
"properties": {
"activeSelectionLanguage": {
"type": ["string", "null"],
"description": "BCP 47 language tag of the active selection (e.g., 'en', 'en-US', 'fr-FR') or null if no language detected",
"pattern": "^(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE|art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang|(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}(?:-[A-Za-z]{3}){0,2})?|[A-Za-z]{4,8})(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:(?:-(?:[0-9A-Za-z]{5,8}|[0-9][0-9A-Za-z]{3}))*)(?:(?:-(?:[0-9A-WY-Za-wy-z](?:-[0-9A-Za-z]{2,8})+))*)(?:-(?:x(?:-[0-9A-Za-z]{1,8})+))?)|(?:x(?:-[0-9A-Za-z]{1,8})+))$",
"examples": ["en", "en-US", "fr-FR", "zh-CN", null]
}
}
}
Loading