Skip to content

Commit 4f468ec

Browse files
committed
Merge branch 'main' of github.com:quarto-dev/quarto-cli into main
2 parents 18ac55f + e72f556 commit 4f468ec

File tree

16 files changed

+835
-44
lines changed

16 files changed

+835
-44
lines changed

package/src/import_map.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
"streams/": "https://deno.land/[email protected]/streams/",
1313
"textproto/": "https://deno.land/[email protected]/textproto/",
1414
"uuid/": "https://deno.land/[email protected]/uuid/",
15+
"node/": "https://deno.land/[email protected]/node/",
1516
"cliffy/": "https://deno.land/x/[email protected]/",
1617
"dayjs/": "https://cdn.skypack.dev/[email protected]/",
1718
"moment-guess": "https://cdn.skypack.dev/[email protected]",
1819
"deno_dom/": "https://deno.land/x/[email protected]/",
1920
"cache/": "https://deno.land/x/[email protected]/",
2021
"media_types/": "https://deno.land/x/[email protected]/",
2122
"observablehq/parser": "https://cdn.skypack.dev/@observablehq/[email protected]",
23+
"events/": "https://deno.land/x/[email protected]/",
2224
"xmlp/": "https://deno.land/x/[email protected]/",
2325
"ajv": "https://cdn.skypack.dev/[email protected]",
2426
"blueimpMd5": "https://cdn.skypack.dev/[email protected]",
@@ -104,4 +106,4 @@
104106
"/npm:[email protected]?dew": "./resources/vendor/dev.jspm.io/[email protected]"
105107
}
106108
}
107-
}
109+
}

src/core/cri/cri.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* cri.ts
3+
*
4+
* Chrome Remote Interface
5+
*
6+
* Copyright (c) 2022 by RStudio, PBC.
7+
*/
8+
9+
import { decode } from "encoding/base64.ts";
10+
import cdp from "./deno-cri/index.js";
11+
import { getBrowserExecutablePath } from "../puppeteer.ts";
12+
import { Semaphore } from "../lib/semaphore.ts";
13+
import { findOpenPort } from "../port.ts";
14+
15+
async function waitForServer(port: number, timeout = 3000) {
16+
const interval = 50;
17+
let soFar = 0;
18+
19+
do {
20+
try {
21+
const response = await fetch(`http://localhost:${port}/json/list`);
22+
if (response.status !== 200) {
23+
soFar += interval;
24+
await new Promise((resolve) => setTimeout(resolve, interval));
25+
continue;
26+
} else {
27+
return true;
28+
}
29+
} catch (_e) {
30+
soFar += interval;
31+
await new Promise((resolve) => setTimeout(resolve, interval));
32+
}
33+
} while (soFar < timeout);
34+
return false;
35+
}
36+
37+
const criSemaphore = new Semaphore(1);
38+
39+
export function withCriClient<T>(
40+
fn: (client: Awaited<ReturnType<typeof criClient>>) => Promise<T>,
41+
appPath?: string,
42+
port?: number,
43+
): Promise<T> {
44+
if (port === undefined) {
45+
port = findOpenPort(9222);
46+
}
47+
48+
return criSemaphore.runExclusive(async () => {
49+
const client = await criClient(appPath, port);
50+
try {
51+
const result = await fn(client);
52+
await client.close();
53+
return result;
54+
} catch (e) {
55+
await client.close();
56+
throw e;
57+
}
58+
});
59+
}
60+
61+
export async function criClient(appPath?: string, port?: number) {
62+
if (port === undefined) {
63+
port = findOpenPort(9222);
64+
}
65+
if (appPath === undefined) {
66+
appPath = await getBrowserExecutablePath();
67+
}
68+
69+
const cmd = [
70+
appPath as string,
71+
"--headless",
72+
"--no-sandbox",
73+
"--single-process",
74+
"--disable-gpu",
75+
`--remote-debugging-port=${port}`,
76+
];
77+
const browser = Deno.run({ cmd, stdout: "piped", stderr: "piped" });
78+
79+
if (!(await waitForServer(port))) {
80+
throw new Error("Couldn't find open server");
81+
}
82+
83+
// deno-lint-ignore no-explicit-any
84+
let client: any;
85+
86+
const result = {
87+
close: async () => {
88+
await client.close();
89+
browser.close();
90+
},
91+
92+
rawClient: () => client,
93+
94+
open: async (url: string) => {
95+
client = await cdp();
96+
const { Network, Page } = client;
97+
await Network.enable();
98+
await Page.enable();
99+
await Page.navigate({ url });
100+
return new Promise((fulfill, _reject) => {
101+
Page.loadEventFired(() => {
102+
fulfill(null);
103+
});
104+
});
105+
},
106+
107+
docQuerySelectorAll: async (cssSelector: string): Promise<number[]> => {
108+
await client.DOM.enable();
109+
const doc = await client.DOM.getDocument();
110+
const nodeIds = await client.DOM.querySelectorAll({
111+
nodeId: doc.root.nodeId,
112+
selector: cssSelector,
113+
});
114+
return nodeIds.nodeIds;
115+
},
116+
117+
contents: async (cssSelector: string): Promise<string[]> => {
118+
const nodeIds = await result.docQuerySelectorAll(cssSelector);
119+
return Promise.all(
120+
// deno-lint-ignore no-explicit-any
121+
nodeIds.map(async (nodeId: any) => {
122+
return (await client.DOM.getOuterHTML({ nodeId })).outerHTML;
123+
}),
124+
);
125+
},
126+
127+
// defaults to screenshotting at 4x scale = 392dpi.
128+
screenshots: async (
129+
cssSelector: string,
130+
scale = 4,
131+
): Promise<{ nodeId: number; data: Uint8Array }[]> => {
132+
const nodeIds = await result.docQuerySelectorAll(cssSelector);
133+
const lst: { nodeId: number; data: Uint8Array }[] = [];
134+
for (const nodeId of nodeIds) {
135+
// the docs say that inline elements might return more than one box
136+
// TODO what do we do in that case?
137+
let quad;
138+
try {
139+
quad = (await client.DOM.getContentQuads({ nodeId })).quads[0];
140+
} catch (_e) {
141+
// TODO report error?
142+
continue;
143+
}
144+
const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
145+
const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
146+
const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
147+
const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
148+
try {
149+
const screenshot = await client.Page.captureScreenshot({
150+
clip: {
151+
x: minX,
152+
y: minY,
153+
width: maxX - minX,
154+
height: maxY - minY,
155+
scale,
156+
},
157+
fromSurface: true,
158+
captureBeyondViewport: true,
159+
});
160+
const buf = decode(screenshot.data);
161+
lst.push({ nodeId, data: buf });
162+
} catch (_e) {
163+
// TODO report error?
164+
continue;
165+
}
166+
}
167+
return lst;
168+
},
169+
};
170+
return result;
171+
}

