@opencraw/studio 0.0.34 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +1053 -855
- package/dist/src/index.d.ts +2 -0
- package/dist/src/studio-api/command.contract.d.ts +33 -0
- package/dist/src/studio-api/event.contract.d.ts +17 -0
- package/dist/src/studio-api/index.d.ts +8 -4
- package/dist/src/studio-api/studio-control-view.contract.d.ts +41 -0
- package/dist/src/studio-api/ui-request.contract.d.ts +9 -0
- package/dist/src/studio-instance/index.d.ts +3 -0
- package/dist/src/studio-instance/instance-file.repository.d.ts +14 -0
- package/dist/src/studio-instance/studio-instance.contract.d.ts +15 -0
- package/dist/src/studio-server/ask-ui.handler.d.ts +12 -0
- package/dist/src/studio-server/run-status.handler.d.ts +12 -0
- package/dist/src/studio-server/studio-http.use-case.d.ts +2 -0
- package/dist/src/studio-server/studio-status.handler.d.ts +10 -0
- package/dist/src/studio-server/workspace.store.d.ts +2 -0
- package/dist/studio-api/command.contract.d.ts +33 -0
- package/dist/studio-api/event.contract.d.ts +17 -0
- package/dist/studio-api/index.d.ts +8 -4
- package/dist/studio-api/studio-control-view.contract.d.ts +41 -0
- package/dist/studio-api/ui-request.contract.d.ts +9 -0
- package/dist/studio-instance/index.d.ts +3 -0
- package/dist/studio-instance/instance-file.repository.d.ts +14 -0
- package/dist/studio-instance/studio-instance.contract.d.ts +15 -0
- package/dist/studio-server/ask-ui.handler.d.ts +12 -0
- package/dist/studio-server/run-status.handler.d.ts +12 -0
- package/dist/studio-server/studio-http.use-case.d.ts +2 -0
- package/dist/studio-server/studio-status.handler.d.ts +10 -0
- package/dist/studio-server/workspace.store.d.ts +2 -0
- package/dist/ui/assets/{index-Db6-8OgZ.js → index-cNoQIuRS.js} +29 -29
- package/dist/ui/assets/{pdf-rdp2xUbL.js → pdf-OWIvAVVv.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/package.json +3 -3
package/dist/index.esm.js
CHANGED
|
@@ -3,14 +3,14 @@ import { createServer } from 'node:http';
|
|
|
3
3
|
import { promisify, stripVTControlCharacters } from 'node:util';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import { BODY_KINDS, formatOfResponse, HttpClient, BrowserClient, NODE_ID_ATTRIBUTE, sessionStateOf,
|
|
6
|
+
import { BODY_KINDS, formatOfResponse, HttpClient, BrowserClient, NODE_ID_ATTRIBUTE, sessionStateOf, readRecipeSource, parseInputRecipe, parseOutputRecipe, calloutLabel, bindRecipeSet, memorySink, createCrawler, traceLine, UnknownHookError, rowsOfCells, parseXml, marginRows, findDeckTables, findGridTables, analyzeTables, parseRegion, regionText, deckRegionText, isDeckDocument, resolveMissingPolicy, isWorkbookDocument, RecipeBindingError, RecipeValidationError, countMatches, matchesOf, tryParseJson, isPdfDocument, BrowserProfiles } from '@opencraw/core';
|
|
7
7
|
import { randomBytes, createHash } from 'node:crypto';
|
|
8
8
|
import { execFile } from 'node:child_process';
|
|
9
|
+
import { mkdir, writeFile, chmod, rm, readFile, rename, stat } from 'node:fs/promises';
|
|
10
|
+
import { homedir, tmpdir } from 'node:os';
|
|
9
11
|
import { createRequire } from 'node:module';
|
|
10
12
|
import { load } from 'cheerio';
|
|
11
|
-
import { readFile, writeFile, rm, rename, mkdir, stat } from 'node:fs/promises';
|
|
12
13
|
import { describeJson } from '@opencraw/probe';
|
|
13
|
-
import { tmpdir } from 'node:os';
|
|
14
14
|
import { existsSync, createReadStream } from 'node:fs';
|
|
15
15
|
|
|
16
16
|
const sentencePartSchema = z.object({
|
|
@@ -41,6 +41,14 @@ const outlineViewSchema = z.object({
|
|
|
41
41
|
steps: z.array(outlineNodeSchema)
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
/** What an outside controller (an agent, through the MCP server) can ask the open Studio page to do. Grows one `kind` at a time. */
|
|
45
|
+
const uiRequestSchema = z.discriminatedUnion('kind', [/** Open a folder, a recipe file or a document, exactly as a Studio link does (`open-path`); `workspace` is the folder a document belongs to. */
|
|
46
|
+
z.object({
|
|
47
|
+
kind: z.literal('open'),
|
|
48
|
+
path: z.string().min(1),
|
|
49
|
+
workspace: z.string().min(1).optional()
|
|
50
|
+
})]);
|
|
51
|
+
|
|
44
52
|
/** A budget for a sample run; never authored into a recipe, only asked for by the caller. */
|
|
45
53
|
const sampleBudgetSchema = z.object({
|
|
46
54
|
maxRecords: z.number().int().positive().optional(),
|
|
@@ -373,7 +381,21 @@ const importDocumentCommandSchema = z.object({
|
|
|
373
381
|
name: z.string().min(1),
|
|
374
382
|
bytes: z.string()
|
|
375
383
|
});
|
|
376
|
-
|
|
384
|
+
/** Asks every open Studio page to do something (`ui-request`); the answer says how many pages were listening. How an agent opens a recipe for the person who is watching. */
|
|
385
|
+
const askUiCommandSchema = z.object({
|
|
386
|
+
type: z.literal('ask-ui'),
|
|
387
|
+
request: uiRequestSchema
|
|
388
|
+
});
|
|
389
|
+
/** Asks what Studio is doing: its workspace, how many pages are open and the run in progress. */
|
|
390
|
+
const studioStatusCommandSchema = z.object({
|
|
391
|
+
type: z.literal('studio-status')
|
|
392
|
+
});
|
|
393
|
+
/** Asks how a recipe's sample run is going: still running, finished (with its first records) or never run. */
|
|
394
|
+
const runStatusCommandSchema = z.object({
|
|
395
|
+
type: z.literal('run-status'),
|
|
396
|
+
recipeId: z.string().min(1)
|
|
397
|
+
});
|
|
398
|
+
const studioCommandSchema = z.discriminatedUnion('type', [openWorkspaceCommandSchema, chooseFolderCommandSchema, openPathCommandSchema, importDocumentCommandSchema, runSampleCommandSchema, stopRunCommandSchema, saveRecipeCommandSchema, fetchStartPageCommandSchema, detectUrlCommandSchema, saveOutlineCommandSchema, takeSnapshotCommandSchema, verifySelectorCommandSchema, inferSelectorCommandSchema, explainWhyCommandSchema, inspectPageCommandSchema, responsesSeenCommandSchema, documentTreeCommandSchema, pdfViewCommandSchema, tablePreviewCommandSchema, regionPreviewCommandSchema, gridViewCommandSchema, gridRowsCommandSchema, gridPreviewCommandSchema, deckViewCommandSchema, deckPreviewCommandSchema, startRecordingCommandSchema, stopRecordingCommandSchema, askUiCommandSchema, studioStatusCommandSchema, runStatusCommandSchema]);
|
|
377
399
|
|
|
378
400
|
/**
|
|
379
401
|
* What `take-snapshot` answers with: the rewritten document ready for the
|
|
@@ -531,7 +553,12 @@ const recordingStoppedEventSchema = z.object({
|
|
|
531
553
|
type: z.literal('recording-stopped'),
|
|
532
554
|
steps: z.array(recordedStepSchema)
|
|
533
555
|
});
|
|
534
|
-
|
|
556
|
+
/** A controller (an agent, through the MCP server) asked the open page to do something; every page does it. */
|
|
557
|
+
const uiRequestEventSchema = z.object({
|
|
558
|
+
type: z.literal('ui-request'),
|
|
559
|
+
request: uiRequestSchema
|
|
560
|
+
});
|
|
561
|
+
const studioEventSchema = z.discriminatedUnion('type', [traceLineEventSchema, recordEventSchema, recordRejectedEventSchema, runFinishedEventSchema, workspaceChangedEventSchema, recordingCardEventSchema, recordingNoteEventSchema, recordingStoppedEventSchema, uiRequestEventSchema]);
|
|
535
562
|
|
|
536
563
|
/** One problem found on a recipe file, with the JSON path it lives at. */
|
|
537
564
|
const recipeIssueSchema = z.object({
|
|
@@ -913,6 +940,37 @@ const deckTablePreviewViewSchema = z.object({
|
|
|
913
940
|
error: z.string().optional()
|
|
914
941
|
});
|
|
915
942
|
|
|
943
|
+
/** How many pages were listening when a controller asked them to do something: none means nobody has Studio open. */
|
|
944
|
+
z.object({
|
|
945
|
+
pages: z.number()
|
|
946
|
+
});
|
|
947
|
+
/** What Studio is doing: its workspace, how many pages are open, and the recipe whose sample run is in progress. */
|
|
948
|
+
z.object({
|
|
949
|
+
folder: z.string().optional(),
|
|
950
|
+
pages: z.number(),
|
|
951
|
+
running: z.string().optional()
|
|
952
|
+
});
|
|
953
|
+
const rejectionSchema = z.object({
|
|
954
|
+
field: z.string(),
|
|
955
|
+
reason: z.string(),
|
|
956
|
+
url: z.string()
|
|
957
|
+
});
|
|
958
|
+
/** How a recipe's sample run is going; `result` only once it finished: the counts, the first records and the first rejections (the rest are in Studio). */
|
|
959
|
+
z.object({
|
|
960
|
+
recipeId: z.string(),
|
|
961
|
+
state: z.enum(['running', 'finished', 'never-run']),
|
|
962
|
+
result: z.object({
|
|
963
|
+
emitted: z.number(),
|
|
964
|
+
rejected: z.number(),
|
|
965
|
+
duplicates: z.number(),
|
|
966
|
+
durationMs: z.number(),
|
|
967
|
+
error: z.string().optional(),
|
|
968
|
+
stoppedBy: z.string().optional(),
|
|
969
|
+
records: z.array(sampleRunRecordSchema),
|
|
970
|
+
rejections: z.array(rejectionSchema)
|
|
971
|
+
}).optional()
|
|
972
|
+
});
|
|
973
|
+
|
|
916
974
|
/** The cookie set on the page's own document response, so its own asset and API requests carry the token too. */
|
|
917
975
|
const TOKEN_COOKIE = 'opencraw_token';
|
|
918
976
|
/** A random token, generated once at server startup and required on every request. */
|
|
@@ -1064,30 +1122,41 @@ async function chooseFolderWithDialog(platform = process.platform, run = runDial
|
|
|
1064
1122
|
throw new Error(`there is no folder dialog on this machine${tried === '' ? '' : ` (${tried} is not installed)`}: type the folder instead`);
|
|
1065
1123
|
}
|
|
1066
1124
|
|
|
1067
|
-
/**
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1125
|
+
/** The announcement's file: one per process and port, so two Studios in one process do not share it. */
|
|
1126
|
+
function fileOf(instance, directory) {
|
|
1127
|
+
return join(directory, `${String(instance.pid)}-${new URL(instance.url).port}.json`);
|
|
1128
|
+
}
|
|
1129
|
+
/** Where running Studios announce themselves, unless the caller says otherwise. */
|
|
1130
|
+
function defaultInstancesDirectory() {
|
|
1131
|
+
return join(homedir(), '.opencraw', 'studio');
|
|
1074
1132
|
}
|
|
1075
1133
|
/**
|
|
1076
|
-
*
|
|
1077
|
-
* runs on and answers the folder picked. Only answers: the UI follows with
|
|
1078
|
-
* `open-workspace`, the same as when the folder is typed. A dialog still
|
|
1079
|
-
* open is not opened a second time; the second request waits for the first.
|
|
1134
|
+
* Writes the file that tells controllers this Studio is running. Failing to write it never stops Studio: it only means no agent can find it.
|
|
1080
1135
|
*
|
|
1081
|
-
* @param
|
|
1082
|
-
* @
|
|
1136
|
+
* @param instance - What to announce.
|
|
1137
|
+
* @param directory - Where announcements live.
|
|
1138
|
+
* @returns Whether the file was written.
|
|
1083
1139
|
*/
|
|
1084
|
-
async function
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1140
|
+
async function announceInstance(instance, directory) {
|
|
1141
|
+
try {
|
|
1142
|
+
await mkdir(directory, {
|
|
1143
|
+
recursive: true
|
|
1144
|
+
});
|
|
1145
|
+
const file = fileOf(instance, directory);
|
|
1146
|
+
await writeFile(file, JSON.stringify(instance), {
|
|
1147
|
+
mode: 0o600
|
|
1148
|
+
});
|
|
1149
|
+
await chmod(file, 0o600);
|
|
1150
|
+
return true;
|
|
1151
|
+
} catch {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
/** Removes the announcement, when Studio stops. */
|
|
1156
|
+
async function withdrawInstance(instance, directory) {
|
|
1157
|
+
await rm(fileOf(instance, directory), {
|
|
1158
|
+
force: true
|
|
1159
|
+
});
|
|
1091
1160
|
}
|
|
1092
1161
|
|
|
1093
1162
|
/** How much of the start of a response is looked at: enough for the engine to tell a PDF or an Office package from its bytes, and for JSON to start. */
|
|
@@ -1980,924 +2049,1000 @@ async function savedRecipe(file) {
|
|
|
1980
2049
|
}
|
|
1981
2050
|
}
|
|
1982
2051
|
|
|
1983
|
-
/** A cell reads as an ISO date (`2026-06-01`), an ISO moment (`2026-06-01T09:30:00`) or a bare time of day (`09:30:00`) — the three shapes `workbook-document`'s `read-xlsx.client.ts` writes for a spreadsheet date. */
|
|
1984
|
-
const ISO_DATE_OR_TIME = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2})?$|^\d{2}:\d{2}:\d{2}$/;
|
|
1985
2052
|
/**
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1988
|
-
* second read of the file): every sheet's cells, typed, with hidden sheets
|
|
1989
|
-
* and rows marked, and merged ranges resolved to bounds so the grid draws
|
|
1990
|
-
* them as one cell. Table-specific diagnostics (the matched rows of the
|
|
1991
|
-
* current `table` extract's options) are not here — they depend on the
|
|
1992
|
-
* picks so far, so they come from `table-preview.use-case.ts`'s
|
|
1993
|
-
* `previewGridTable` instead, computed live as those options change.
|
|
2053
|
+
* Finds and parses one input recipe of a workspace folder, and the output
|
|
2054
|
+
* recipe it names.
|
|
1994
2055
|
*
|
|
1995
|
-
* @param
|
|
1996
|
-
* @
|
|
2056
|
+
* @param folder - The workspace folder.
|
|
2057
|
+
* @param recipeId - The input recipe's id.
|
|
2058
|
+
* @returns The parsed pair.
|
|
2059
|
+
* @throws RecipeValidationError when either recipe does not parse.
|
|
2060
|
+
* @throws Error when the input or its output is not in the folder.
|
|
1997
2061
|
*/
|
|
1998
|
-
function
|
|
2062
|
+
async function loadRecipePair(folder, recipeId) {
|
|
2063
|
+
const documents = await readRecipeSource(folder);
|
|
2064
|
+
const inputDocument = documents.find(document => kindOf(document.content) === 'input' && idOf(document.content) === recipeId);
|
|
2065
|
+
if (inputDocument === undefined) throw new Error(`no input recipe "${recipeId}" in this workspace`);
|
|
2066
|
+
const input = parseInputRecipe(inputDocument.content, inputDocument.source);
|
|
2067
|
+
const outputDocument = documents.find(document => kindOf(document.content) === 'output' && idOf(document.content) === input.output);
|
|
2068
|
+
if (outputDocument === undefined) throw new Error(`no output recipe "${input.output}" in this workspace`);
|
|
1999
2069
|
return {
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
csv: document.csv
|
|
2003
|
-
})
|
|
2070
|
+
input,
|
|
2071
|
+
output: parseOutputRecipe(outputDocument.content, outputDocument.source)
|
|
2004
2072
|
};
|
|
2005
2073
|
}
|
|
2006
|
-
function
|
|
2007
|
-
|
|
2008
|
-
return {
|
|
2009
|
-
name: sheet.name,
|
|
2010
|
-
hidden: sheet.hidden === true,
|
|
2011
|
-
hiddenRows: sheet.hiddenRows === undefined ? [] : [...sheet.hiddenRows],
|
|
2012
|
-
// Not `Math.max(...lengths)`: a spread of a few hundred thousand rows overflows the call stack.
|
|
2013
|
-
columnCount: sheet.rows.reduce((widest, row) => Math.max(widest, row.length), 0),
|
|
2014
|
-
rows: (windowed ? sheet.rows.slice(0, windowRows) : sheet.rows).map(row => row.map(cell => cellView(cell))),
|
|
2015
|
-
...(windowed && {
|
|
2016
|
-
rowCount: sheet.rows.length
|
|
2017
|
-
}),
|
|
2018
|
-
merges: (sheet.merges ?? []).flatMap(reference => {
|
|
2019
|
-
const range = rangeOf(reference);
|
|
2020
|
-
return range === undefined ? [] : [{
|
|
2021
|
-
ref: reference,
|
|
2022
|
-
...range
|
|
2023
|
-
}];
|
|
2024
|
-
}),
|
|
2025
|
-
...(sheet.columnWidths !== undefined && {
|
|
2026
|
-
columnWidths: {
|
|
2027
|
-
...sheet.columnWidths
|
|
2028
|
-
}
|
|
2029
|
-
}),
|
|
2030
|
-
...(sheet.rowHeights !== undefined && {
|
|
2031
|
-
rowHeights: {
|
|
2032
|
-
...sheet.rowHeights
|
|
2033
|
-
}
|
|
2034
|
-
}),
|
|
2035
|
-
...(sheet.defaultColumnWidth !== undefined && {
|
|
2036
|
-
defaultColumnWidth: sheet.defaultColumnWidth
|
|
2037
|
-
}),
|
|
2038
|
-
...(sheet.defaultRowHeight !== undefined && {
|
|
2039
|
-
defaultRowHeight: sheet.defaultRowHeight
|
|
2040
|
-
})
|
|
2041
|
-
};
|
|
2074
|
+
function kindOf(content) {
|
|
2075
|
+
return content?.kind;
|
|
2042
2076
|
}
|
|
2077
|
+
function idOf(content) {
|
|
2078
|
+
return content?.id;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2043
2081
|
/**
|
|
2044
|
-
*
|
|
2045
|
-
* sent to the browser until it is needed).
|
|
2082
|
+
* The hooks a sample run uses: the ones Studio was started with, except that
|
|
2046
2083
|
*
|
|
2047
|
-
*
|
|
2048
|
-
*
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
2051
|
-
* @
|
|
2084
|
+
* - a hook the person stubbed returns the stub's value and calls nothing, and
|
|
2085
|
+
* - a hook that calls outside the process says so in the trace, once per call, because it may be slow
|
|
2086
|
+
* and it may cost money.
|
|
2087
|
+
*
|
|
2088
|
+
* @param hooks - The hooks Studio was started with.
|
|
2089
|
+
* @param stubs - Values to answer with instead of calling a hook, by hook name. A name that is not a hook is ignored.
|
|
2090
|
+
* @param announce - Where the trace lines go.
|
|
2091
|
+
* @returns The hooks to run the sample with.
|
|
2052
2092
|
*/
|
|
2053
|
-
function
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
const start = cellOf(from);
|
|
2073
|
-
const end = cellOf(to);
|
|
2074
|
-
if (start === undefined || end === undefined) return undefined;
|
|
2075
|
-
return {
|
|
2076
|
-
top: Math.min(start.row, end.row),
|
|
2077
|
-
left: Math.min(start.column, end.column),
|
|
2078
|
-
bottom: Math.max(start.row, end.row),
|
|
2079
|
-
right: Math.max(start.column, end.column)
|
|
2080
|
-
};
|
|
2081
|
-
}
|
|
2082
|
-
function cellOf(reference) {
|
|
2083
|
-
const match = /^\$?([A-Z]+)\$?(\d+)$/i.exec(reference.trim());
|
|
2084
|
-
if (match === null) return undefined;
|
|
2085
|
-
const letters = match[1].toUpperCase();
|
|
2086
|
-
let column = 0;
|
|
2087
|
-
for (const char of letters) column = column * 26 + (char.codePointAt(0) ?? 64) - 64;
|
|
2088
|
-
return {
|
|
2089
|
-
row: Number(match[2]) - 1,
|
|
2090
|
-
column: column - 1
|
|
2091
|
-
};
|
|
2093
|
+
function calloutHooks(hooks, stubs, announce) {
|
|
2094
|
+
if (hooks === undefined) return undefined;
|
|
2095
|
+
const prepared = {};
|
|
2096
|
+
for (const [name, hook] of Object.entries(hooks)) {
|
|
2097
|
+
if (stubs !== undefined && Object.hasOwn(stubs, name)) {
|
|
2098
|
+
const stub = stubs[name];
|
|
2099
|
+
prepared[name] = () => {
|
|
2100
|
+
announce(`☎ hook "${name}" is stubbed: answering ${JSON.stringify(stub) ?? 'undefined'} without calling it`);
|
|
2101
|
+
return stub;
|
|
2102
|
+
};
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
const label = calloutLabel(hook);
|
|
2106
|
+
prepared[name] = label === undefined ? hook : async (input, args, context) => {
|
|
2107
|
+
announce(`☎ hook "${name}" calls ${label} (slow, and it may cost: stub it to skip the call)`);
|
|
2108
|
+
return await hook(input, args, context);
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
return prepared;
|
|
2092
2112
|
}
|
|
2093
2113
|
|
|
2094
2114
|
/**
|
|
2095
|
-
*
|
|
2096
|
-
*
|
|
2097
|
-
*
|
|
2098
|
-
*
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2102
|
-
* `
|
|
2103
|
-
*
|
|
2104
|
-
*
|
|
2115
|
+
* Runs one input recipe of a workspace folder as a sample: `debug: true` (so
|
|
2116
|
+
* each `record:emit` carries its scope snapshot and mapping trace, and each
|
|
2117
|
+
* `record:reject` its scope), an in-memory sink, and `budget` as the
|
|
2118
|
+
* crawler's `sample` (`@opencraw/core`'s `CrawlOptions.sample`, #89). Every
|
|
2119
|
+
* engine event becomes a trace line with the same `traceLine` formatter the
|
|
2120
|
+
* cli's `--trace` uses; every emitted record reaches `callbacks.onRecord` as
|
|
2121
|
+
* it happens, not only in the final result. Step events (`step:start`,
|
|
2122
|
+
* `step:skip`, `step:retry`) are aggregated by path into `result.steps`, so
|
|
2123
|
+
* `explain-why` (#92) can later say whether the step that bound a mapping's
|
|
2124
|
+
* source id ran, was skipped, or retried — without a second run.
|
|
2105
2125
|
*
|
|
2106
|
-
* @param
|
|
2107
|
-
* @
|
|
2126
|
+
* @param folder - The workspace folder the recipe lives in.
|
|
2127
|
+
* @param recipeId - The input recipe to run.
|
|
2128
|
+
* @param budget - The sample budget; unset runs the recipe to completion (still capped by its own `limits`).
|
|
2129
|
+
* @param callbacks - Where trace lines and records go as the run proceeds.
|
|
2130
|
+
* @param browser - Browser launch settings for a web-mode recipe (an executable path override, headless…); the server-wide setting `studio-http.use-case.ts`'s `StudioServerOptions.browser` carries in, e.g. `OPENCRAW_CHROMIUM` in a sandbox with no full Playwright install.
|
|
2131
|
+
* @param plugins - Hooks, access plugins and captcha solvers the person started Studio with (`--hooks`, issue #150); none when it was started without.
|
|
2132
|
+
* @param stubs - Values a hook answers with instead of being called, by hook name, for this run only (issue #201): how a sample skips a slow or paid callout.
|
|
2133
|
+
* @returns A handle: `stop` closes the crawler cleanly, `result` resolves when the run ends.
|
|
2134
|
+
* @throws RecipeValidationError, RecipeBindingError, Error when the recipe or its output cannot be loaded or bound.
|
|
2108
2135
|
*/
|
|
2109
|
-
function
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2136
|
+
async function runSample(folder, recipeId, budget, callbacks, browser, plugins, stubs) {
|
|
2137
|
+
const {
|
|
2138
|
+
input,
|
|
2139
|
+
output
|
|
2140
|
+
} = await loadRecipePair(folder, recipeId);
|
|
2141
|
+
const set = bindRecipeSet(output, [input]);
|
|
2142
|
+
const sink = memorySink();
|
|
2143
|
+
const records = [];
|
|
2144
|
+
const rejectedRecords = [];
|
|
2145
|
+
const steps = new Map();
|
|
2146
|
+
const crawler = createCrawler({
|
|
2147
|
+
sink,
|
|
2148
|
+
debug: true,
|
|
2149
|
+
sample: budget,
|
|
2150
|
+
browser,
|
|
2151
|
+
hooks: calloutHooks(plugins?.hooks, stubs, callbacks.onTraceLine),
|
|
2152
|
+
accessPlugins: plugins?.accessPlugins,
|
|
2153
|
+
captchaSolvers: plugins?.captchaSolvers,
|
|
2154
|
+
onEvent: event => {
|
|
2155
|
+
const line = traceLine(event);
|
|
2156
|
+
if (line !== undefined) callbacks.onTraceLine(line);
|
|
2157
|
+
trackStep(steps, event);
|
|
2158
|
+
if (event.type === 'record:emit') {
|
|
2159
|
+
const record = {
|
|
2160
|
+
key: event.key,
|
|
2161
|
+
data: event.data,
|
|
2162
|
+
scope: event.scope,
|
|
2163
|
+
mapping: event.mapping
|
|
2164
|
+
};
|
|
2165
|
+
records.push(record);
|
|
2166
|
+
callbacks.onRecord(record);
|
|
2167
|
+
} else if (event.type === 'record:reject') {
|
|
2168
|
+
const rejected = {
|
|
2169
|
+
field: event.field,
|
|
2170
|
+
reason: event.reason,
|
|
2171
|
+
url: event.url,
|
|
2172
|
+
scope: event.scope
|
|
2173
|
+
};
|
|
2174
|
+
rejectedRecords.push(rejected);
|
|
2175
|
+
callbacks.onRejected?.(rejected);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
const result = runToResult(crawler, set, recipeId, records, rejectedRecords, steps, plugins);
|
|
2145
2180
|
return {
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
title: chart.title
|
|
2149
|
-
}),
|
|
2150
|
-
series: chart.series.map(series => ({
|
|
2151
|
-
name: series.name,
|
|
2152
|
-
categories: [...series.categories],
|
|
2153
|
-
values: [...series.values]
|
|
2154
|
-
}))
|
|
2181
|
+
stop: () => crawler.close(),
|
|
2182
|
+
result
|
|
2155
2183
|
};
|
|
2156
2184
|
}
|
|
2157
2185
|
/**
|
|
2158
|
-
*
|
|
2159
|
-
*
|
|
2160
|
-
*
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
2163
|
-
*
|
|
2164
|
-
*
|
|
2165
|
-
* @param shapes - The slide's shapes, in reading order.
|
|
2166
|
-
* @param slideHeight - The deck's slide height, in points (`DeckDocument.height`).
|
|
2167
|
-
* @returns Each row's shape indices, left to right; a blank shape (whitespace-only text) is never part of a row.
|
|
2186
|
+
* Folds one `step:*` event into `steps`, by path: `skip` always wins over an
|
|
2187
|
+
* earlier `ran`/`retried` outcome for the same path (a step skipped even
|
|
2188
|
+
* once is worth flagging), `retry` wins over a plain `ran`, and a path seen
|
|
2189
|
+
* only as `finish`/`start` (no skip, no retry) settles on `ran`. A `forEach`
|
|
2190
|
+
* runs its body once per item, so the same path folds repeatedly; this is
|
|
2191
|
+
* deliberately a summary across the whole sample, not a per-record trace.
|
|
2168
2192
|
*/
|
|
2169
|
-
function
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2193
|
+
function trackStep(steps, event) {
|
|
2194
|
+
if (event.type === 'step:start') {
|
|
2195
|
+
if (!steps.has(event.path)) steps.set(event.path, {
|
|
2196
|
+
path: event.path,
|
|
2197
|
+
stepType: event.stepType,
|
|
2198
|
+
stepId: event.stepId,
|
|
2199
|
+
outcome: 'ran'
|
|
2200
|
+
});
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
if (event.type === 'step:retry') {
|
|
2204
|
+
steps.set(event.path, {
|
|
2205
|
+
path: event.path,
|
|
2206
|
+
stepType: event.stepType,
|
|
2207
|
+
stepId: event.stepId,
|
|
2208
|
+
outcome: 'retried',
|
|
2209
|
+
error: event.error
|
|
2210
|
+
});
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (event.type === 'step:skip') {
|
|
2214
|
+
steps.set(event.path, {
|
|
2215
|
+
path: event.path,
|
|
2216
|
+
stepType: event.stepType,
|
|
2217
|
+
stepId: event.stepId,
|
|
2218
|
+
outcome: 'skipped',
|
|
2219
|
+
error: event.error
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2185
2222
|
}
|
|
2186
|
-
|
|
2187
|
-
const TEXT_LIMIT$1 = 120;
|
|
2188
2223
|
/**
|
|
2189
|
-
*
|
|
2190
|
-
*
|
|
2191
|
-
*
|
|
2192
|
-
*
|
|
2193
|
-
* @param body - The parsed body (`HttpBody`); YAML and JSON Lines both
|
|
2194
|
-
* arrive here already normalised to `{ kind: 'json' }` by `http.client.ts`.
|
|
2195
|
-
* @returns The tree.
|
|
2196
|
-
* @throws Error when `body`'s kind is not `json` or `xml` (PDF, workbook and
|
|
2197
|
-
* deck documents get their own canvases — 5b/5c/5d, not this mapper).
|
|
2224
|
+
* Awaits the run and shapes its result. A throw from `crawler.run` (an unknown
|
|
2225
|
+
* hook, a recipe that cannot start) becomes the run's `error` instead of a
|
|
2226
|
+
* rejection: nothing awaits `result` but a `.then`, and an unhandled rejection
|
|
2227
|
+
* would take the whole Studio server down (issue #148).
|
|
2198
2228
|
*/
|
|
2199
|
-
function
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
};
|
|
2204
|
-
if (body.kind === 'xml') return xmlTreeView(body.xml);
|
|
2205
|
-
throw new Error(`document-tree: "${body.kind}" has no tree canvas (only "json" and "xml" bodies do)`);
|
|
2206
|
-
}
|
|
2207
|
-
function listFields(path, context) {
|
|
2208
|
-
if (context === undefined) return {};
|
|
2209
|
-
return {
|
|
2210
|
-
listPath: context.generalPrefix + path.slice(context.itemPrefix.length),
|
|
2211
|
-
listCount: context.count
|
|
2212
|
-
};
|
|
2213
|
-
}
|
|
2214
|
-
function jsonNode(value, path, label, context) {
|
|
2215
|
-
if (Array.isArray(value)) {
|
|
2216
|
-
const children = value.map((item, index) => {
|
|
2217
|
-
const itemPath = `${path}[${index}]`;
|
|
2218
|
-
return jsonNode(item, itemPath, String(index), {
|
|
2219
|
-
itemPrefix: itemPath,
|
|
2220
|
-
generalPrefix: `${path}[*]`,
|
|
2221
|
-
count: value.length
|
|
2222
|
-
});
|
|
2223
|
-
});
|
|
2229
|
+
async function runToResult(crawler, set, recipeId, records, rejectedRecords, steps, plugins) {
|
|
2230
|
+
try {
|
|
2231
|
+
const report = await crawler.run(set);
|
|
2232
|
+
const recipe = report.recipes[0];
|
|
2224
2233
|
return {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2234
|
+
recipeId: recipe.recipeId,
|
|
2235
|
+
emitted: recipe.emitted,
|
|
2236
|
+
rejected: recipe.rejected,
|
|
2237
|
+
duplicates: recipe.duplicates,
|
|
2238
|
+
durationMs: recipe.durationMs,
|
|
2239
|
+
error: recipe.error,
|
|
2240
|
+
stoppedBy: recipe.stoppedBy,
|
|
2241
|
+
records,
|
|
2242
|
+
rejectedRecords,
|
|
2243
|
+
// eslint-disable-next-line unicorn/prefer-iterator-to-array -- `.toArray()` needs a `lib` newer than this repo's `es2022` (tsconfig.base.json)
|
|
2244
|
+
steps: [...steps.values()]
|
|
2231
2245
|
};
|
|
2232
|
-
}
|
|
2233
|
-
if (value !== null && typeof value === 'object') {
|
|
2234
|
-
const entries = Object.entries(value);
|
|
2235
|
-
// A plain object passes its own context through unchanged: only an array (or, in XML, a repeated element) starts a new one.
|
|
2236
|
-
const children = entries.map(([key, item]) => jsonNode(item, `${path}${propertyAccess(key)}`, key, context));
|
|
2246
|
+
} catch (error) {
|
|
2237
2247
|
return {
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2248
|
+
recipeId,
|
|
2249
|
+
emitted: 0,
|
|
2250
|
+
rejected: 0,
|
|
2251
|
+
duplicates: 0,
|
|
2252
|
+
durationMs: 0,
|
|
2253
|
+
error: failureMessage(error, plugins),
|
|
2254
|
+
records,
|
|
2255
|
+
rejectedRecords,
|
|
2256
|
+
// eslint-disable-next-line unicorn/prefer-iterator-to-array -- `.toArray()` needs a `lib` newer than this repo's `es2022` (tsconfig.base.json)
|
|
2257
|
+
steps: [...steps.values()]
|
|
2244
2258
|
};
|
|
2259
|
+
} finally {
|
|
2260
|
+
await crawler.close();
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
/** What a run that could not start reports. A hook is the author's own code, which Studio only runs when it was started with `--hooks` (issue #150): say which case this is, instead of leaving "unknown hook" to read as a typo. */
|
|
2264
|
+
function failureMessage(error, plugins) {
|
|
2265
|
+
if (error instanceof UnknownHookError) {
|
|
2266
|
+
const message = stripVTControlCharacters(error.message);
|
|
2267
|
+
if (plugins !== undefined) return `${message}. It is not one of the hooks in ${plugins.source}: ${Object.keys(plugins.hooks).join(', ') || 'none exported'}.`;
|
|
2268
|
+
return `${message}. Studio was started without hooks: restart it with \`opencraw studio --hooks <file>\` (a module you trust; it runs as your code), or temporarily replace the hook step to preview the rest.`;
|
|
2245
2269
|
}
|
|
2270
|
+
if (error instanceof Error) return stripVTControlCharacters(error.message);
|
|
2271
|
+
return stripVTControlCharacters(String(error));
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/** @returns A fresh, empty cache. */
|
|
2275
|
+
function createLastRunCache() {
|
|
2246
2276
|
return {
|
|
2247
|
-
|
|
2248
|
-
label,
|
|
2249
|
-
valueType: leafType(value),
|
|
2250
|
-
jsonpath: path,
|
|
2251
|
-
preview: previewOf(value),
|
|
2252
|
-
...listFields(path, context),
|
|
2253
|
-
children: []
|
|
2277
|
+
entries: new Map()
|
|
2254
2278
|
};
|
|
2255
2279
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2280
|
+
/** Records a finished sample run, replacing whatever this recipe's cache entry held before. */
|
|
2281
|
+
function recordLastRun(cache, recipeId, result) {
|
|
2282
|
+
cache.entries.set(recipeId, {
|
|
2283
|
+
recipeId,
|
|
2284
|
+
result
|
|
2285
|
+
});
|
|
2262
2286
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
return
|
|
2287
|
+
/** @returns The recipe's last completed sample run, or `undefined` when none has finished yet. */
|
|
2288
|
+
function lastRunOf(cache, recipeId) {
|
|
2289
|
+
return cache.entries.get(recipeId);
|
|
2266
2290
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2291
|
+
|
|
2292
|
+
/** Where the studio's own recorder profile lives when `StudioServerOptions.recorderProfileDir` is not given: never a crawl's own `session.browserProfile` directory, never the person's default browser profile. */
|
|
2293
|
+
function defaultRecorderProfileDir() {
|
|
2294
|
+
return join(tmpdir(), 'opencraw-studio', 'recorder-profile');
|
|
2270
2295
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
function xmlTreeView(xml) {
|
|
2274
|
-
const document = parseXml(xml, 'document.xml');
|
|
2275
|
-
const root = document.documentElement;
|
|
2276
|
-
if (root === null) throw new Error('document-tree: the XML document has no root element');
|
|
2277
|
-
const namespaces = declaredNamespaces(root);
|
|
2278
|
-
const tree = xmlNode(root, '', namespaces, undefined);
|
|
2296
|
+
/** @returns Fresh state for a new server instance: no workspace open, no run, no recording, no sockets, an empty snapshot cache, no last runs, Playwright's own default browser, the default recorder profile directory. */
|
|
2297
|
+
function createStudioState() {
|
|
2279
2298
|
return {
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
})
|
|
2299
|
+
sockets: new Set(),
|
|
2300
|
+
snapshots: createSnapshotCache(),
|
|
2301
|
+
lastRuns: createLastRunCache(),
|
|
2302
|
+
recorderProfileDir: defaultRecorderProfileDir()
|
|
2285
2303
|
};
|
|
2286
2304
|
}
|
|
2287
|
-
/**
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2305
|
+
/**
|
|
2306
|
+
* Sends one event to every connected client. A socket that errors while
|
|
2307
|
+
* sending (gone away without a clean close yet reaching us) is dropped
|
|
2308
|
+
* rather than left to fail every later broadcast too.
|
|
2309
|
+
*
|
|
2310
|
+
* @param state - The server state holding the connected sockets.
|
|
2311
|
+
* @param event - The event to send.
|
|
2312
|
+
*/
|
|
2313
|
+
function broadcast(state, event) {
|
|
2314
|
+
const text = JSON.stringify(event);
|
|
2315
|
+
for (const socket of state.sockets) {
|
|
2316
|
+
try {
|
|
2317
|
+
socket.send(text);
|
|
2318
|
+
} catch {
|
|
2319
|
+
state.sockets.delete(socket);
|
|
2295
2320
|
}
|
|
2296
2321
|
}
|
|
2297
|
-
return namespaces;
|
|
2298
2322
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2323
|
+
|
|
2324
|
+
/**
|
|
2325
|
+
* Handles `ask-ui`: tells every open page what a controller (an agent, through the MCP server) asked for, so
|
|
2326
|
+
* the person watching sees it happen. Nothing is done server-side: the page does it the way a click would.
|
|
2327
|
+
*
|
|
2328
|
+
* @param state - The server state holding the connected pages.
|
|
2329
|
+
* @param command - What to ask the pages.
|
|
2330
|
+
* @returns How many pages were listening; 0 means nobody has Studio open.
|
|
2331
|
+
*/
|
|
2332
|
+
function handleAskUi(state, command) {
|
|
2333
|
+
broadcast(state, {
|
|
2334
|
+
type: 'ui-request',
|
|
2335
|
+
request: command.request
|
|
2336
|
+
});
|
|
2337
|
+
return {
|
|
2338
|
+
pages: state.sockets.size
|
|
2339
|
+
};
|
|
2303
2340
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2341
|
+
|
|
2342
|
+
/** Asks the dialog, and clears `openDialog` once it has answered or failed, so the next request opens a new one. */
|
|
2343
|
+
async function askOnce(state, ask) {
|
|
2344
|
+
try {
|
|
2345
|
+
return await ask();
|
|
2346
|
+
} finally {
|
|
2347
|
+
state.openDialog = undefined;
|
|
2348
|
+
}
|
|
2307
2349
|
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
const
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2350
|
+
/**
|
|
2351
|
+
* Handles `choose-folder`: opens the folder dialog of the machine the server
|
|
2352
|
+
* runs on and answers the folder picked. Only answers: the UI follows with
|
|
2353
|
+
* `open-workspace`, the same as when the folder is typed. A dialog still
|
|
2354
|
+
* open is not opened a second time; the second request waits for the first.
|
|
2355
|
+
*
|
|
2356
|
+
* @param state - The server state holding the dialog to use.
|
|
2357
|
+
* @returns The folder, or none when the dialog was closed.
|
|
2358
|
+
*/
|
|
2359
|
+
async function handleChooseFolder(state) {
|
|
2360
|
+
if (state.chooseFolder === undefined) throw new Error('this server has no folder dialog: type the folder instead');
|
|
2361
|
+
state.openDialog ??= askOnce(state, state.chooseFolder);
|
|
2362
|
+
const folder = await state.openDialog;
|
|
2363
|
+
return folder === undefined ? {} : {
|
|
2364
|
+
folder
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
/** A cell reads as an ISO date (`2026-06-01`), an ISO moment (`2026-06-01T09:30:00`) or a bare time of day (`09:30:00`) — the three shapes `workbook-document`'s `read-xlsx.client.ts` writes for a spreadsheet date. */
|
|
2369
|
+
const ISO_DATE_OR_TIME = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2})?$|^\d{2}:\d{2}:\d{2}$/;
|
|
2370
|
+
/**
|
|
2371
|
+
* Builds the grid canvas's view model (studio plan §3.4, issue #94's 5c) from
|
|
2372
|
+
* an already-read `WorkbookDocument` (`take-snapshot`'s cached body — no
|
|
2373
|
+
* second read of the file): every sheet's cells, typed, with hidden sheets
|
|
2374
|
+
* and rows marked, and merged ranges resolved to bounds so the grid draws
|
|
2375
|
+
* them as one cell. Table-specific diagnostics (the matched rows of the
|
|
2376
|
+
* current `table` extract's options) are not here — they depend on the
|
|
2377
|
+
* picks so far, so they come from `table-preview.use-case.ts`'s
|
|
2378
|
+
* `previewGridTable` instead, computed live as those options change.
|
|
2379
|
+
*
|
|
2380
|
+
* @param document - The read workbook (a spreadsheet or a CSV).
|
|
2381
|
+
* @returns The view.
|
|
2382
|
+
*/
|
|
2383
|
+
function workbookDocumentView(document, options = {}) {
|
|
2325
2384
|
return {
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2385
|
+
sheets: document.sheets.map(sheet => sheetView(sheet, options.windowRows)),
|
|
2386
|
+
...(document.csv !== undefined && {
|
|
2387
|
+
csv: document.csv
|
|
2388
|
+
})
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
function sheetView(sheet, windowRows) {
|
|
2392
|
+
const windowed = windowRows !== undefined && sheet.rows.length > windowRows;
|
|
2393
|
+
return {
|
|
2394
|
+
name: sheet.name,
|
|
2395
|
+
hidden: sheet.hidden === true,
|
|
2396
|
+
hiddenRows: sheet.hiddenRows === undefined ? [] : [...sheet.hiddenRows],
|
|
2397
|
+
// Not `Math.max(...lengths)`: a spread of a few hundred thousand rows overflows the call stack.
|
|
2398
|
+
columnCount: sheet.rows.reduce((widest, row) => Math.max(widest, row.length), 0),
|
|
2399
|
+
rows: (windowed ? sheet.rows.slice(0, windowRows) : sheet.rows).map(row => row.map(cell => cellView(cell))),
|
|
2400
|
+
...(windowed && {
|
|
2401
|
+
rowCount: sheet.rows.length
|
|
2333
2402
|
}),
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2403
|
+
merges: (sheet.merges ?? []).flatMap(reference => {
|
|
2404
|
+
const range = rangeOf(reference);
|
|
2405
|
+
return range === undefined ? [] : [{
|
|
2406
|
+
ref: reference,
|
|
2407
|
+
...range
|
|
2408
|
+
}];
|
|
2337
2409
|
}),
|
|
2338
|
-
|
|
2410
|
+
...(sheet.columnWidths !== undefined && {
|
|
2411
|
+
columnWidths: {
|
|
2412
|
+
...sheet.columnWidths
|
|
2413
|
+
}
|
|
2414
|
+
}),
|
|
2415
|
+
...(sheet.rowHeights !== undefined && {
|
|
2416
|
+
rowHeights: {
|
|
2417
|
+
...sheet.rowHeights
|
|
2418
|
+
}
|
|
2419
|
+
}),
|
|
2420
|
+
...(sheet.defaultColumnWidth !== undefined && {
|
|
2421
|
+
defaultColumnWidth: sheet.defaultColumnWidth
|
|
2422
|
+
}),
|
|
2423
|
+
...(sheet.defaultRowHeight !== undefined && {
|
|
2424
|
+
defaultRowHeight: sheet.defaultRowHeight
|
|
2425
|
+
})
|
|
2339
2426
|
};
|
|
2340
2427
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2428
|
+
/**
|
|
2429
|
+
* A run of one sheet's rows, typed, for a grid that only holds the screens around where it is scrolled to (the rest of a big file is never
|
|
2430
|
+
* sent to the browser until it is needed).
|
|
2431
|
+
*
|
|
2432
|
+
* @param document - The read workbook.
|
|
2433
|
+
* @param sheetIndex - Which sheet, 0-based.
|
|
2434
|
+
* @param from - The first row, 0-based.
|
|
2435
|
+
* @param count - How many rows at most.
|
|
2436
|
+
* @returns The rows (fewer at the end of the sheet, none past it).
|
|
2437
|
+
*/
|
|
2438
|
+
function workbookSheetRows(document, sheetIndex, from, count) {
|
|
2439
|
+
const sheet = document.sheets[sheetIndex];
|
|
2440
|
+
if (sheet === undefined) return [];
|
|
2441
|
+
return sheet.rows.slice(from, from + count).map(row => row.map(cell => cellView(cell)));
|
|
2442
|
+
}
|
|
2443
|
+
function cellView(cell) {
|
|
2343
2444
|
return {
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
valueType: 'string',
|
|
2347
|
-
xpath: path,
|
|
2348
|
-
preview: previewOf(value),
|
|
2349
|
-
...listFields(path, context),
|
|
2350
|
-
children: []
|
|
2445
|
+
value: cell,
|
|
2446
|
+
type: cellType(cell)
|
|
2351
2447
|
};
|
|
2352
2448
|
}
|
|
2353
|
-
function
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
let index = 1;
|
|
2358
|
-
let sibling = element.previousSibling;
|
|
2359
|
-
while (sibling !== null) {
|
|
2360
|
-
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) index += 1;
|
|
2361
|
-
sibling = sibling.previousSibling;
|
|
2362
|
-
}
|
|
2363
|
-
return index;
|
|
2449
|
+
function cellType(cell) {
|
|
2450
|
+
if (typeof cell === 'number') return 'number';
|
|
2451
|
+
if (typeof cell === 'boolean') return 'boolean';
|
|
2452
|
+
return ISO_DATE_OR_TIME.test(cell) ? 'date' : 'string';
|
|
2364
2453
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2454
|
+
/** `B10:B13` (or a single cell, `A1`) as 0-based inclusive bounds; `undefined` for anything else — mirrors `@opencraw/core`'s own (unexported) `rangeOf`/`cellOf` in `grid-table.algorithm.ts`: a small pure parse, kept here rather than reached for across the package boundary (a sibling slice's non-`index.ts` file is never imported, and this is `@opencraw/core`'s own private helper, not even on its barrel). */
|
|
2455
|
+
function rangeOf(reference) {
|
|
2456
|
+
const [from, to = from] = reference.split(':', 2);
|
|
2457
|
+
const start = cellOf(from);
|
|
2458
|
+
const end = cellOf(to);
|
|
2459
|
+
if (start === undefined || end === undefined) return undefined;
|
|
2460
|
+
return {
|
|
2461
|
+
top: Math.min(start.row, end.row),
|
|
2462
|
+
left: Math.min(start.column, end.column),
|
|
2463
|
+
bottom: Math.max(start.row, end.row),
|
|
2464
|
+
right: Math.max(start.column, end.column)
|
|
2465
|
+
};
|
|
2369
2466
|
}
|
|
2370
|
-
function
|
|
2371
|
-
const
|
|
2372
|
-
|
|
2467
|
+
function cellOf(reference) {
|
|
2468
|
+
const match = /^\$?([A-Z]+)\$?(\d+)$/i.exec(reference.trim());
|
|
2469
|
+
if (match === null) return undefined;
|
|
2470
|
+
const letters = match[1].toUpperCase();
|
|
2471
|
+
let column = 0;
|
|
2472
|
+
for (const char of letters) column = column * 26 + (char.codePointAt(0) ?? 64) - 64;
|
|
2473
|
+
return {
|
|
2474
|
+
row: Number(match[2]) - 1,
|
|
2475
|
+
column: column - 1
|
|
2476
|
+
};
|
|
2373
2477
|
}
|
|
2374
2478
|
|
|
2375
2479
|
/**
|
|
2376
|
-
* Builds the
|
|
2377
|
-
* an already-read `
|
|
2378
|
-
* read of the file): every
|
|
2379
|
-
*
|
|
2380
|
-
*
|
|
2381
|
-
*
|
|
2382
|
-
*
|
|
2383
|
-
*
|
|
2480
|
+
* Builds the deck canvas's view model (studio plan §3.4, issue #94's 5d)
|
|
2481
|
+
* from an already-read `DeckDocument` (`take-snapshot`'s cached body — no
|
|
2482
|
+
* second read of the file): every slide's shapes (with their visual-row
|
|
2483
|
+
* grouping, for the text-box-grid pick), native tables (through
|
|
2484
|
+
* `workbook-view.mapper.ts`'s own sheet view — a deck table *is* a sheet),
|
|
2485
|
+
* charts and notes. Table-specific diagnostics for a `table` extract's own
|
|
2486
|
+
* options (the live preview) are not here — they come from
|
|
2487
|
+
* `table-preview.use-case.ts`'s `previewDeckTable` instead, computed live as
|
|
2488
|
+
* those options change, the same split `pdf-view.mapper.ts`/`workbook-view.mapper.ts`
|
|
2489
|
+
* already use.
|
|
2384
2490
|
*
|
|
2385
|
-
* @param document - The read
|
|
2491
|
+
* @param document - The read deck.
|
|
2386
2492
|
* @returns The view.
|
|
2387
2493
|
*/
|
|
2388
|
-
function
|
|
2389
|
-
const margins = marginRows(document);
|
|
2494
|
+
function deckDocumentView(document) {
|
|
2390
2495
|
return {
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
height: page.height,
|
|
2395
|
-
rows: page.rows.map(row => ({
|
|
2396
|
-
top: row.top,
|
|
2397
|
-
bottom: row.bottom,
|
|
2398
|
-
cells: row.cells.map(cell => ({
|
|
2399
|
-
x: cell.x,
|
|
2400
|
-
y: cell.y,
|
|
2401
|
-
width: cell.width,
|
|
2402
|
-
height: cell.height,
|
|
2403
|
-
text: cell.text
|
|
2404
|
-
})),
|
|
2405
|
-
...(margins.has(row) && {
|
|
2406
|
-
running: true
|
|
2407
|
-
})
|
|
2408
|
-
})),
|
|
2409
|
-
rowCount: page.rows.length,
|
|
2410
|
-
cellCount: page.rows.reduce((total, row) => total + row.cells.length, 0),
|
|
2411
|
-
hasTextLayer: page.rows.length > 0
|
|
2412
|
-
}))
|
|
2496
|
+
width: document.width,
|
|
2497
|
+
height: document.height,
|
|
2498
|
+
slides: document.slides.map(slide => slideView(slide, document.height))
|
|
2413
2499
|
};
|
|
2414
2500
|
}
|
|
2415
|
-
|
|
2416
|
-
/**
|
|
2417
|
-
* Runs a `table` extract's options against an already-read PDF (studio plan
|
|
2418
|
-
* §3.4, issue #94's 5b: the live preview) — a pure function of the document
|
|
2419
|
-
* and the options, so re-running it on every keystroke as the person edits
|
|
2420
|
-
* `selector`/`until`/`columns` costs nothing: no re-fetch, no re-parse
|
|
2421
|
-
* (`readPdf` already ran once, when `take-snapshot` captured the document).
|
|
2422
|
-
*
|
|
2423
|
-
* The workbook grid's live preview (issue #94's 5c) is `previewGridTable`,
|
|
2424
|
-
* below: the same idea (a pure re-run of the options against the cached
|
|
2425
|
-
* document), but its own result shape — a workbook table has no page, no
|
|
2426
|
-
* column bands, no wrapped-row regrouping, so folding it into
|
|
2427
|
-
* `TablePreviewMatch`/`TablePreviewResult` would leave most of those fields
|
|
2428
|
-
* meaningless for a grid pick; a `GridTablePreviewMatch`/`GridTablePreviewResult`
|
|
2429
|
-
* sibling reads honestly instead.
|
|
2430
|
-
*
|
|
2431
|
-
* @param document - The already-read PDF.
|
|
2432
|
-
* @param options - The `table` extract's options, not yet compiled to `RegExp`.
|
|
2433
|
-
* @returns Every match, each with the exact row indices to highlight — or `error` (and no matches) when a pattern is not a valid regular expression, expected while the person is still typing it.
|
|
2434
|
-
*/
|
|
2435
|
-
function previewPdfTable(document, options) {
|
|
2436
|
-
let query;
|
|
2437
|
-
try {
|
|
2438
|
-
query = {
|
|
2439
|
-
header: compile(options.header, 'selector'),
|
|
2440
|
-
until: options.until === undefined ? undefined : compile(options.until, 'until'),
|
|
2441
|
-
columns: options.columns === undefined ? undefined : Object.fromEntries(Object.entries(options.columns).map(([key, pattern]) => [key, compile(pattern, `columns.${key}`)])),
|
|
2442
|
-
align: options.align
|
|
2443
|
-
};
|
|
2444
|
-
} catch (error) {
|
|
2445
|
-
return {
|
|
2446
|
-
matches: [],
|
|
2447
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2448
|
-
};
|
|
2449
|
-
}
|
|
2450
|
-
const analyses = analyzeTables(document, query);
|
|
2501
|
+
function slideView(slide, slideHeight) {
|
|
2451
2502
|
return {
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2503
|
+
number: slide.number,
|
|
2504
|
+
...(slide.title !== undefined && {
|
|
2505
|
+
title: slide.title
|
|
2506
|
+
}),
|
|
2507
|
+
hidden: slide.hidden,
|
|
2508
|
+
shapes: slide.shapes.map(shape => shapeView(shape)),
|
|
2509
|
+
shapeRows: shapeRowsOf(slide.shapes, slideHeight),
|
|
2510
|
+
tables: workbookDocumentView({
|
|
2511
|
+
sheets: slide.tables
|
|
2512
|
+
}).sheets,
|
|
2513
|
+
charts: slide.charts.map(chart => chartView(chart)),
|
|
2514
|
+
notes: slide.notes
|
|
2460
2515
|
};
|
|
2461
2516
|
}
|
|
2462
|
-
function
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2517
|
+
function shapeView(shape) {
|
|
2518
|
+
return {
|
|
2519
|
+
x: shape.x,
|
|
2520
|
+
y: shape.y,
|
|
2521
|
+
width: shape.width,
|
|
2522
|
+
height: shape.height,
|
|
2523
|
+
text: shape.text,
|
|
2524
|
+
...(shape.placeholder !== undefined && {
|
|
2525
|
+
placeholder: shape.placeholder
|
|
2526
|
+
})
|
|
2527
|
+
};
|
|
2466
2528
|
}
|
|
2467
|
-
function
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2529
|
+
function chartView(chart) {
|
|
2530
|
+
return {
|
|
2531
|
+
type: chart.type,
|
|
2532
|
+
...(chart.title !== undefined && {
|
|
2533
|
+
title: chart.title
|
|
2534
|
+
}),
|
|
2535
|
+
series: chart.series.map(series => ({
|
|
2536
|
+
name: series.name,
|
|
2537
|
+
categories: [...series.categories],
|
|
2538
|
+
values: [...series.values]
|
|
2539
|
+
}))
|
|
2540
|
+
};
|
|
2475
2541
|
}
|
|
2476
2542
|
/**
|
|
2477
|
-
*
|
|
2478
|
-
*
|
|
2479
|
-
*
|
|
2480
|
-
*
|
|
2481
|
-
*
|
|
2543
|
+
* Groups a slide's shapes into visual rows the same way `deck-table.algorithm.ts`'s
|
|
2544
|
+
* own (core-internal) `shapeTables` does: each shape is one cell, `y`
|
|
2545
|
+
* flipped to the PDF-style bottom-up convention `rowsOfCells` (and the whole
|
|
2546
|
+
* table-reading pipeline it feeds) works in, so the grouping this returns is
|
|
2547
|
+
* byte-for-byte what `findDeckTables(document, { shapes: true })` groups at
|
|
2548
|
+
* run time — a pick built from it matches for real, not just visually.
|
|
2482
2549
|
*
|
|
2483
|
-
* @param
|
|
2484
|
-
* @param
|
|
2485
|
-
* @returns
|
|
2550
|
+
* @param shapes - The slide's shapes, in reading order.
|
|
2551
|
+
* @param slideHeight - The deck's slide height, in points (`DeckDocument.height`).
|
|
2552
|
+
* @returns Each row's shape indices, left to right; a blank shape (whitespace-only text) is never part of a row.
|
|
2486
2553
|
*/
|
|
2487
|
-
function
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
};
|
|
2499
|
-
} catch (error) {
|
|
2500
|
-
return {
|
|
2501
|
-
matches: [],
|
|
2502
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2554
|
+
function shapeRowsOf(shapes, slideHeight) {
|
|
2555
|
+
const indexOf = new Map();
|
|
2556
|
+
const cells = shapes.flatMap((shape, index) => {
|
|
2557
|
+
const text = shape.text.replaceAll(/\s+/g, ' ').trim();
|
|
2558
|
+
if (text === '') return [];
|
|
2559
|
+
const cell = {
|
|
2560
|
+
x: shape.x,
|
|
2561
|
+
y: slideHeight - shape.y - shape.height,
|
|
2562
|
+
width: shape.width,
|
|
2563
|
+
height: shape.height,
|
|
2564
|
+
text
|
|
2503
2565
|
};
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
sheet: table.sheet,
|
|
2509
|
-
title: table.title,
|
|
2510
|
-
header: table.header,
|
|
2511
|
-
rows: table.rows
|
|
2512
|
-
}))
|
|
2513
|
-
};
|
|
2566
|
+
indexOf.set(cell, index);
|
|
2567
|
+
return [cell];
|
|
2568
|
+
});
|
|
2569
|
+
return rowsOfCells(cells).map(row => row.cells.map(cell => indexOf.get(cell)));
|
|
2514
2570
|
}
|
|
2571
|
+
|
|
2572
|
+
const TEXT_LIMIT$1 = 120;
|
|
2515
2573
|
/**
|
|
2516
|
-
*
|
|
2517
|
-
*
|
|
2518
|
-
*
|
|
2519
|
-
* no re-fetch, no re-parse (`readPptxDeck` already ran once, when
|
|
2520
|
-
* `take-snapshot` captured the document).
|
|
2574
|
+
* Builds the document tree for `document-tree` (studio plan §3.4, issue
|
|
2575
|
+
* #94's 5a) from the body `page-snapshot`'s `take-snapshot.use-case.ts`
|
|
2576
|
+
* already fetched — no second read of the document.
|
|
2521
2577
|
*
|
|
2522
|
-
* @param
|
|
2523
|
-
*
|
|
2524
|
-
* @returns
|
|
2578
|
+
* @param body - The parsed body (`HttpBody`); YAML and JSON Lines both
|
|
2579
|
+
* arrive here already normalised to `{ kind: 'json' }` by `http.client.ts`.
|
|
2580
|
+
* @returns The tree.
|
|
2581
|
+
* @throws Error when `body`'s kind is not `json` or `xml` (PDF, workbook and
|
|
2582
|
+
* deck documents get their own canvases — 5b/5c/5d, not this mapper).
|
|
2525
2583
|
*/
|
|
2526
|
-
function
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2584
|
+
function documentTreeView(body) {
|
|
2585
|
+
if (body.kind === 'json') return {
|
|
2586
|
+
format: 'json',
|
|
2587
|
+
root: jsonNode(body.data, '$', 'root')
|
|
2588
|
+
};
|
|
2589
|
+
if (body.kind === 'xml') return xmlTreeView(body.xml);
|
|
2590
|
+
throw new Error(`document-tree: "${body.kind}" has no tree canvas (only "json" and "xml" bodies do)`);
|
|
2591
|
+
}
|
|
2592
|
+
function listFields(path, context) {
|
|
2593
|
+
if (context === undefined) return {};
|
|
2594
|
+
return {
|
|
2595
|
+
listPath: context.generalPrefix + path.slice(context.itemPrefix.length),
|
|
2596
|
+
listCount: context.count
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
function jsonNode(value, path, label, context) {
|
|
2600
|
+
if (Array.isArray(value)) {
|
|
2601
|
+
const children = value.map((item, index) => {
|
|
2602
|
+
const itemPath = `${path}[${index}]`;
|
|
2603
|
+
return jsonNode(item, itemPath, String(index), {
|
|
2604
|
+
itemPrefix: itemPath,
|
|
2605
|
+
generalPrefix: `${path}[*]`,
|
|
2606
|
+
count: value.length
|
|
2607
|
+
});
|
|
2608
|
+
});
|
|
2609
|
+
return {
|
|
2610
|
+
id: path,
|
|
2611
|
+
label,
|
|
2612
|
+
valueType: 'array',
|
|
2613
|
+
jsonpath: path,
|
|
2614
|
+
...listFields(path, context),
|
|
2615
|
+
children
|
|
2539
2616
|
};
|
|
2540
|
-
}
|
|
2617
|
+
}
|
|
2618
|
+
if (value !== null && typeof value === 'object') {
|
|
2619
|
+
const entries = Object.entries(value);
|
|
2620
|
+
// A plain object passes its own context through unchanged: only an array (or, in XML, a repeated element) starts a new one.
|
|
2621
|
+
const children = entries.map(([key, item]) => jsonNode(item, `${path}${propertyAccess(key)}`, key, context));
|
|
2541
2622
|
return {
|
|
2542
|
-
|
|
2543
|
-
|
|
2623
|
+
id: path,
|
|
2624
|
+
label,
|
|
2625
|
+
valueType: 'object',
|
|
2626
|
+
jsonpath: path,
|
|
2627
|
+
...listFields(path, context),
|
|
2628
|
+
children
|
|
2544
2629
|
};
|
|
2545
2630
|
}
|
|
2546
|
-
const tables = findDeckTables(document, query);
|
|
2547
2631
|
return {
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2632
|
+
id: path,
|
|
2633
|
+
label,
|
|
2634
|
+
valueType: leafType(value),
|
|
2635
|
+
jsonpath: path,
|
|
2636
|
+
preview: previewOf(value),
|
|
2637
|
+
...listFields(path, context),
|
|
2638
|
+
children: []
|
|
2555
2639
|
};
|
|
2556
2640
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2641
|
+
function leafType(value) {
|
|
2642
|
+
if (value === null) return 'null';
|
|
2643
|
+
if (typeof value === 'string') return 'string';
|
|
2644
|
+
if (typeof value === 'number') return 'number';
|
|
2645
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
2646
|
+
return 'string';
|
|
2647
|
+
}
|
|
2648
|
+
function previewOf(value) {
|
|
2649
|
+
const text = value === null ? 'null' : typeof value === 'string' ? value : JSON.stringify(value);
|
|
2650
|
+
return text.length > TEXT_LIMIT$1 ? `${text.slice(0, TEXT_LIMIT$1)}…` : text;
|
|
2651
|
+
}
|
|
2652
|
+
/** A JSONPath property step: dot notation for a plain identifier, bracket notation (quoted) otherwise — jsonpath-plus accepts both, but only dot notation round-trips through a person reading the card. */
|
|
2653
|
+
function propertyAccess(key) {
|
|
2654
|
+
return /^[A-Z_]\w*$/i.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
|
|
2655
|
+
}
|
|
2656
|
+
// --- XML ---
|
|
2657
|
+
const DEFAULT_NS_PREFIX = 'ns';
|
|
2658
|
+
function xmlTreeView(xml) {
|
|
2659
|
+
const document = parseXml(xml, 'document.xml');
|
|
2660
|
+
const root = document.documentElement;
|
|
2661
|
+
if (root === null) throw new Error('document-tree: the XML document has no root element');
|
|
2662
|
+
const namespaces = declaredNamespaces(root);
|
|
2663
|
+
const tree = xmlNode(root, '', namespaces, undefined);
|
|
2664
|
+
return {
|
|
2665
|
+
format: 'xml',
|
|
2666
|
+
root: tree,
|
|
2667
|
+
...(Object.keys(namespaces).length > 0 && {
|
|
2668
|
+
namespaces
|
|
2669
|
+
})
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
/** Every `xmlns:*` the root declares, plus a synthesised prefix (`ns`, `ns2`, …) for a bare default `xmlns="…"` — XPath 1.0 has no default namespace, so a query needs a prefix even when the markup does not use one (mirrors `xpath.algorithm.ts`'s own `declaredPrefixes`, extended for the default case that function leaves out on purpose — it is resolved automatically there, but a *picked* card still needs to name it explicitly in `namespaces`). */
|
|
2673
|
+
function declaredNamespaces(root) {
|
|
2674
|
+
const namespaces = {};
|
|
2675
|
+
let synthetic = 0;
|
|
2676
|
+
for (const attribute of root.attributes) {
|
|
2677
|
+
if (attribute.prefix === 'xmlns') namespaces[attribute.localName] = attribute.value;else if (attribute.name === 'xmlns') {
|
|
2678
|
+
synthetic += 1;
|
|
2679
|
+
namespaces[synthetic === 1 ? DEFAULT_NS_PREFIX : `${DEFAULT_NS_PREFIX}${synthetic}`] = attribute.value;
|
|
2587
2680
|
}
|
|
2588
|
-
if (region.on !== 'slide') return {
|
|
2589
|
-
matches: [],
|
|
2590
|
-
error: 'region: "page=" reads a PDF; this document is a deck (name a "slide=")'
|
|
2591
|
-
};
|
|
2592
|
-
return {
|
|
2593
|
-
matches: deckRegionText(document, region).map(match => ({
|
|
2594
|
-
page: match.slide,
|
|
2595
|
-
text: match.text,
|
|
2596
|
-
cells: [],
|
|
2597
|
-
shapes: match.shapes
|
|
2598
|
-
}))
|
|
2599
|
-
};
|
|
2600
|
-
} catch (error) {
|
|
2601
|
-
return {
|
|
2602
|
-
matches: [],
|
|
2603
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2604
|
-
};
|
|
2605
2681
|
}
|
|
2682
|
+
return namespaces;
|
|
2606
2683
|
}
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
* `previewDeckTable`, studio plan §3.4, issue #94's 5d), off the same cached
|
|
2612
|
-
* snapshot `take-snapshot`/`deck-view` already captured — a pure computation
|
|
2613
|
-
* over it, so this answers instantly on every option change, no re-fetch or
|
|
2614
|
-
* re-parse.
|
|
2615
|
-
*
|
|
2616
|
-
* @param state - The server state.
|
|
2617
|
-
* @param command - The command; `command.path` must already have a cached snapshot (call `take-snapshot` first).
|
|
2618
|
-
* @returns The matched tables, or `error` when an option's pattern does not compile.
|
|
2619
|
-
* @throws Error when no workspace is open, or `command.path` has no cached snapshot yet.
|
|
2620
|
-
*/
|
|
2621
|
-
function handleDeckPreview(state, command) {
|
|
2622
|
-
const snapshot = cachedSnapshot(state.snapshots, command.recipeId, command.path);
|
|
2623
|
-
if (snapshot === undefined) throw new Error(`no cached snapshot for "${command.recipeId}" at "${command.path}": call take-snapshot first`);
|
|
2624
|
-
if (!isDeckDocument(snapshot.body)) throw new Error(`"${command.recipeId}" at "${command.path}" is not a deck snapshot: deck-preview only reads a read PowerPoint document`);
|
|
2625
|
-
return previewDeckTable(snapshot.body, command.options);
|
|
2684
|
+
function prefixFor(element, namespaces) {
|
|
2685
|
+
if (element.namespaceURI === null || element.namespaceURI === undefined) return undefined;
|
|
2686
|
+
if (element.prefix !== null && element.prefix !== undefined && element.prefix !== '') return element.prefix;
|
|
2687
|
+
return Object.entries(namespaces).find(([, uri]) => uri === element.namespaceURI)?.[0];
|
|
2626
2688
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
* notes (`document-view`'s `deck-view.mapper.ts`, studio plan §3.4, issue
|
|
2631
|
-
* #94's 5d), off the same cached snapshot `take-snapshot`/`document-tree`
|
|
2632
|
-
* already use.
|
|
2633
|
-
*
|
|
2634
|
-
* @param state - The server state.
|
|
2635
|
-
* @param command - The command; `command.path` must already have a cached snapshot (call `take-snapshot` first).
|
|
2636
|
-
* @returns The document's view.
|
|
2637
|
-
* @throws Error when no workspace is open, `command.path` has no cached snapshot yet, or the snapshot's document is not a read deck.
|
|
2638
|
-
*/
|
|
2639
|
-
function handleDeckView(state, command) {
|
|
2640
|
-
const snapshot = cachedSnapshot(state.snapshots, command.recipeId, command.path);
|
|
2641
|
-
if (snapshot === undefined) throw new Error(`no cached snapshot for "${command.recipeId}" at "${command.path}": call take-snapshot first`);
|
|
2642
|
-
if (!isDeckDocument(snapshot.body)) throw new Error(`"${command.recipeId}" at "${command.path}" is not a deck snapshot: the deck canvas only reads a read PowerPoint document`);
|
|
2643
|
-
return deckDocumentView(snapshot.body);
|
|
2689
|
+
function qualifiedName(element, namespaces) {
|
|
2690
|
+
const prefix = prefixFor(element, namespaces);
|
|
2691
|
+
return prefix === undefined ? element.localName ?? element.tagName : `${prefix}:${element.localName ?? element.tagName}`;
|
|
2644
2692
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
const
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2693
|
+
function xmlNode(element, parentPath, namespaces, context) {
|
|
2694
|
+
const name = qualifiedName(element, namespaces);
|
|
2695
|
+
const siblingIndex = elementSiblingIndex(element);
|
|
2696
|
+
const siblingCount = elementSiblingCount(element);
|
|
2697
|
+
const step = siblingCount > 1 ? `${name}[${siblingIndex}]` : name;
|
|
2698
|
+
const path = `${parentPath}/${step}`;
|
|
2699
|
+
// I am one of several same-named siblings: my own listPath generalises myself, and my children inherit *my* context (innermost repetition wins) instead of whatever ancestor context I received.
|
|
2700
|
+
const selfContext = siblingCount > 1 ? {
|
|
2701
|
+
itemPrefix: path,
|
|
2702
|
+
generalPrefix: `${parentPath}/${name}`,
|
|
2703
|
+
count: siblingCount
|
|
2704
|
+
} : context;
|
|
2705
|
+
const childContext = siblingCount > 1 ? selfContext : context;
|
|
2706
|
+
const attributes = elementAttributes(element);
|
|
2707
|
+
const childElements = [...element.childNodes].filter(node => node.nodeType === 1);
|
|
2708
|
+
const children = [...Object.entries(attributes).map(([key, value]) => attributeNode(key, value, path, childContext)), ...childElements.map(child => xmlNode(child, path, namespaces, childContext))];
|
|
2709
|
+
const text = childElements.length === 0 ? textOf(element) : undefined;
|
|
2710
|
+
return {
|
|
2711
|
+
id: path,
|
|
2712
|
+
label: name,
|
|
2713
|
+
valueType: 'element',
|
|
2714
|
+
xpath: path,
|
|
2715
|
+
...listFields(path, selfContext),
|
|
2716
|
+
...(Object.keys(attributes).length > 0 && {
|
|
2717
|
+
attributes
|
|
2718
|
+
}),
|
|
2719
|
+
...(text !== undefined && {
|
|
2720
|
+
preview: text,
|
|
2721
|
+
valueType: childElements.length === 0 ? 'string' : 'element'
|
|
2722
|
+
}),
|
|
2723
|
+
children
|
|
2724
|
+
};
|
|
2661
2725
|
}
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
* Finds and parses one input recipe of a workspace folder, and the output
|
|
2665
|
-
* recipe it names.
|
|
2666
|
-
*
|
|
2667
|
-
* @param folder - The workspace folder.
|
|
2668
|
-
* @param recipeId - The input recipe's id.
|
|
2669
|
-
* @returns The parsed pair.
|
|
2670
|
-
* @throws RecipeValidationError when either recipe does not parse.
|
|
2671
|
-
* @throws Error when the input or its output is not in the folder.
|
|
2672
|
-
*/
|
|
2673
|
-
async function loadRecipePair(folder, recipeId) {
|
|
2674
|
-
const documents = await readRecipeSource(folder);
|
|
2675
|
-
const inputDocument = documents.find(document => kindOf(document.content) === 'input' && idOf(document.content) === recipeId);
|
|
2676
|
-
if (inputDocument === undefined) throw new Error(`no input recipe "${recipeId}" in this workspace`);
|
|
2677
|
-
const input = parseInputRecipe(inputDocument.content, inputDocument.source);
|
|
2678
|
-
const outputDocument = documents.find(document => kindOf(document.content) === 'output' && idOf(document.content) === input.output);
|
|
2679
|
-
if (outputDocument === undefined) throw new Error(`no output recipe "${input.output}" in this workspace`);
|
|
2726
|
+
function attributeNode(name, value, elementPath, context) {
|
|
2727
|
+
const path = `${elementPath}/@${name}`;
|
|
2680
2728
|
return {
|
|
2681
|
-
|
|
2682
|
-
|
|
2729
|
+
id: path,
|
|
2730
|
+
label: `@${name}`,
|
|
2731
|
+
valueType: 'string',
|
|
2732
|
+
xpath: path,
|
|
2733
|
+
preview: previewOf(value),
|
|
2734
|
+
...listFields(path, context),
|
|
2735
|
+
children: []
|
|
2683
2736
|
};
|
|
2684
2737
|
}
|
|
2685
|
-
function
|
|
2686
|
-
return
|
|
2738
|
+
function elementAttributes(element) {
|
|
2739
|
+
return Object.fromEntries([...element.attributes].filter(attribute => attribute.prefix !== 'xmlns' && attribute.name !== 'xmlns').map(attribute => [attribute.name, attribute.value]));
|
|
2687
2740
|
}
|
|
2688
|
-
function
|
|
2689
|
-
|
|
2741
|
+
function elementSiblingIndex(element) {
|
|
2742
|
+
let index = 1;
|
|
2743
|
+
let sibling = element.previousSibling;
|
|
2744
|
+
while (sibling !== null) {
|
|
2745
|
+
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) index += 1;
|
|
2746
|
+
sibling = sibling.previousSibling;
|
|
2747
|
+
}
|
|
2748
|
+
return index;
|
|
2749
|
+
}
|
|
2750
|
+
function elementSiblingCount(element) {
|
|
2751
|
+
const parent = element.parentNode;
|
|
2752
|
+
if (parent === null) return 1;
|
|
2753
|
+
return [...parent.childNodes].filter(node => node.nodeType === 1 && node.tagName === element.tagName).length;
|
|
2754
|
+
}
|
|
2755
|
+
function textOf(element) {
|
|
2756
|
+
const text = (element.textContent ?? '').trim().replaceAll(/\s+/g, ' ');
|
|
2757
|
+
return text === '' ? undefined : previewOf(text);
|
|
2690
2758
|
}
|
|
2691
2759
|
|
|
2692
2760
|
/**
|
|
2693
|
-
*
|
|
2694
|
-
*
|
|
2695
|
-
*
|
|
2696
|
-
* -
|
|
2697
|
-
*
|
|
2761
|
+
* Builds the PDF canvas's view model (studio plan §3.4, issue #94's 5b) from
|
|
2762
|
+
* an already-read `PdfDocument` (`take-snapshot`'s cached body — no second
|
|
2763
|
+
* read of the file): every cell and row `readPdf` found, in PDF points, plus
|
|
2764
|
+
* per-page counts and whether the page has a text layer at all. Table-specific
|
|
2765
|
+
* diagnostics (the header row, the column bands, the wrapped-row regrouping)
|
|
2766
|
+
* are not here — they depend on a `table` extract's own options, so they come
|
|
2767
|
+
* from `table-preview.use-case.ts` instead, computed live as those options
|
|
2768
|
+
* change.
|
|
2698
2769
|
*
|
|
2699
|
-
* @param
|
|
2700
|
-
* @
|
|
2701
|
-
* @param announce - Where the trace lines go.
|
|
2702
|
-
* @returns The hooks to run the sample with.
|
|
2770
|
+
* @param document - The read PDF.
|
|
2771
|
+
* @returns The view.
|
|
2703
2772
|
*/
|
|
2704
|
-
function
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2773
|
+
function pdfDocumentView(document) {
|
|
2774
|
+
const margins = marginRows(document);
|
|
2775
|
+
return {
|
|
2776
|
+
pages: document.pages.map(page => ({
|
|
2777
|
+
number: page.number,
|
|
2778
|
+
width: page.width,
|
|
2779
|
+
height: page.height,
|
|
2780
|
+
rows: page.rows.map(row => ({
|
|
2781
|
+
top: row.top,
|
|
2782
|
+
bottom: row.bottom,
|
|
2783
|
+
cells: row.cells.map(cell => ({
|
|
2784
|
+
x: cell.x,
|
|
2785
|
+
y: cell.y,
|
|
2786
|
+
width: cell.width,
|
|
2787
|
+
height: cell.height,
|
|
2788
|
+
text: cell.text
|
|
2789
|
+
})),
|
|
2790
|
+
...(margins.has(row) && {
|
|
2791
|
+
running: true
|
|
2792
|
+
})
|
|
2793
|
+
})),
|
|
2794
|
+
rowCount: page.rows.length,
|
|
2795
|
+
cellCount: page.rows.reduce((total, row) => total + row.cells.length, 0),
|
|
2796
|
+
hasTextLayer: page.rows.length > 0
|
|
2797
|
+
}))
|
|
2798
|
+
};
|
|
2723
2799
|
}
|
|
2724
2800
|
|
|
2725
2801
|
/**
|
|
2726
|
-
* Runs
|
|
2727
|
-
*
|
|
2728
|
-
*
|
|
2729
|
-
*
|
|
2730
|
-
*
|
|
2731
|
-
* cli's `--trace` uses; every emitted record reaches `callbacks.onRecord` as
|
|
2732
|
-
* it happens, not only in the final result. Step events (`step:start`,
|
|
2733
|
-
* `step:skip`, `step:retry`) are aggregated by path into `result.steps`, so
|
|
2734
|
-
* `explain-why` (#92) can later say whether the step that bound a mapping's
|
|
2735
|
-
* source id ran, was skipped, or retried — without a second run.
|
|
2802
|
+
* Runs a `table` extract's options against an already-read PDF (studio plan
|
|
2803
|
+
* §3.4, issue #94's 5b: the live preview) — a pure function of the document
|
|
2804
|
+
* and the options, so re-running it on every keystroke as the person edits
|
|
2805
|
+
* `selector`/`until`/`columns` costs nothing: no re-fetch, no re-parse
|
|
2806
|
+
* (`readPdf` already ran once, when `take-snapshot` captured the document).
|
|
2736
2807
|
*
|
|
2737
|
-
*
|
|
2738
|
-
*
|
|
2739
|
-
*
|
|
2740
|
-
*
|
|
2741
|
-
*
|
|
2742
|
-
*
|
|
2743
|
-
*
|
|
2744
|
-
*
|
|
2745
|
-
* @
|
|
2808
|
+
* The workbook grid's live preview (issue #94's 5c) is `previewGridTable`,
|
|
2809
|
+
* below: the same idea (a pure re-run of the options against the cached
|
|
2810
|
+
* document), but its own result shape — a workbook table has no page, no
|
|
2811
|
+
* column bands, no wrapped-row regrouping, so folding it into
|
|
2812
|
+
* `TablePreviewMatch`/`TablePreviewResult` would leave most of those fields
|
|
2813
|
+
* meaningless for a grid pick; a `GridTablePreviewMatch`/`GridTablePreviewResult`
|
|
2814
|
+
* sibling reads honestly instead.
|
|
2815
|
+
*
|
|
2816
|
+
* @param document - The already-read PDF.
|
|
2817
|
+
* @param options - The `table` extract's options, not yet compiled to `RegExp`.
|
|
2818
|
+
* @returns Every match, each with the exact row indices to highlight — or `error` (and no matches) when a pattern is not a valid regular expression, expected while the person is still typing it.
|
|
2746
2819
|
*/
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
accessPlugins: plugins?.accessPlugins,
|
|
2764
|
-
captchaSolvers: plugins?.captchaSolvers,
|
|
2765
|
-
onEvent: event => {
|
|
2766
|
-
const line = traceLine(event);
|
|
2767
|
-
if (line !== undefined) callbacks.onTraceLine(line);
|
|
2768
|
-
trackStep(steps, event);
|
|
2769
|
-
if (event.type === 'record:emit') {
|
|
2770
|
-
const record = {
|
|
2771
|
-
key: event.key,
|
|
2772
|
-
data: event.data,
|
|
2773
|
-
scope: event.scope,
|
|
2774
|
-
mapping: event.mapping
|
|
2775
|
-
};
|
|
2776
|
-
records.push(record);
|
|
2777
|
-
callbacks.onRecord(record);
|
|
2778
|
-
} else if (event.type === 'record:reject') {
|
|
2779
|
-
const rejected = {
|
|
2780
|
-
field: event.field,
|
|
2781
|
-
reason: event.reason,
|
|
2782
|
-
url: event.url,
|
|
2783
|
-
scope: event.scope
|
|
2784
|
-
};
|
|
2785
|
-
rejectedRecords.push(rejected);
|
|
2786
|
-
callbacks.onRejected?.(rejected);
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
});
|
|
2790
|
-
const result = runToResult(crawler, set, recipeId, records, rejectedRecords, steps, plugins);
|
|
2820
|
+
function previewPdfTable(document, options) {
|
|
2821
|
+
let query;
|
|
2822
|
+
try {
|
|
2823
|
+
query = {
|
|
2824
|
+
header: compile(options.header, 'selector'),
|
|
2825
|
+
until: options.until === undefined ? undefined : compile(options.until, 'until'),
|
|
2826
|
+
columns: options.columns === undefined ? undefined : Object.fromEntries(Object.entries(options.columns).map(([key, pattern]) => [key, compile(pattern, `columns.${key}`)])),
|
|
2827
|
+
align: options.align
|
|
2828
|
+
};
|
|
2829
|
+
} catch (error) {
|
|
2830
|
+
return {
|
|
2831
|
+
matches: [],
|
|
2832
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
const analyses = analyzeTables(document, query);
|
|
2791
2836
|
return {
|
|
2792
|
-
|
|
2793
|
-
|
|
2837
|
+
matches: analyses.map(analysis => ({
|
|
2838
|
+
page: analysis.page,
|
|
2839
|
+
headerRowIndex: analysis.headerRowIndex,
|
|
2840
|
+
matchedRowIndices: rowIndicesOf(analysis),
|
|
2841
|
+
bands: analysis.bands,
|
|
2842
|
+
bandTolerance: analysis.bandTolerance,
|
|
2843
|
+
table: analysis.table
|
|
2844
|
+
}))
|
|
2794
2845
|
};
|
|
2795
2846
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
path: event.path,
|
|
2808
|
-
stepType: event.stepType,
|
|
2809
|
-
stepId: event.stepId,
|
|
2810
|
-
outcome: 'ran'
|
|
2811
|
-
});
|
|
2812
|
-
return;
|
|
2813
|
-
}
|
|
2814
|
-
if (event.type === 'step:retry') {
|
|
2815
|
-
steps.set(event.path, {
|
|
2816
|
-
path: event.path,
|
|
2817
|
-
stepType: event.stepType,
|
|
2818
|
-
stepId: event.stepId,
|
|
2819
|
-
outcome: 'retried',
|
|
2820
|
-
error: event.error
|
|
2847
|
+
function rowIndicesOf(analysis) {
|
|
2848
|
+
const indices = new Set([analysis.headerRowIndex, ...analysis.rowGroups.flat()]);
|
|
2849
|
+
if (analysis.untilRowIndex !== undefined) indices.add(analysis.untilRowIndex);
|
|
2850
|
+
return [...indices].sort((a, b) => a - b);
|
|
2851
|
+
}
|
|
2852
|
+
function compile(source, where) {
|
|
2853
|
+
try {
|
|
2854
|
+
return new RegExp(source, 'i');
|
|
2855
|
+
} catch (error) {
|
|
2856
|
+
throw new Error(`${where}: invalid pattern ${source} (${error.message})`, {
|
|
2857
|
+
cause: error
|
|
2821
2858
|
});
|
|
2822
|
-
return;
|
|
2823
2859
|
}
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2860
|
+
}
|
|
2861
|
+
/**
|
|
2862
|
+
* Runs a `table` extract's options against an already-read workbook (studio
|
|
2863
|
+
* plan §3.4, issue #94's 5c: the live preview) — a pure function of the
|
|
2864
|
+
* document and the options, exactly like `previewPdfTable` above: no
|
|
2865
|
+
* re-fetch, no re-parse (`csvWorkbook`/`readXlsxWorkbook` already ran once,
|
|
2866
|
+
* when `take-snapshot` captured the document).
|
|
2867
|
+
*
|
|
2868
|
+
* @param document - The already-read workbook.
|
|
2869
|
+
* @param options - The `table` extract's options, not yet compiled to `RegExp`.
|
|
2870
|
+
* @returns Every matched table — or `error` (and no matches) when a pattern is not a valid regular expression, expected while the person is still typing it.
|
|
2871
|
+
*/
|
|
2872
|
+
function previewGridTable(document, options) {
|
|
2873
|
+
let query;
|
|
2874
|
+
try {
|
|
2875
|
+
query = {
|
|
2876
|
+
header: compile(options.header, 'selector'),
|
|
2877
|
+
until: options.until === undefined ? undefined : compile(options.until, 'until'),
|
|
2878
|
+
columns: options.columns === undefined ? undefined : Object.fromEntries(Object.entries(options.columns).map(([key, pattern]) => [key, compile(pattern, `columns.${key}`)])),
|
|
2879
|
+
sheet: options.sheet === undefined ? undefined : compile(options.sheet, 'sheet'),
|
|
2880
|
+
headerRows: options.headerRows,
|
|
2881
|
+
fillDown: options.fillDown,
|
|
2882
|
+
includeHidden: options.includeHidden
|
|
2883
|
+
};
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
return {
|
|
2886
|
+
matches: [],
|
|
2887
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2888
|
+
};
|
|
2832
2889
|
}
|
|
2890
|
+
const tables = findGridTables(document, query);
|
|
2891
|
+
return {
|
|
2892
|
+
matches: tables.map(table => ({
|
|
2893
|
+
sheet: table.sheet,
|
|
2894
|
+
title: table.title,
|
|
2895
|
+
header: table.header,
|
|
2896
|
+
rows: table.rows
|
|
2897
|
+
}))
|
|
2898
|
+
};
|
|
2833
2899
|
}
|
|
2834
2900
|
/**
|
|
2835
|
-
*
|
|
2836
|
-
*
|
|
2837
|
-
*
|
|
2838
|
-
*
|
|
2901
|
+
* Runs a `table` extract's options against an already-read deck (studio plan
|
|
2902
|
+
* §3.4, issue #94's 5d: the live preview) — a pure function of the document
|
|
2903
|
+
* and the options, exactly like `previewPdfTable`/`previewGridTable` above:
|
|
2904
|
+
* no re-fetch, no re-parse (`readPptxDeck` already ran once, when
|
|
2905
|
+
* `take-snapshot` captured the document).
|
|
2906
|
+
*
|
|
2907
|
+
* @param document - The already-read deck.
|
|
2908
|
+
* @param options - The `table` extract's options, not yet compiled to `RegExp`.
|
|
2909
|
+
* @returns Every matched table — or `error` (and no matches) when a pattern is not a valid regular expression, expected while the person is still typing it.
|
|
2839
2910
|
*/
|
|
2840
|
-
|
|
2911
|
+
function previewDeckTable(document, options) {
|
|
2912
|
+
let query;
|
|
2841
2913
|
try {
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
records,
|
|
2853
|
-
rejectedRecords,
|
|
2854
|
-
// eslint-disable-next-line unicorn/prefer-iterator-to-array -- `.toArray()` needs a `lib` newer than this repo's `es2022` (tsconfig.base.json)
|
|
2855
|
-
steps: [...steps.values()]
|
|
2914
|
+
query = {
|
|
2915
|
+
slide: options.slide === undefined ? undefined : compile(options.slide, 'slide'),
|
|
2916
|
+
shapes: options.shapes,
|
|
2917
|
+
align: options.align,
|
|
2918
|
+
header: compile(options.header, 'selector'),
|
|
2919
|
+
until: options.until === undefined ? undefined : compile(options.until, 'until'),
|
|
2920
|
+
columns: options.columns === undefined ? undefined : Object.fromEntries(Object.entries(options.columns).map(([key, pattern]) => [key, compile(pattern, `columns.${key}`)])),
|
|
2921
|
+
headerRows: options.headerRows,
|
|
2922
|
+
fillDown: options.fillDown,
|
|
2923
|
+
includeHidden: options.includeHidden
|
|
2856
2924
|
};
|
|
2857
2925
|
} catch (error) {
|
|
2858
2926
|
return {
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
rejected: 0,
|
|
2862
|
-
duplicates: 0,
|
|
2863
|
-
durationMs: 0,
|
|
2864
|
-
error: failureMessage(error, plugins),
|
|
2865
|
-
records,
|
|
2866
|
-
rejectedRecords,
|
|
2867
|
-
// eslint-disable-next-line unicorn/prefer-iterator-to-array -- `.toArray()` needs a `lib` newer than this repo's `es2022` (tsconfig.base.json)
|
|
2868
|
-
steps: [...steps.values()]
|
|
2927
|
+
matches: [],
|
|
2928
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2869
2929
|
};
|
|
2870
|
-
} finally {
|
|
2871
|
-
await crawler.close();
|
|
2872
2930
|
}
|
|
2931
|
+
const tables = findDeckTables(document, query);
|
|
2932
|
+
return {
|
|
2933
|
+
matches: tables.map(table => ({
|
|
2934
|
+
slide: table.slide,
|
|
2935
|
+
slideTitle: table.slideTitle,
|
|
2936
|
+
title: table.title,
|
|
2937
|
+
header: table.header,
|
|
2938
|
+
rows: table.rows
|
|
2939
|
+
}))
|
|
2940
|
+
};
|
|
2873
2941
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2942
|
+
|
|
2943
|
+
/**
|
|
2944
|
+
* Runs a `region` extract's selector against an already-read PDF or deck
|
|
2945
|
+
* (issues #121 and #122: the canvases' click-to-snap selection) — the
|
|
2946
|
+
* engine's own `regionText` / `deckRegionText`, so what the canvas
|
|
2947
|
+
* highlights and the text it shows are exactly what the step will read,
|
|
2948
|
+
* and a pure function of the document, so re-running it on every change
|
|
2949
|
+
* of the box costs nothing. A `slide=` on a PDF (or a `page=` on a deck)
|
|
2950
|
+
* is the same mismatch the step would fail with, reported as `error`.
|
|
2951
|
+
*
|
|
2952
|
+
* @param document - The read PDF or deck (`take-snapshot`'s cached body).
|
|
2953
|
+
* @param selector - The `region` selector, `page=1 x=72..252 y=640..664` or `slide=2 x=60..900 y=30..90`.
|
|
2954
|
+
* @returns The matches, or `error` when the selector does not parse or does not fit the document.
|
|
2955
|
+
*/
|
|
2956
|
+
function previewRegion(document, selector) {
|
|
2957
|
+
try {
|
|
2958
|
+
const region = parseRegion(selector);
|
|
2959
|
+
if (document.kind === 'pdf') {
|
|
2960
|
+
if (region.on !== 'page') return {
|
|
2961
|
+
matches: [],
|
|
2962
|
+
error: 'region: "slide=" reads a deck; this document is a PDF (name a "page=")'
|
|
2963
|
+
};
|
|
2964
|
+
return {
|
|
2965
|
+
matches: regionText(document, region).map(match => ({
|
|
2966
|
+
page: match.page,
|
|
2967
|
+
text: match.text,
|
|
2968
|
+
cells: match.cells,
|
|
2969
|
+
shapes: []
|
|
2970
|
+
}))
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
if (region.on !== 'slide') return {
|
|
2974
|
+
matches: [],
|
|
2975
|
+
error: 'region: "page=" reads a PDF; this document is a deck (name a "slide=")'
|
|
2976
|
+
};
|
|
2977
|
+
return {
|
|
2978
|
+
matches: deckRegionText(document, region).map(match => ({
|
|
2979
|
+
page: match.slide,
|
|
2980
|
+
text: match.text,
|
|
2981
|
+
cells: [],
|
|
2982
|
+
shapes: match.shapes
|
|
2983
|
+
}))
|
|
2984
|
+
};
|
|
2985
|
+
} catch (error) {
|
|
2986
|
+
return {
|
|
2987
|
+
matches: [],
|
|
2988
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2989
|
+
};
|
|
2880
2990
|
}
|
|
2881
|
-
if (error instanceof Error) return stripVTControlCharacters(error.message);
|
|
2882
|
-
return stripVTControlCharacters(String(error));
|
|
2883
2991
|
}
|
|
2884
2992
|
|
|
2885
|
-
/**
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2993
|
+
/**
|
|
2994
|
+
* Handles `deck-preview`: the deck canvas's live preview of a `table`
|
|
2995
|
+
* extract's options (`document-view`'s `table-preview.use-case.ts`'s
|
|
2996
|
+
* `previewDeckTable`, studio plan §3.4, issue #94's 5d), off the same cached
|
|
2997
|
+
* snapshot `take-snapshot`/`deck-view` already captured — a pure computation
|
|
2998
|
+
* over it, so this answers instantly on every option change, no re-fetch or
|
|
2999
|
+
* re-parse.
|
|
3000
|
+
*
|
|
3001
|
+
* @param state - The server state.
|
|
3002
|
+
* @param command - The command; `command.path` must already have a cached snapshot (call `take-snapshot` first).
|
|
3003
|
+
* @returns The matched tables, or `error` when an option's pattern does not compile.
|
|
3004
|
+
* @throws Error when no workspace is open, or `command.path` has no cached snapshot yet.
|
|
3005
|
+
*/
|
|
3006
|
+
function handleDeckPreview(state, command) {
|
|
3007
|
+
const snapshot = cachedSnapshot(state.snapshots, command.recipeId, command.path);
|
|
3008
|
+
if (snapshot === undefined) throw new Error(`no cached snapshot for "${command.recipeId}" at "${command.path}": call take-snapshot first`);
|
|
3009
|
+
if (!isDeckDocument(snapshot.body)) throw new Error(`"${command.recipeId}" at "${command.path}" is not a deck snapshot: deck-preview only reads a read PowerPoint document`);
|
|
3010
|
+
return previewDeckTable(snapshot.body, command.options);
|
|
2890
3011
|
}
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3012
|
+
|
|
3013
|
+
/**
|
|
3014
|
+
* Handles `deck-view`: the deck canvas's slides, shapes, tables, charts and
|
|
3015
|
+
* notes (`document-view`'s `deck-view.mapper.ts`, studio plan §3.4, issue
|
|
3016
|
+
* #94's 5d), off the same cached snapshot `take-snapshot`/`document-tree`
|
|
3017
|
+
* already use.
|
|
3018
|
+
*
|
|
3019
|
+
* @param state - The server state.
|
|
3020
|
+
* @param command - The command; `command.path` must already have a cached snapshot (call `take-snapshot` first).
|
|
3021
|
+
* @returns The document's view.
|
|
3022
|
+
* @throws Error when no workspace is open, `command.path` has no cached snapshot yet, or the snapshot's document is not a read deck.
|
|
3023
|
+
*/
|
|
3024
|
+
function handleDeckView(state, command) {
|
|
3025
|
+
const snapshot = cachedSnapshot(state.snapshots, command.recipeId, command.path);
|
|
3026
|
+
if (snapshot === undefined) throw new Error(`no cached snapshot for "${command.recipeId}" at "${command.path}": call take-snapshot first`);
|
|
3027
|
+
if (!isDeckDocument(snapshot.body)) throw new Error(`"${command.recipeId}" at "${command.path}" is not a deck snapshot: the deck canvas only reads a read PowerPoint document`);
|
|
3028
|
+
return deckDocumentView(snapshot.body);
|
|
2897
3029
|
}
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3030
|
+
|
|
3031
|
+
/**
|
|
3032
|
+
* Handles `document-tree`: the content pane's tree canvas for a JSON, YAML
|
|
3033
|
+
* or XML document (`document-view`, studio plan §3.4, issue #94's 5a), off
|
|
3034
|
+
* the same cached snapshot `take-snapshot`/`inspect-page` already use.
|
|
3035
|
+
*
|
|
3036
|
+
* @param state - The server state.
|
|
3037
|
+
* @param command - The command; `command.path` must already have a cached snapshot (call `take-snapshot` first).
|
|
3038
|
+
* @returns The tree.
|
|
3039
|
+
* @throws Error when no workspace is open, `command.path` has no cached snapshot yet, or the snapshot's document is not JSON/YAML/XML (no parsed body at all in web mode; PDF/workbook/deck in api mode — see `documentTreeView`'s own doc comment).
|
|
3040
|
+
*/
|
|
3041
|
+
function handleDocumentTree(state, command) {
|
|
3042
|
+
const snapshot = cachedSnapshot(state.snapshots, command.recipeId, command.path);
|
|
3043
|
+
if (snapshot === undefined) throw new Error(`no cached snapshot for "${command.recipeId}" at "${command.path}": call take-snapshot first`);
|
|
3044
|
+
if (snapshot.body === undefined) throw new Error(`"${command.recipeId}" at "${command.path}" is a web-mode (HTML) snapshot: the tree canvas only reads a JSON/YAML/XML document`);
|
|
3045
|
+
return documentTreeView(snapshot.body);
|
|
2901
3046
|
}
|
|
2902
3047
|
|
|
2903
3048
|
/**
|
|
@@ -4835,38 +4980,6 @@ function remoteCallouts(plugins) {
|
|
|
4835
4980
|
return found.sort((a, b) => order[a.kind] - order[b.kind] || a.name.localeCompare(b.name));
|
|
4836
4981
|
}
|
|
4837
4982
|
|
|
4838
|
-
/** Where the studio's own recorder profile lives when `StudioServerOptions.recorderProfileDir` is not given: never a crawl's own `session.browserProfile` directory, never the person's default browser profile. */
|
|
4839
|
-
function defaultRecorderProfileDir() {
|
|
4840
|
-
return join(tmpdir(), 'opencraw-studio', 'recorder-profile');
|
|
4841
|
-
}
|
|
4842
|
-
/** @returns Fresh state for a new server instance: no workspace open, no run, no recording, no sockets, an empty snapshot cache, no last runs, Playwright's own default browser, the default recorder profile directory. */
|
|
4843
|
-
function createStudioState() {
|
|
4844
|
-
return {
|
|
4845
|
-
sockets: new Set(),
|
|
4846
|
-
snapshots: createSnapshotCache(),
|
|
4847
|
-
lastRuns: createLastRunCache(),
|
|
4848
|
-
recorderProfileDir: defaultRecorderProfileDir()
|
|
4849
|
-
};
|
|
4850
|
-
}
|
|
4851
|
-
/**
|
|
4852
|
-
* Sends one event to every connected client. A socket that errors while
|
|
4853
|
-
* sending (gone away without a clean close yet reaching us) is dropped
|
|
4854
|
-
* rather than left to fail every later broadcast too.
|
|
4855
|
-
*
|
|
4856
|
-
* @param state - The server state holding the connected sockets.
|
|
4857
|
-
* @param event - The event to send.
|
|
4858
|
-
*/
|
|
4859
|
-
function broadcast(state, event) {
|
|
4860
|
-
const text = JSON.stringify(event);
|
|
4861
|
-
for (const socket of state.sockets) {
|
|
4862
|
-
try {
|
|
4863
|
-
socket.send(text);
|
|
4864
|
-
} catch {
|
|
4865
|
-
state.sockets.delete(socket);
|
|
4866
|
-
}
|
|
4867
|
-
}
|
|
4868
|
-
}
|
|
4869
|
-
|
|
4870
4983
|
/**
|
|
4871
4984
|
* Handles `open-workspace`: reads the folder, remembers it as the server's
|
|
4872
4985
|
* current workspace (every later command that names a recipe resolves it
|
|
@@ -5711,6 +5824,52 @@ function handleRegionPreview(state, command) {
|
|
|
5711
5824
|
return previewRegion(snapshot.body, command.selector);
|
|
5712
5825
|
}
|
|
5713
5826
|
|
|
5827
|
+
/** How many records and rejections a status carries: enough to judge a run, the rest stay in Studio. */
|
|
5828
|
+
const FIRST = 20;
|
|
5829
|
+
/**
|
|
5830
|
+
* Handles `run-status`: how a recipe's sample run is going, for a controller that started it and cannot watch
|
|
5831
|
+
* the event stream. A run in progress is `running`; one that finished carries its counts and first records.
|
|
5832
|
+
*
|
|
5833
|
+
* @param state - The server state holding the run in progress and the last runs.
|
|
5834
|
+
* @param command - The recipe asked about.
|
|
5835
|
+
* @returns The state of the recipe's sample run.
|
|
5836
|
+
*/
|
|
5837
|
+
function handleRunStatus(state, command) {
|
|
5838
|
+
if (state.activeRun !== undefined && state.activeRunId === command.recipeId) return {
|
|
5839
|
+
recipeId: command.recipeId,
|
|
5840
|
+
state: 'running'
|
|
5841
|
+
};
|
|
5842
|
+
const last = lastRunOf(state.lastRuns, command.recipeId);
|
|
5843
|
+
if (last === undefined) return {
|
|
5844
|
+
recipeId: command.recipeId,
|
|
5845
|
+
state: 'never-run'
|
|
5846
|
+
};
|
|
5847
|
+
const {
|
|
5848
|
+
result
|
|
5849
|
+
} = last;
|
|
5850
|
+
return {
|
|
5851
|
+
recipeId: command.recipeId,
|
|
5852
|
+
state: 'finished',
|
|
5853
|
+
result: {
|
|
5854
|
+
emitted: result.emitted,
|
|
5855
|
+
rejected: result.rejected,
|
|
5856
|
+
duplicates: result.duplicates,
|
|
5857
|
+
durationMs: result.durationMs,
|
|
5858
|
+
error: result.error,
|
|
5859
|
+
stoppedBy: result.stoppedBy,
|
|
5860
|
+
records: result.records.slice(0, FIRST).map(record => ({
|
|
5861
|
+
key: record.key,
|
|
5862
|
+
data: record.data
|
|
5863
|
+
})),
|
|
5864
|
+
rejections: result.rejectedRecords.slice(0, FIRST).map(rejection => ({
|
|
5865
|
+
field: rejection.field,
|
|
5866
|
+
reason: rejection.reason,
|
|
5867
|
+
url: rejection.url
|
|
5868
|
+
}))
|
|
5869
|
+
}
|
|
5870
|
+
};
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5714
5873
|
/**
|
|
5715
5874
|
* Handles `run-sample`: stops whatever sample was still running (one run at
|
|
5716
5875
|
* a time), starts the new one, and returns once it has started, not once it
|
|
@@ -5753,6 +5912,7 @@ async function handleRunSample(state, command) {
|
|
|
5753
5912
|
}
|
|
5754
5913
|
}, state.browser, state.plugins, command.stubs);
|
|
5755
5914
|
state.activeRun = handle;
|
|
5915
|
+
state.activeRunId = command.recipeId;
|
|
5756
5916
|
void handle.result.then(result => {
|
|
5757
5917
|
if (state.activeRun === handle) state.activeRun = undefined;
|
|
5758
5918
|
recordLastRun(state.lastRuns, command.recipeId, result);
|
|
@@ -5875,6 +6035,20 @@ function serveStatic(root, pathname, response) {
|
|
|
5875
6035
|
createReadStream(file).pipe(response);
|
|
5876
6036
|
}
|
|
5877
6037
|
|
|
6038
|
+
/**
|
|
6039
|
+
* Handles `studio-status`: what Studio is doing right now.
|
|
6040
|
+
*
|
|
6041
|
+
* @param state - The server state.
|
|
6042
|
+
* @returns The open workspace, how many pages are open and the recipe being sampled.
|
|
6043
|
+
*/
|
|
6044
|
+
function handleStudioStatus(state) {
|
|
6045
|
+
return {
|
|
6046
|
+
folder: state.folder,
|
|
6047
|
+
pages: state.sockets.size,
|
|
6048
|
+
running: state.activeRun === undefined ? undefined : state.activeRunId
|
|
6049
|
+
};
|
|
6050
|
+
}
|
|
6051
|
+
|
|
5878
6052
|
/**
|
|
5879
6053
|
* Handles `table-preview`: the PDF canvas's live preview of a `table`
|
|
5880
6054
|
* extract's options (`document-view`'s `table-preview.use-case.ts`, studio
|
|
@@ -6114,6 +6288,15 @@ async function startStudioServer(options = {}) {
|
|
|
6114
6288
|
const port = typeof address === 'object' && address !== null ? address.port : 0;
|
|
6115
6289
|
const folderParam = options.initialFolder === undefined ? '' : `&folder=${encodeURIComponent(options.initialFolder)}`;
|
|
6116
6290
|
const base = `http://127.0.0.1:${port}/?token=${token}`;
|
|
6291
|
+
const instancesDir = options.instancesDir ?? defaultInstancesDirectory();
|
|
6292
|
+
const instance = {
|
|
6293
|
+
pid: process.pid,
|
|
6294
|
+
url: `http://127.0.0.1:${port}`,
|
|
6295
|
+
token,
|
|
6296
|
+
folder: options.initialFolder,
|
|
6297
|
+
startedAt: new Date().toISOString()
|
|
6298
|
+
};
|
|
6299
|
+
await announceInstance(instance, instancesDir);
|
|
6117
6300
|
return {
|
|
6118
6301
|
url: `${base}${folderParam}`,
|
|
6119
6302
|
token,
|
|
@@ -6122,7 +6305,10 @@ async function startStudioServer(options = {}) {
|
|
|
6122
6305
|
const open = target?.open === undefined ? '' : `&open=${encodeURIComponent(target.open)}`;
|
|
6123
6306
|
return `${base}${workspace === undefined ? '' : `&folder=${encodeURIComponent(workspace)}`}${open}`;
|
|
6124
6307
|
},
|
|
6125
|
-
close: () =>
|
|
6308
|
+
close: async () => {
|
|
6309
|
+
await withdrawInstance(instance, instancesDir);
|
|
6310
|
+
await stop(server, state);
|
|
6311
|
+
}
|
|
6126
6312
|
};
|
|
6127
6313
|
}
|
|
6128
6314
|
async function stop(server, state) {
|
|
@@ -6321,6 +6507,18 @@ function dispatch(command, state) {
|
|
|
6321
6507
|
{
|
|
6322
6508
|
return handleStopRecording(state);
|
|
6323
6509
|
}
|
|
6510
|
+
case 'ask-ui':
|
|
6511
|
+
{
|
|
6512
|
+
return Promise.resolve(handleAskUi(state, command));
|
|
6513
|
+
}
|
|
6514
|
+
case 'studio-status':
|
|
6515
|
+
{
|
|
6516
|
+
return Promise.resolve(handleStudioStatus(state));
|
|
6517
|
+
}
|
|
6518
|
+
case 'run-status':
|
|
6519
|
+
{
|
|
6520
|
+
return Promise.resolve(handleRunStatus(state, command));
|
|
6521
|
+
}
|
|
6324
6522
|
}
|
|
6325
6523
|
}
|
|
6326
6524
|
function respondJson(response, status, body) {
|
|
@@ -6388,5 +6586,5 @@ async function main(argv, options = {}) {
|
|
|
6388
6586
|
process.on('SIGTERM', shutdown);
|
|
6389
6587
|
}
|
|
6390
6588
|
|
|
6391
|
-
export { chooseFolderCommandSchema, chooseFolderViewSchema, deckChartSeriesViewSchema, deckChartViewSchema, deckDocumentViewSchema, deckPreviewCommandSchema, deckPreviewOptionsSchema, deckShapeViewSchema, deckSlideViewSchema, deckTablePreviewMatchSchema, deckTablePreviewViewSchema, deckViewCommandSchema, detectUrlCommandSchema, detectUrlViewSchema, documentTreeCommandSchema, documentTreeNodeViewSchema, documentTreeViewSchema, domTreeNodeViewSchema, explainWhyCommandSchema, fetchStartPage, fetchStartPageCommandSchema, fieldPickSchema, fieldTraceSchema, gridCellViewSchema, gridMergeViewSchema, gridPreviewCommandSchema, gridPreviewOptionsSchema, gridRowsCommandSchema, gridRowsViewSchema, gridSheetViewSchema, gridTablePreviewMatchSchema, gridTablePreviewViewSchema, gridViewCommandSchema, importDocument, importDocumentCommandSchema, importDocumentViewSchema, inferSelectorCommandSchema, inferSelectorViewSchema, inspectPageCommandSchema, inspectViewSchema, loadRecipePair, loadedHooksSchema, main, missingWhyTargetSchema, observedResponseSchema, openPathCommandSchema, openPathViewSchema, openWorkspace, openWorkspaceCommandSchema, outlineBracketSchema, outlineCardSchema, outlineNodeSchema, outlineViewSchema, pageDataFindingSchema, pdfCellViewSchema, pdfDocumentViewSchema, pdfPageViewSchema, pdfRowViewSchema, pdfViewCommandSchema, recipeIssueSchema, recipeListingSchema, recordEventSchema, recordRejectedEventSchema, recordingCardEventSchema, recordingNoteEventSchema, recordingStoppedEventSchema, regionPreviewCommandSchema, regionPreviewMatchSchema, regionPreviewViewSchema, rejectedWhyTargetSchema, responsesSeenCommandSchema, responsesSeenViewSchema, runFinishedEventSchema, runSample, runSampleCommandSchema, sampleBudgetSchema, sampleRunRecordSchema, sampleRunViewSchema, saveOutlineCommandSchema, saveRecipe, saveRecipeCommandSchema, selectorAlternativeSchema, sentencePartSchema, snapshotViewSchema, startPageViewSchema, startRecordingCommandSchema, startStudioServer, stopRecordingCommandSchema, stopRunCommandSchema, stoppedBySchema, studioCommandSchema, studioEventSchema, tablePreviewBandSchema, tablePreviewCommandSchema, tablePreviewMatchSchema, tablePreviewOptionsSchema, tablePreviewTableSchema, tablePreviewViewSchema, takeSnapshotCommandSchema, traceLineEventSchema, verifySelectorCommandSchema, verifySelectorViewSchema, whyTargetSchema, whyViewSchema, workbookDocumentViewSchema, workspaceChangedEventSchema, workspaceViewSchema };
|
|
6589
|
+
export { chooseFolderCommandSchema, chooseFolderViewSchema, deckChartSeriesViewSchema, deckChartViewSchema, deckDocumentViewSchema, deckPreviewCommandSchema, deckPreviewOptionsSchema, deckShapeViewSchema, deckSlideViewSchema, deckTablePreviewMatchSchema, deckTablePreviewViewSchema, deckViewCommandSchema, defaultInstancesDirectory, detectUrlCommandSchema, detectUrlViewSchema, documentTreeCommandSchema, documentTreeNodeViewSchema, documentTreeViewSchema, domTreeNodeViewSchema, explainWhyCommandSchema, fetchStartPage, fetchStartPageCommandSchema, fieldPickSchema, fieldTraceSchema, gridCellViewSchema, gridMergeViewSchema, gridPreviewCommandSchema, gridPreviewOptionsSchema, gridRowsCommandSchema, gridRowsViewSchema, gridSheetViewSchema, gridTablePreviewMatchSchema, gridTablePreviewViewSchema, gridViewCommandSchema, importDocument, importDocumentCommandSchema, importDocumentViewSchema, inferSelectorCommandSchema, inferSelectorViewSchema, inspectPageCommandSchema, inspectViewSchema, loadRecipePair, loadedHooksSchema, main, missingWhyTargetSchema, observedResponseSchema, openPathCommandSchema, openPathViewSchema, openWorkspace, openWorkspaceCommandSchema, outlineBracketSchema, outlineCardSchema, outlineNodeSchema, outlineViewSchema, pageDataFindingSchema, pdfCellViewSchema, pdfDocumentViewSchema, pdfPageViewSchema, pdfRowViewSchema, pdfViewCommandSchema, recipeIssueSchema, recipeListingSchema, recordEventSchema, recordRejectedEventSchema, recordingCardEventSchema, recordingNoteEventSchema, recordingStoppedEventSchema, regionPreviewCommandSchema, regionPreviewMatchSchema, regionPreviewViewSchema, rejectedWhyTargetSchema, responsesSeenCommandSchema, responsesSeenViewSchema, runFinishedEventSchema, runSample, runSampleCommandSchema, sampleBudgetSchema, sampleRunRecordSchema, sampleRunViewSchema, saveOutlineCommandSchema, saveRecipe, saveRecipeCommandSchema, selectorAlternativeSchema, sentencePartSchema, snapshotViewSchema, startPageViewSchema, startRecordingCommandSchema, startStudioServer, stopRecordingCommandSchema, stopRunCommandSchema, stoppedBySchema, studioCommandSchema, studioEventSchema, tablePreviewBandSchema, tablePreviewCommandSchema, tablePreviewMatchSchema, tablePreviewOptionsSchema, tablePreviewTableSchema, tablePreviewViewSchema, takeSnapshotCommandSchema, traceLineEventSchema, verifySelectorCommandSchema, verifySelectorViewSchema, whyTargetSchema, whyViewSchema, workbookDocumentViewSchema, workspaceChangedEventSchema, workspaceViewSchema };
|
|
6392
6590
|
//# sourceMappingURL=index.esm.js.map
|