@opencraw/cli 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -1,13 +1,17 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/russoedu/open.craw/main/docs/assets/opencraw-logo.svg" alt="OpenCraw" width="360">
3
+ </p>
4
+
1
5
  # @opencraw/cli
2
6
 
3
- Command-line tools for [`@opencraw/core`](../core): probe a page for scrapable data, validate recipes,
7
+ Command-line tools for [`@opencraw/core`](https://github.com/russoedu/open.craw/blob/main/packages/core): probe a page for scrapable data, validate recipes,
4
8
  and run a crawl from the terminal.
5
9
 
6
10
  ## Install
7
11
 
8
12
  ```sh
9
13
  npm install -g @opencraw/cli
10
- # or, inside this workspace: node packages/cli/bin/opencraw.mjs <command>
14
+ # or, inside this workspace, after `npm run cli:build`: node packages/cli/bin/opencraw.mjs <command>
11
15
  ```
12
16
 
13
17
  ## Commands
@@ -16,6 +20,7 @@ npm install -g @opencraw/cli
16
20
  opencraw validate <recipe files or directories...>
17
21
  opencraw run <recipe files or directories...> [options]
18
22
  opencraw probe <url> [options]
23
+ opencraw diff <previous.jsonl> <current.jsonl> [--key a,b] [--ignore a,b] [--changes <file>]
19
24
  ```
20
25
 
21
26
  ### `validate`
@@ -43,11 +48,11 @@ input recipes) and reports what happened.
43
48
  | `--dry-run` | One record per input recipe, printed with the scope it was mapped from — for checking a recipe under construction without a full run. |
44
49
  | `--trace` | Print the crawl trace to stderr. |
45
50
  | `--headed` | Show the browser instead of running headless. |
46
- | `--parallel <n>` | Run this many input recipes at once (default 1). Iterations inside a recipe follow its `limits.concurrency`. See [§3.9 of the authoring guide](../../docs/recipes/authoring.md#39-concurrency). |
47
- | `--retries <n>` | Tries per request that fails in passing (a dropped connection, a timeout, a 429, a 5xx), for recipes whose `limits.retry` says nothing. Default 3; `1` turns retrying off. See [§7 of the authoring guide](../../docs/recipes/authoring.md#7-policies-what-happens-when-something-is-missing-or-fails). |
48
- | `--profiles <dir>` | Where the persistent browser profiles of `session.browserProfile` live (or `OPENCRAW_PROFILES`; default `.opencraw/profiles`). See [access.md](../../docs/recipes/access.md#persistent-browser-profiles). |
49
- | `--host-delay <ms>`, `--host-concurrency <n>` | Per-site politeness across every recipe: at least `ms` between two requests to one site, at most `n` in flight. They override the access config's `throttle` defaults. See [access.md](../../docs/recipes/access.md#throttling-per-site). |
50
- | `--plugins <file>` (or `--hooks`) | A plugins module: named exports `hooks` (the recipes' hook steps and transforms), `accessPlugins` (for `{ kind: "plugin" }` access profiles) and `captchaSolvers`. A module whose default export is `{ name: function }` is read as hooks alone. `OPENCRAW_PLUGINS` / `OPENCRAW_HOOKS` when not given; `probe` reads it too. It runs as your code; load only files you trust. See [§6 of the authoring guide](../../docs/recipes/authoring.md#6-hooks). |
51
+ | `--parallel <n>` | Run this many input recipes at once (default 1). Iterations inside a recipe follow its `limits.concurrency`. See [§3.9 of the authoring guide](https://github.com/russoedu/open.craw/blob/main/docs/recipes/authoring.md#39-concurrency). |
52
+ | `--retries <n>` | Tries per request that fails in passing (a dropped connection, a timeout, a 429, a 5xx), for recipes whose `limits.retry` says nothing. Default 3; `1` turns retrying off. See [§7 of the authoring guide](https://github.com/russoedu/open.craw/blob/main/docs/recipes/authoring.md#7-policies-what-happens-when-something-is-missing-or-fails). |
53
+ | `--profiles <dir>` | Where the persistent browser profiles of `session.browserProfile` live (or `OPENCRAW_PROFILES`; default `.opencraw/profiles`). See [access.md](https://github.com/russoedu/open.craw/blob/main/docs/recipes/access.md#persistent-browser-profiles). |
54
+ | `--host-delay <ms>`, `--host-concurrency <n>` | Per-site politeness across every recipe: at least `ms` between two requests to one site, at most `n` in flight. They override the access config's `throttle` defaults. See [access.md](https://github.com/russoedu/open.craw/blob/main/docs/recipes/access.md#throttling-per-site). |
55
+ | `--plugins <file>` (or `--hooks`) | A plugins module: named exports `hooks` (the recipes' hook steps and transforms), `accessPlugins` (for `{ kind: "plugin" }` access profiles) and `captchaSolvers`. A module whose default export is `{ name: function }` is read as hooks alone. `OPENCRAW_PLUGINS` / `OPENCRAW_HOOKS` when not given; `probe` reads it too. It runs as your code; load only files you trust. See [§6 of the authoring guide](https://github.com/russoedu/open.craw/blob/main/docs/recipes/authoring.md#6-hooks). |
51
56
 
52
57
  ```sh
53
58
  opencraw run recipes/ --out out/products.jsonl --trace
@@ -113,7 +118,7 @@ opencraw probe ./decks/incentivi.pptx
113
118
  ```
114
119
 
115
120
  Use it before writing an input recipe, to find the shape a site's data actually takes (§9 of
116
- [the authoring guide](../../docs/recipes/authoring.md)).
121
+ [the authoring guide](https://github.com/russoedu/open.craw/blob/main/docs/recipes/authoring.md)).
117
122
 
118
123
  ### Options common to `run` and `probe`
119
124
 
@@ -121,7 +126,7 @@ Use it before writing an input recipe, to find the shape a site's data actually
121
126
  |---|---|
122
127
  | `--browser-path <path>` | A browser binary other than the one Playwright installed (or `OPENCRAW_CHROMIUM`). |
123
128
  | `--insecure-tls` | Accept an intercepting proxy's certificate (or `OPENCRAW_INSECURE_TLS=1`). |
124
- | `--access <file>` | An access config: proxy profiles, with credentials as `{{env.NAME}}` (or `OPENCRAW_ACCESS`). See [access.md](../../docs/recipes/access.md). |
129
+ | `--access <file>` | An access config: proxy profiles, with credentials as `{{env.NAME}}` (or `OPENCRAW_ACCESS`). See [access.md](https://github.com/russoedu/open.craw/blob/main/docs/recipes/access.md). |
125
130
  | `--access-profile <name>` | The profile used by recipes that name none, overriding the config's `default`. `probe` uses it for its fetch. |
126
131
  | `--user-agent <ua>` | The user agent to send. |
127
132
 
package/dist/index.esm.js CHANGED
@@ -558,6 +558,41 @@ function tryParse(text) {
558
558
  }
559
559
  }
560
560
 
561
+ /**
562
+ * Whether a cell holds a figure (an amount, a count, a date written in
563
+ * digits) rather than words: it starts with a number, or its digits are at
564
+ * least as many as its letters. `2,820`, `$560`, `2659 $560`, `(£0.58m)`,
565
+ * `-3 days` and `2026` are figures; `FY26 Lodging Rate`,
566
+ * `FM Classes A, B1 & C3` and `(APRIL 1, 2020)` are words that happen to
567
+ * contain digits.
568
+ *
569
+ * @param cell - The cell's text.
570
+ * @returns Whether it is a figure.
571
+ */
572
+ function isFigure(cell) {
573
+ if (/^[\s(+\-–−<>=~$€£¥]*\d/u.test(cell)) return true;
574
+ const digits = cell.match(/\d/g)?.length ?? 0;
575
+ return digits > 0 && digits >= (cell.match(/\p{L}/gu)?.length ?? 0);
576
+ }
577
+ /**
578
+ * A row that reads like a table's header: at least two cells, none a figure.
579
+ *
580
+ * @param cells - The row's non-empty cells.
581
+ * @returns Whether it could be a header.
582
+ */
583
+ function isHeaderRow(cells) {
584
+ return cells.length >= 2 && cells.every(cell => !isFigure(cell));
585
+ }
586
+ /**
587
+ * A row that reads like a table's data: at least two cells, one a figure.
588
+ *
589
+ * @param cells - The row's non-empty cells.
590
+ * @returns Whether it looks like data.
591
+ */
592
+ function isDataRow(cells) {
593
+ return cells.length >= 2 && cells.some(cell => isFigure(cell));
594
+ }
595
+
561
596
  /** Rows shown per sheet in a probe, at most. */
562
597
  const ROW_LIMIT$1 = 20;
563
598
  /** How far below a header row a probe looks for the first data row. */
@@ -596,23 +631,34 @@ function describeWorkbook(document) {
596
631
  function headersOf(sheet) {
597
632
  const rows = filledRows(sheet);
598
633
  const merged = mergedRows(sheet);
599
- return rows.flatMap(({
634
+ const headers = [];
635
+ // The table a row belongs to: the header it follows, until an empty row ends the block.
636
+ let table;
637
+ for (const [position, {
600
638
  index,
601
639
  cells
602
- }, position) => {
603
- if (cells.length < 2 || cells.some(cell => /\d/.test(cell))) return [];
604
- if (rows.slice(position + 1, position + 1 + LOOKAHEAD).every(row => !(row.cells.length >= 2 && row.cells.some(cell => /\d/.test(cell))))) return [];
640
+ }] of rows.entries()) {
641
+ const previous = rows[position - 1]?.index;
642
+ if (previous === undefined || index > previous + 1) table = undefined;
643
+ const secondHeaderRow = table?.merged === true && index === table.header + 1;
644
+ if (table !== undefined && !secondHeaderRow) continue;
645
+ if (!isHeaderRow(cells) || rows.slice(position + 1, position + 1 + LOOKAHEAD).every(row => !isDataRow(row.cells))) continue;
605
646
  const header = {
606
647
  sheet: sheet.name,
607
648
  row: index + 1,
608
649
  text: cells.join(' | '),
609
650
  selector: `^${escape$1(cells[0])}`
610
651
  };
611
- return [merged.has(index) ? {
652
+ headers.push(merged.has(index) ? {
612
653
  ...header,
613
654
  hint: 'merged header cells: try "headerRows": 2'
614
- } : header];
615
- });
655
+ } : header);
656
+ table ??= {
657
+ header: index,
658
+ merged: merged.has(index)
659
+ };
660
+ }
661
+ return headers;
616
662
  }
617
663
  /** The non-empty rows, as their non-empty cells, whitespace collapsed. */
618
664
  function filledRows(sheet) {
@@ -826,7 +872,7 @@ function describePdf(document) {
826
872
  const headers = rows.filter(({
827
873
  row,
828
874
  next
829
- }) => row.cells.length >= 2 && row.cells.every(cell => !/\d/.test(cell.text)) && next.some(candidate => dataRow(candidate)));
875
+ }) => isHeaderRow(row.cells.map(cell => cell.text)) && next.some(candidate => isDataRow(candidate.cells.map(cell => cell.text))));
830
876
  return {
831
877
  pages: document.pages.length,
832
878
  rows: rows.slice(0, ROW_LIMIT).map(({
@@ -846,10 +892,6 @@ function describePdf(document) {
846
892
  }))
847
893
  };
848
894
  }
849
- /** A row of at least two cells, one with a digit: data, not a heading. */
850
- function dataRow(row) {
851
- return row.cells.length >= 2 && row.cells.some(cell => /\d/.test(cell.text));
852
- }
853
895
  function escape(text) {
854
896
  return text.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw`\$&`);
855
897
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Whether a cell holds a figure (an amount, a count, a date written in
3
+ * digits) rather than words: it starts with a number, or its digits are at
4
+ * least as many as its letters. `2,820`, `$560`, `2659 $560`, `(£0.58m)`,
5
+ * `-3 days` and `2026` are figures; `FY26 Lodging Rate`,
6
+ * `FM Classes A, B1 & C3` and `(APRIL 1, 2020)` are words that happen to
7
+ * contain digits.
8
+ *
9
+ * @param cell - The cell's text.
10
+ * @returns Whether it is a figure.
11
+ */
12
+ export declare function isFigure(cell: string): boolean;
13
+ /**
14
+ * A row that reads like a table's header: at least two cells, none a figure.
15
+ *
16
+ * @param cells - The row's non-empty cells.
17
+ * @returns Whether it could be a header.
18
+ */
19
+ export declare function isHeaderRow(cells: readonly string[]): boolean;
20
+ /**
21
+ * A row that reads like a table's data: at least two cells, one a figure.
22
+ *
23
+ * @param cells - The row's non-empty cells.
24
+ * @returns Whether it looks like data.
25
+ */
26
+ export declare function isDataRow(cells: readonly string[]): boolean;
27
+ //# sourceMappingURL=table-header.policy.d.ts.map
@@ -19,9 +19,10 @@ export interface WorkbookFindings {
19
19
  text: string;
20
20
  }[];
21
21
  /**
22
- * Rows of text cells (no digit) with a data row (a digit) just below: likely
23
- * table headers, with the `sheet` and `selector` a `table` extract needs, and
24
- * a hint when the header has merged cells (a header over two rows).
22
+ * Rows of words (no cell a figure) with a data row (a figure) just below:
23
+ * likely table headers, with the `sheet` and `selector` a `table` extract
24
+ * needs, and a hint when the header has merged cells (a header over two
25
+ * rows). The rows right under a header are its table's body, not headers.
25
26
  */
26
27
  headers: {
27
28
  sheet: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencraw/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.esm.js",
6
6
  "module": "./dist/index.esm.js",
@@ -24,7 +24,7 @@
24
24
  "!**/*.js.map"
25
25
  ],
26
26
  "dependencies": {
27
- "@opencraw/core": "^0.1.9",
27
+ "@opencraw/core": "^0.1.11",
28
28
  "playwright": "^1.63.0"
29
29
  },
30
30
  "publishConfig": {