src/core/cri/deno-cri/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## A deno port of https://github.com/cyrus-and/chrome-remote-interface
2+
3+
This directory contains a minimal, self-contained deno port of https://github.com/cyrus-and/chrome-remote-interface

src/core/cri/deno-cri/api.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* api.js
3+
*
4+
* Copyright (c) 2021 Andrea Cardaci <[email protected]>
5+
*
6+
* Deno port Copyright (C) 2022 by RStudio, PBC
7+
*/
8+
9+
function arrayToObject(parameters) {
10+
const keyValue = {};
11+
parameters.forEach((parameter) => {
12+
const name = parameter.name;
13+
delete parameter.name;
14+
keyValue[name] = parameter;
15+
});
16+
return keyValue;
17+
}
18+
19+
function decorate(to, category, object) {
20+
to.category = category;
21+
Object.keys(object).forEach((field) => {
22+
// skip the 'name' field as it is part of the function prototype
23+
if (field === "name") {
24+
return;
25+
}
26+
// commands and events have parameters whereas types have properties
27+
if (
28+
(category === "type" && field === "properties") ||
29+
field === "parameters"
30+
) {
31+
to[field] = arrayToObject(object[field]);
32+
} else {
33+
to[field] = object[field];
34+
}
35+
});
36+
}
37+
38+
function addCommand(chrome, domainName, command) {
39+
const commandName = `${domainName}.${command.name}`;
40+
const handler = (params, sessionId, callback) => {
41+
return chrome.send(commandName, params, sessionId, callback);
42+
};
43+
decorate(handler, "command", command);
44+
chrome[commandName] = chrome[domainName][command.name] = handler;
45+
}
46+
47+
function addEvent(chrome, domainName, event) {
48+
const eventName = `${domainName}.${event.name}`;
49+
const handler = (sessionId, handler) => {
50+
if (typeof sessionId === "function") {
51+
handler = sessionId;
52+
sessionId = undefined;
53+
}
54+
const rawEventName = sessionId ? `${eventName}.${sessionId}` : eventName;
55+
if (typeof handler === "function") {
56+
chrome.on(rawEventName, handler);
57+
return () => chrome.removeListener(rawEventName, handler);
58+
} else {
59+
return new Promise((fulfill, _reject) => {
60+
chrome.once(rawEventName, fulfill);
61+
});
62+
}
63+
};
64+
decorate(handler, "event", event);
65+
chrome[eventName] = chrome[domainName][event.name] = handler;
66+
}
67+
68+
function addType(chrome, domainName, type) {
69+
const typeName = `${domainName}.${type.id}`;
70+
const help = {};
71+
decorate(help, "type", type);
72+
chrome[typeName] = chrome[domainName][type.id] = help;
73+
}
74+
75+
export function prepare(object, protocol) {
76+
// assign the protocol and generate the shorthands
77+
object.protocol = protocol;
78+
protocol.domains.forEach((domain) => {
79+
const domainName = domain.domain;
80+
object[domainName] = {};
81+
// add commands
82+
(domain.commands || []).forEach((command) => {
83+
addCommand(object, domainName, command);
84+
});
85+
// add events
86+
(domain.events || []).forEach((event) => {
87+
addEvent(object, domainName, event);
88+
});
89+
// add types
90+
(domain.types || []).forEach((type) => {
91+
addType(object, domainName, type);
92+
});
93+
// add utility listener for each domain
94+
object[domainName].on = (eventName, handler) => {
95+
return object[domainName][eventName](handler);
96+
};
97+
});
98+
}

0 commit comments

Comments
 (0)