mirror of
https://github.com/TriliumNext/Trilium.git
synced 2026-10-12 02:09:59 +00:00
The previous commit parsed every page twice with Defuddle, once as an
article and once as a whole, so that the Article / Whole page switch
was instant. Most clips never use the switch, so every popup and every
shortcut save paid for a parse whose result was thrown away.
`readable-page.js` now reads one way per call. A file injection takes
no arguments, so the script only registers
`globalThis.triliumReadPage(variant)`, and `extractPage(tabId, variant)`
calls it with a second `executeScript({ func, args })` in the same
isolated world. Two script files would have bundled Defuddle twice.
- The popup reads the article, and the whole page only when there is
no article. Switching to Whole page reads it then, showing "Reading
the page…" with Save disabled, and `PagePreview` keeps the result in
`laterReadings` so switching back and forth reads it once. A reading
is `undefined` until made and `null` when it shows nothing or fails,
which disables its button and falls back to the article.
- The shortcut and the context menu read the article, and the whole
page only when there is none: one parse on an article page, as
before the switch existed.
`readActivePage()` falls back with consecutive `if`s rather than
`article ?? await extractPage(...)`: written with `??`, v8 recorded a
negative count for the code after it, and the branch gate failed with
every case tested.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
export type Rect = { x: number, y: number, width: number, height: number };
|
|
|
|
/** An image of a clipping; the clipping's `content` references it by `imageId`. */
|
|
export type ClippedImage = { imageId: string, src: string };
|
|
|
|
/** A page clip, as `entrypoints/readable-page/index.ts` extracts it. */
|
|
export interface ReadablePage {
|
|
title: string;
|
|
content: string;
|
|
images: ClippedImage[];
|
|
pageUrl: string;
|
|
clipType: "page";
|
|
labels: Record<string, string>;
|
|
}
|
|
|
|
/** The two ways to read a page: its article, as Defuddle finds it, and all of it. */
|
|
export type PageVariant = "article" | "wholePage";
|
|
|
|
declare global {
|
|
/** Registered in the tab's isolated world by `entrypoints/readable-page/index.ts`. */
|
|
var triliumReadPage: (variant: PageVariant) => ReadablePage | undefined;
|
|
}
|
|
|
|
/**
|
|
* Injects `entrypoints/readable-page/index.ts` into the tab and reads the page as `variant`, or
|
|
* returns `undefined` when that reading shows nothing. Throws where the extension cannot run
|
|
* scripts, such as browser pages.
|
|
*/
|
|
export async function extractPage(tabId: number, variant: PageVariant): Promise<ReadablePage | undefined> {
|
|
await browser.scripting.executeScript({ target: { tabId }, files: [ "/readable-page.js" ] });
|
|
const [ injection ] = await browser.scripting.executeScript({
|
|
target: { tabId },
|
|
func: (variant: PageVariant) => globalThis.triliumReadPage(variant),
|
|
args: [ variant ]
|
|
});
|
|
return (injection?.result ?? undefined) as ReadablePage | undefined;
|
|
}
|
|
|
|
export function randomString(len: number) {
|
|
let text = "";
|
|
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
export function getPageLocationOrigin() {
|
|
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
|
|
// but for file:// protocol this is browser dependant and in particular Firefox returns "null" in this case.
|
|
return location.protocol === 'file:' ? 'file://' : location.origin;
|
|
}
|
|
|
|
export function createLink(clickAction: object, text: string, color = "lightskyblue") {
|
|
const link = document.createElement('a');
|
|
link.href = "javascript:";
|
|
link.style.color = color;
|
|
link.appendChild(document.createTextNode(text));
|
|
link.addEventListener("click", () => {
|
|
browser.runtime.sendMessage(null, clickAction);
|
|
});
|
|
|
|
return link;
|
|
}
|
|
|
|
/** The page's own summary, preferred to the one it gives social sites, on a single line. */
|
|
export function getPageDescription() {
|
|
const selectors = [
|
|
"meta[name='description']",
|
|
"meta[property='og:description']",
|
|
"meta[name='twitter:description']"
|
|
];
|
|
for (const selector of selectors) {
|
|
const description = document.querySelector(selector)?.getAttribute("content")
|
|
?.replace(/\s+/g, " ").trim();
|
|
if (description) return description;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** The `#pageDescription` label of every clip of the page, if the page has one. */
|
|
export function descriptionLabels(): Record<string, string> {
|
|
const description = getPageDescription();
|
|
return description ? { pageDescription: description } : {};
|
|
}
|
|
|
|
export function makeLinksAbsolute(container: HTMLElement) {
|
|
// `link.href` reads the URL the browser resolved against the page, and leaves
|
|
// `mailto:`, `tel:` and other non-web schemes unchanged.
|
|
for (const link of container.getElementsByTagName('a')) {
|
|
if (link.href) {
|
|
link.setAttribute("href", link.href);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replaces the `src` of each image in `container` with an `imageId`, the same one for the same image.
|
|
* An image waiting to be lazy-loaded is listed under the address the page keeps for later.
|
|
*/
|
|
export function getImages(container: HTMLElement) {
|
|
const images: ClippedImage[] = [];
|
|
|
|
for (const img of container.getElementsByTagName('img')) {
|
|
const src = lazyImageSource(img) ?? img.src;
|
|
if (!src) {
|
|
continue;
|
|
}
|
|
|
|
const existingImage = images.find(image => image.src === src);
|
|
|
|
if (existingImage) {
|
|
img.src = existingImage.imageId;
|
|
}
|
|
else {
|
|
const imageId = randomString(20);
|
|
|
|
images.push({
|
|
imageId,
|
|
src
|
|
});
|
|
|
|
img.src = imageId;
|
|
}
|
|
}
|
|
|
|
return images;
|
|
}
|
|
|
|
/** The attributes lazy-loading scripts keep an image's address in, the same ones Defuddle reads. */
|
|
const LAZY_SOURCE_ATTRIBUTES = [ "data-src", "data-original", "data-lazy-src" ];
|
|
|
|
/** The web address in the first lazy-loading attribute of `img` that holds one. */
|
|
function lazyImageSource(img: HTMLImageElement) {
|
|
for (const attribute of LAZY_SOURCE_ATTRIBUTES) {
|
|
const value = img.getAttribute(attribute)?.trim();
|
|
if (!value) continue;
|
|
|
|
let url: URL;
|
|
try {
|
|
url = new URL(value, img.baseURI);
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
return url.href;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|