@opencraw/cli 0.1.17 → 0.1.19

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.esm.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { parseArgs } from 'node:util';
2
2
  import { writeFile } from 'node:fs/promises';
3
- import { readableKey, readRecordsFile, diffRecords, loadAccessConfig, htmlTableSheets, HostAllowlist, AccessBroker, HttpClient, parseXml, BrowserClient, readRecipeSource, parseOutputRecipe, parseInputRecipe, bindRecipeSet, memorySink, jsonLinesSink, createCrawler, RecipeSet, variantLabel, diffOptionsFor, RecipeValidationError, RecipeBindingError, traceLine } from '@opencraw/core';
3
+ import { readableKey, readRecordsFile, diffRecords, loadAccessConfig, HostAllowlist, AccessBroker, HttpClient, parseXml, BrowserClient, readRecipeSource, parseOutputRecipe, parseInputRecipe, bindRecipeSet, memorySink, jsonLinesSink, createCrawler, RecipeSet, variantLabel, diffOptionsFor, RecipeValidationError, RecipeBindingError, traceLine } from '@opencraw/core';
4
4
  import { resolve } from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
+ import { describePdf, findData, describeWorkbook, describeDeck, describeJson, describeXml, describeHtml } from '@opencraw/probe';
7
+ export { findData } from '@opencraw/probe';
6
8
  import { readFileSync } from 'node:fs';
7
9
 
8
10
  const USAGE = `opencraw <command> [options]
@@ -457,455 +459,6 @@ function hookExports(exported) {
457
459
  return Object.fromEntries(named);
458
460
  }
459
461
 
460
- /**
461
- * Finds where a fetched page's data lives: JSON-LD blocks, inline JSON objects
462
- * big enough to matter, URLs that look like a data endpoint, script hosts and
463
- * links whose path suggests an API. Pattern-matching only; it never fetches.
464
- */
465
- const MIN_INLINE_SIZE = 200;
466
- const JSON_LD = /<script[^\s>]*\s+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
467
- const INLINE_SCRIPT = /<script(?![^>]*\ssrc=)(?![^>]*\stype=["']application\/ld\+json["'])[^>]*>([\s\S]*?)<\/script>/gi;
468
- const DATA_ATTR = /data-[\w-]+=["'](\{[\s\S]*?\})["']/g;
469
- const SCRIPT_SRC = /<script[^\s>]*\s+src=["']([^"']+)["']/gi;
470
- const HREF_OR_JSON_URL = /["'](https?:\/\/[^"'\s]+\.json[^"'\s]*|\/[^"'\s]*\.json[^"'\s]*)["']/g;
471
- const LINK = /<a[^\s>]*\s+href=["']([^"']+)["']/gi;
472
- /**
473
- * Scans a page's HTML for data-shaped things.
474
- *
475
- * @param html - The document.
476
- * @returns What was found, each list capped at a handful of entries.
477
- */
478
- function findData(html) {
479
- return {
480
- jsonLd: jsonLdBlocks(html),
481
- inlineJson: inlineJsonBlocks(html),
482
- jsonUrls: unique(matchesOf(HREF_OR_JSON_URL, html)).slice(0, 10),
483
- scriptHosts: scriptHosts(html),
484
- apiLinks: apiLinks(html)
485
- };
486
- }
487
- function jsonLdBlocks(html) {
488
- const findings = [];
489
- for (const text of matchesOf(JSON_LD, html)) {
490
- const parsed = tryParse(text);
491
- if (parsed === undefined) continue;
492
- findings.push(...entriesOf(parsed));
493
- }
494
- return findings.slice(0, 10);
495
- }
496
- /** Every plain object in a JSON-LD block (it may be one object or an array of them), as a finding. */
497
- function entriesOf(parsed) {
498
- const list = Array.isArray(parsed) ? parsed : [parsed];
499
- return list.flatMap(entry => {
500
- if (typeof entry !== 'object' || entry === null) return [];
501
- const record = entry;
502
- const type = record['@type'];
503
- return [{
504
- types: Array.isArray(type) ? type.join(',') : String(type ?? '?'),
505
- keys: Object.keys(record).length
506
- }];
507
- });
508
- }
509
- function inlineJsonBlocks(html) {
510
- const findings = [];
511
- for (const text of matchesOf(INLINE_SCRIPT, html)) findings.push(...objectsIn(text, 'inline script'));
512
- for (const text of matchesOf(DATA_ATTR, html, 1)) findings.push(...objectsIn(text, 'data-* attribute'));
513
- return findings.filter(finding => finding.size >= MIN_INLINE_SIZE).sort((a, b) => b.size - a.size).slice(0, 10);
514
- }
515
- /** Every `{...}` in a text that parses as JSON, by scanning for balanced braces (regex cannot nest). */
516
- function objectsIn(text, where) {
517
- const found = [];
518
- for (let at = text.indexOf('{'); at !== -1; at = text.indexOf('{', at + 1)) {
519
- const end = matchingBrace(text, at);
520
- if (end === -1) continue;
521
- const candidate = text.slice(at, end + 1);
522
- const parsed = tryParse(candidate);
523
- if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
524
- found.push({
525
- where,
526
- keys: Object.keys(parsed),
527
- size: candidate.length
528
- });
529
- at = end;
530
- }
531
- }
532
- return found;
533
- }
534
- function matchingBrace(text, open) {
535
- let depth = 0;
536
- for (let at = open; at < text.length; at += 1) {
537
- if (text[at] === '{') depth += 1;else if (text[at] === '}') {
538
- depth -= 1;
539
- if (depth === 0) return at;
540
- }
541
- }
542
- return -1;
543
- }
544
- function scriptHosts(html) {
545
- const hosts = matchesOf(SCRIPT_SRC, html).map(src => {
546
- try {
547
- return new URL(src).host;
548
- } catch {
549
- return;
550
- }
551
- }).filter(host => host !== undefined);
552
- return unique(hosts).slice(0, 10);
553
- }
554
- function apiLinks(html) {
555
- return unique(matchesOf(LINK, html).filter(href => /\bapi\b/i.test(href))).slice(0, 10);
556
- }
557
- function matchesOf(pattern, text, group = 1) {
558
- return Array.from(text.matchAll(pattern), match => match[group]);
559
- }
560
- function unique(values) {
561
- return [...new Set(values)];
562
- }
563
- function tryParse(text) {
564
- try {
565
- return JSON.parse(text.trim());
566
- } catch {
567
- return undefined;
568
- }
569
- }
570
-
571
- /**
572
- * Whether a cell holds a figure (an amount, a count, a date written in
573
- * digits) rather than words: it starts with a number, or its digits are at
574
- * least as many as its letters. `2,820`, `$560`, `2659 $560`, `(£0.58m)`,
575
- * `-3 days` and `2026` are figures; `FY26 Lodging Rate`,
576
- * `FM Classes A, B1 & C3` and `(APRIL 1, 2020)` are words that happen to
577
- * contain digits.
578
- *
579
- * @param cell - The cell's text.
580
- * @returns Whether it is a figure.
581
- */
582
- function isFigure(cell) {
583
- if (/^[\s(+\-–−<>=~$€£¥]*\d/u.test(cell)) return true;
584
- const digits = cell.match(/\d/g)?.length ?? 0;
585
- return digits > 0 && digits >= (cell.match(/\p{L}/gu)?.length ?? 0);
586
- }
587
- /**
588
- * A row that reads like a table's header: at least two cells, none a figure.
589
- *
590
- * @param cells - The row's non-empty cells.
591
- * @returns Whether it could be a header.
592
- */
593
- function isHeaderRow(cells) {
594
- return cells.length >= 2 && cells.every(cell => !isFigure(cell));
595
- }
596
- /**
597
- * A row that reads like a table's data: at least two cells, one a figure.
598
- *
599
- * @param cells - The row's non-empty cells.
600
- * @returns Whether it looks like data.
601
- */
602
- function isDataRow(cells) {
603
- return cells.length >= 2 && cells.some(cell => isFigure(cell));
604
- }
605
-
606
- /** Rows shown per sheet in a probe, at most. */
607
- const ROW_LIMIT$1 = 20;
608
- /** How far below a header row a probe looks for the first data row. */
609
- const LOOKAHEAD = 3;
610
- /**
611
- * Summarises a workbook for someone writing a recipe: its sheets, what their
612
- * rows look like and where the tables start, so a `table` extract's `sheet`,
613
- * `selector` and columns can be read off the result instead of guessed.
614
- *
615
- * @param document - The read workbook.
616
- * @returns The findings.
617
- */
618
- function describeWorkbook(document) {
619
- const visible = document.sheets.filter(sheet => sheet.hidden !== true);
620
- return {
621
- ...(document.csv !== undefined && {
622
- csv: document.csv
623
- }),
624
- sheets: document.sheets.map(sheet => ({
625
- name: sheet.name,
626
- hidden: sheet.hidden === true,
627
- rows: sheet.rows.length,
628
- columns: Math.max(0, ...sheet.rows.map(row => row.length))
629
- })),
630
- rows: visible.flatMap(sheet => filledRows(sheet).slice(0, ROW_LIMIT$1).map(({
631
- index,
632
- cells
633
- }) => ({
634
- sheet: sheet.name,
635
- row: index + 1,
636
- text: cells.join(' | ')
637
- }))),
638
- headers: visible.flatMap(sheet => headersOf(sheet))
639
- };
640
- }
641
- function headersOf(sheet) {
642
- const rows = filledRows(sheet);
643
- const merged = mergedRows(sheet);
644
- const headers = [];
645
- // The table a row belongs to: the header it follows, until an empty row ends the block.
646
- let table;
647
- for (const [position, {
648
- index,
649
- cells
650
- }] of rows.entries()) {
651
- const previous = rows[position - 1]?.index;
652
- if (previous === undefined || index > previous + 1) table = undefined;
653
- const secondHeaderRow = table?.merged === true && index === table.header + 1;
654
- if (table !== undefined && !secondHeaderRow) continue;
655
- if (!isHeaderRow(cells) || rows.slice(position + 1, position + 1 + LOOKAHEAD).every(row => !isDataRow(row.cells))) continue;
656
- const header = {
657
- sheet: sheet.name,
658
- row: index + 1,
659
- text: cells.join(' | '),
660
- selector: `^${escape$1(cells[0])}`
661
- };
662
- headers.push(merged.has(index) ? {
663
- ...header,
664
- hint: 'merged header cells: try "headerRows": 2'
665
- } : header);
666
- table ??= {
667
- header: index,
668
- merged: merged.has(index)
669
- };
670
- }
671
- return headers;
672
- }
673
- /** The non-empty rows, as their non-empty cells, whitespace collapsed. */
674
- function filledRows(sheet) {
675
- const hidden = new Set(sheet.hiddenRows);
676
- return sheet.rows.flatMap((row, index) => {
677
- const cells = row.map(cell => String(cell).replaceAll(/\s+/g, ' ').trim()).filter(cell => cell !== '');
678
- return cells.length === 0 || hidden.has(index) ? [] : [{
679
- index,
680
- cells
681
- }];
682
- });
683
- }
684
- /** Rows a merged range starts on. */
685
- function mergedRows(sheet) {
686
- const rows = new Set();
687
- const merges = sheet.merges ?? [];
688
- for (const reference of merges) {
689
- const match = /^\$?[A-Z]+\$?(\d+)/i.exec(reference);
690
- if (match !== null) rows.add(Number(match[1]) - 1);
691
- }
692
- return rows;
693
- }
694
- function escape$1(text) {
695
- return text.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw`\$&`);
696
- }
697
-
698
- /** Short text boxes on one slide from which a probe suggests reading them as a table. */
699
- const GRID_BOXES = 6;
700
- const SHORT_TEXT = 40;
701
- /**
702
- * Summarises a deck for someone writing a recipe: its slides, where the native
703
- * tables start, what the charts hold, and which slides lay text boxes out as
704
- * a table.
705
- *
706
- * @param document - The read deck.
707
- * @returns The findings.
708
- */
709
- function describeDeck(document) {
710
- return {
711
- width: document.width,
712
- height: document.height,
713
- slides: document.slides.map(slide => ({
714
- number: slide.number,
715
- title: slide.title ?? '',
716
- hidden: slide.hidden,
717
- shapes: slide.shapes.length,
718
- tables: slide.tables.length,
719
- charts: slide.charts.length
720
- })),
721
- headers: document.slides.flatMap(slide => describeWorkbook({
722
- sheets: slide.tables
723
- }).headers.map(({
724
- sheet: _sheet,
725
- row: _row,
726
- ...header
727
- }) => ({
728
- slide: slide.number,
729
- ...header
730
- }))),
731
- charts: document.slides.flatMap(slide => slide.charts.map(chart => ({
732
- slide: slide.number,
733
- type: chart.type,
734
- ...(chart.title !== undefined && {
735
- title: chart.title
736
- }),
737
- series: chart.series.map(series => ({
738
- name: series.name,
739
- points: series.values.length
740
- }))
741
- }))),
742
- grids: document.slides.flatMap(slide => {
743
- const boxes = slide.shapes.filter(shape => shape.placeholder === undefined && shape.text.length <= SHORT_TEXT).length;
744
- return boxes >= GRID_BOXES ? [{
745
- slide: slide.number,
746
- title: slide.title ?? '',
747
- boxes
748
- }] : [];
749
- })
750
- };
751
- }
752
-
753
- const SECTION = /<section data-heading="([^"]*)" data-level="(\d)">/g;
754
- const FRONT_MATTER = /<script type="application\/json" data-front-matter>([\s\S]*?)<\/script>/;
755
- /**
756
- * Summarises an HTML document for a `table` extract, and, when it is rendered
757
- * Markdown or a Word document, its outline (and Markdown's front matter).
758
- *
759
- * @param html - The document.
760
- * @param markdown - Whether it was rendered from Markdown or Word, and so has sections.
761
- * @returns The findings.
762
- */
763
- function describeHtml(html, markdown) {
764
- const tables = htmlTableSheets(html);
765
- const headers = describeWorkbook({
766
- sheets: tables
767
- }).headers.map(({
768
- sheet,
769
- row: _row,
770
- ...header
771
- }) => ({
772
- table: sheet,
773
- ...header
774
- }));
775
- if (!markdown) return {
776
- tables: headers
777
- };
778
- const outline = Array.from(html.matchAll(SECTION), ([, heading, level]) => ({
779
- level: Number(level),
780
- heading: unescape(heading),
781
- selector: `section[data-heading='${unescape(heading).replaceAll("'", String.raw`\'`)}' i]`
782
- }));
783
- const front = FRONT_MATTER.exec(html)?.[1];
784
- const data = front === undefined ? undefined : JSON.parse(front);
785
- return {
786
- tables: headers,
787
- outline,
788
- frontMatter: typeof data === 'object' && data !== null && !Array.isArray(data) ? Object.keys(data) : []
789
- };
790
- }
791
- function unescape(text) {
792
- return text.replaceAll('&quot;', '"').replaceAll('&lt;', '<').replaceAll('&amp;', '&');
793
- }
794
-
795
- /** How deep a probe describes a JSON document. */
796
- const DEPTH$1 = 4;
797
- /** Keys shown per object, at most. */
798
- const KEY_LIMIT = 25;
799
- /** Entries of a record list whose keys are compared. */
800
- const SAMPLE = 50;
801
- /** A sample value longer than this is cut. */
802
- const SAMPLE_CHARS$1 = 40;
803
- /**
804
- * Summarises JSON data for someone writing a recipe: its shape, and where its
805
- * record lists are, so a `jsonpath` selector can be read off the result.
806
- *
807
- * @param data - The parsed document.
808
- * @param format - What it was read as (`json`, `jsonl`, `yaml`).
809
- * @returns The findings.
810
- */
811
- function describeJson(data, format) {
812
- const tree = [];
813
- const lists = [];
814
- walk$1(data, '$', 0, tree, lists);
815
- return {
816
- format,
817
- type: typeOf(data),
818
- tree,
819
- lists: lists.sort((first, second) => second.length - first.length)
820
- };
821
- }
822
- function walk$1(value, path, depth, tree, lists) {
823
- if (Array.isArray(value)) {
824
- const objects = value.filter(entry => isObject(entry));
825
- if (objects.length >= 2 && objects.length === value.length) lists.push({
826
- path: `${path}[*]`,
827
- length: value.length,
828
- keys: sharedKeys(objects.slice(0, SAMPLE))
829
- });
830
- if (depth > 0) tree.push(`${path} ${typeOf(value)}`);
831
- if (depth < DEPTH$1 && value.length > 0) walk$1(value[0], `${path}[*]`, depth + 1, tree, lists);
832
- return;
833
- }
834
- if (isObject(value)) {
835
- if (depth > 0) tree.push(`${path} object`);
836
- if (depth >= DEPTH$1) return;
837
- for (const [key, child] of Object.entries(value).slice(0, KEY_LIMIT)) walk$1(child, childPath(path, key), depth + 1, tree, lists);
838
- return;
839
- }
840
- tree.push(`${path} ${typeOf(value)}${sample(value)}`);
841
- }
842
- function typeOf(value) {
843
- if (Array.isArray(value)) {
844
- const kinds = [...new Set(value.slice(0, SAMPLE).map(entry => typeOf(entry).split(' ', 1)[0]))];
845
- return `array (${value.length})${kinds.length === 0 ? '' : ` of ${kinds.join(' | ')}`}`;
846
- }
847
- if (value === null) return 'null';
848
- return typeof value === 'object' ? 'object' : typeof value;
849
- }
850
- function sample(value) {
851
- if (value === null || value === undefined) return '';
852
- const text = JSON.stringify(value);
853
- return ` ${text.length > SAMPLE_CHARS$1 ? `${text.slice(0, SAMPLE_CHARS$1)}…` : text}`;
854
- }
855
- function sharedKeys(objects) {
856
- const [first, ...rest] = objects;
857
- return Object.keys(first).filter(key => rest.every(object => Object.hasOwn(object, key)));
858
- }
859
- function childPath(path, key) {
860
- return /^[$A-Z_][\w$]*$/i.test(key) ? `${path}.${key}` : `${path}['${key.replaceAll("'", String.raw`\'`)}']`;
861
- }
862
- function isObject(value) {
863
- return typeof value === 'object' && value !== null && !Array.isArray(value);
864
- }
865
-
866
- /** Rows shown in a probe, at most. */
867
- const ROW_LIMIT = 80;
868
- /**
869
- * Summarises a PDF for someone writing a recipe: what the rows look like, and
870
- * where the tables start, so a `table` extract's selector and columns can be
871
- * read off the result instead of guessed.
872
- *
873
- * @param document - The read PDF.
874
- * @returns The findings.
875
- */
876
- function describePdf(document) {
877
- const rows = document.pages.flatMap(page => page.rows.map((row, index) => ({
878
- page: page.number,
879
- row,
880
- next: page.rows.slice(index + 1, index + 3)
881
- })));
882
- const headers = rows.filter(({
883
- row,
884
- next
885
- }) => isHeaderRow(row.cells.map(cell => cell.text)) && next.some(candidate => isDataRow(candidate.cells.map(cell => cell.text))));
886
- return {
887
- pages: document.pages.length,
888
- rows: rows.slice(0, ROW_LIMIT).map(({
889
- page,
890
- row
891
- }) => ({
892
- page,
893
- text: row.cells.map(cell => cell.text).join(' | ')
894
- })),
895
- headers: headers.map(({
896
- page,
897
- row
898
- }) => ({
899
- page,
900
- text: row.cells.map(cell => cell.text).join(' | '),
901
- selector: `^${escape(row.cells[0].text)}`
902
- }))
903
- };
904
- }
905
- function escape(text) {
906
- return text.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw`\$&`);
907
- }
908
-
909
462
  /**
910
463
  * Renders a probe's findings as text.
911
464
  *
@@ -999,86 +552,6 @@ function section(title, rows) {
999
552
  return [`${title}:`, ...rows, ''].join('\n');
1000
553
  }
1001
554
 
1002
- /** How deep a probe describes an XML document. */
1003
- const DEPTH = 5;
1004
- /** Distinct paths shown, at most. */
1005
- const PATH_LIMIT = 60;
1006
- /** A sample value longer than this is cut. */
1007
- const SAMPLE_CHARS = 40;
1008
- const ELEMENT = 1;
1009
- /**
1010
- * Summarises an XML document for someone writing a recipe: its namespaces
1011
- * (and the `namespaces` a query needs), the elements that repeat (the
1012
- * records a `forEach` walks), and its structure.
1013
- *
1014
- * @param document - A parsed document (`parseXml` from the core), read through the W3C DOM properties.
1015
- * @returns The findings.
1016
- */
1017
- function describeXml(document) {
1018
- const root = document.documentElement;
1019
- if (root === null) return {
1020
- root: '',
1021
- namespaces: {},
1022
- lists: [],
1023
- tree: []
1024
- };
1025
- const paths = new Map();
1026
- walk(root, `/${root.localName ?? ''}`, 1, paths);
1027
- const namespaces = Object.fromEntries(Array.from(root.attributes ?? [], attribute => attribute).filter(attribute => attribute.name === 'xmlns' || attribute.prefix === 'xmlns').map(attribute => [attribute.name === 'xmlns' ? '' : attribute.localName, attribute.value]));
1028
- const lists = [...paths].filter(([, info]) => info.maxSiblings >= 2).map(([path, info]) => ({
1029
- path: `//${path.split('/').slice(-2).join('/')}`.replace(/^\/\/\//, '//'),
1030
- count: info.count,
1031
- children: [...info.children]
1032
- })).sort((first, second) => second.count - first.count);
1033
- const tree = [...paths].slice(0, PATH_LIMIT).map(([path, info]) => `${path}${info.count > 1 ? ` ×${info.count}` : ''}${info.attributes.size > 0 ? ` ${[...info.attributes].map(name => `@${name}`).join(' ')}` : ''}${info.sample === undefined ? '' : ` ${JSON.stringify(info.sample)}`}`);
1034
- const name = root.localName ?? '';
1035
- const sitemap = name === 'urlset' || name === 'sitemapindex' ? {
1036
- kind: name,
1037
- locations: paths.get(`/${name}/${name === 'urlset' ? 'url' : 'sitemap'}/loc`)?.count ?? 0
1038
- } : undefined;
1039
- return {
1040
- root: name,
1041
- namespaces,
1042
- lists,
1043
- tree,
1044
- ...(sitemap !== undefined && {
1045
- sitemap
1046
- })
1047
- };
1048
- }
1049
- function walk(element, path, depth, paths) {
1050
- const info = paths.get(path) ?? {
1051
- count: 0,
1052
- attributes: new Set(),
1053
- children: new Set(),
1054
- maxSiblings: 1
1055
- };
1056
- paths.set(path, info);
1057
- info.count += 1;
1058
- const attributes = element.attributes ?? [];
1059
- for (const attribute of attributes) {
1060
- if (attribute.name !== 'xmlns' && attribute.prefix !== 'xmlns') info.attributes.add(attribute.name);
1061
- }
1062
- const children = [...element.childNodes].filter(child => child.nodeType === ELEMENT);
1063
- if (children.length === 0) {
1064
- const text = (element.textContent ?? '').replaceAll(/\s+/g, ' ').trim();
1065
- if (text !== '' && info.sample === undefined) info.sample = text.length > SAMPLE_CHARS ? `${text.slice(0, SAMPLE_CHARS)}…` : text;
1066
- return;
1067
- }
1068
- const siblings = new Map();
1069
- for (const child of children) {
1070
- const name = child.localName ?? '';
1071
- info.children.add(name);
1072
- siblings.set(name, (siblings.get(name) ?? 0) + 1);
1073
- }
1074
- for (const child of children) {
1075
- const childPath = `${path}/${child.localName ?? ''}`;
1076
- if (depth < DEPTH) walk(child, childPath, depth + 1, paths);
1077
- const childInfo = paths.get(childPath);
1078
- if (childInfo !== undefined) childInfo.maxSiblings = Math.max(childInfo.maxSiblings, siblings.get(child.localName ?? '') ?? 1);
1079
- }
1080
- }
1081
-
1082
555
  const BROWSER_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36';
1083
556
  const OBSERVE_MS = 4000;
1084
557
  /**
@@ -1556,5 +1029,5 @@ function processTerminal() {
1556
1029
  };
1557
1030
  }
1558
1031
 
1559
- export { USAGE, diffReport, findData, loadForRun, loadHooks, loadPlugins, main, parseArguments, probeReport, probeUrl, processTerminal, readRecipeFiles, resolveAccess, runStudio, writeChanges };
1032
+ export { USAGE, diffReport, loadForRun, loadHooks, loadPlugins, main, parseArguments, probeReport, probeUrl, processTerminal, readRecipeFiles, resolveAccess, runStudio, writeChanges };
1560
1033
  //# sourceMappingURL=index.esm.js.map
@@ -1,18 +1,6 @@
1
1
  export { probePage, probeUrl } from './probe-page.use-case.js';
2
2
  export type { ProbeResult, ProbeOptions } from './probe-page.use-case.js';
3
- export { findData } from './find-data.algorithm.js';
4
- export type { ProbeFindings, JsonLdFinding, InlineJsonFinding } from './find-data.algorithm.js';
5
3
  export { probeReport, pdfReport, workbookReport, deckReport, jsonReport, xmlReport } from './probe-report.mapper.js';
6
- export { describeXml } from './xml-findings.mapper.js';
7
- export type { XmlFindings } from './xml-findings.mapper.js';
8
- export { describePdf } from './pdf-findings.mapper.js';
9
- export type { PdfFindings } from './pdf-findings.mapper.js';
10
- export { describeWorkbook } from './workbook-findings.mapper.js';
11
- export type { WorkbookFindings } from './workbook-findings.mapper.js';
12
- export { describeDeck } from './deck-findings.mapper.js';
13
- export type { DeckFindings } from './deck-findings.mapper.js';
14
- export { describeJson } from './json-findings.mapper.js';
15
- export { describeHtml } from './html-findings.mapper.js';
16
- export type { HtmlFindings } from './html-findings.mapper.js';
17
- export type { JsonFindings } from './json-findings.mapper.js';
4
+ export { findData } from '@opencraw/probe';
5
+ export type { ProbeFindings, JsonLdFinding, InlineJsonFinding } from '@opencraw/probe';
18
6
  //# sourceMappingURL=index.d.ts.map
@@ -1,13 +1,8 @@
1
1
  import type { AccessConfig, AccessPlugin } from '@opencraw/core';
2
+ import { findData } from '@opencraw/probe';
3
+ import type { DeckFindings, HtmlFindings, JsonFindings, PdfFindings, WorkbookFindings, XmlFindings } from '@opencraw/probe';
2
4
  import type { CommonOptions } from '../arguments/index.js';
3
5
  import type { Terminal } from '../terminal/index.js';
4
- import { findData } from './find-data.algorithm.js';
5
- import type { DeckFindings } from './deck-findings.mapper.js';
6
- import type { HtmlFindings } from './html-findings.mapper.js';
7
- import type { JsonFindings } from './json-findings.mapper.js';
8
- import type { PdfFindings } from './pdf-findings.mapper.js';
9
- import type { XmlFindings } from './xml-findings.mapper.js';
10
- import type { WorkbookFindings } from './workbook-findings.mapper.js';
11
6
  /** What fetching and, optionally, rendering a page found. */
12
7
  export interface ProbeResult {
13
8
  url: string;
@@ -1,10 +1,4 @@
1
- import type { ProbeFindings } from './find-data.algorithm.js';
2
- import type { DeckFindings } from './deck-findings.mapper.js';
3
- import type { HtmlFindings } from './html-findings.mapper.js';
4
- import type { JsonFindings } from './json-findings.mapper.js';
5
- import type { XmlFindings } from './xml-findings.mapper.js';
6
- import type { PdfFindings } from './pdf-findings.mapper.js';
7
- import type { WorkbookFindings } from './workbook-findings.mapper.js';
1
+ import type { DeckFindings, HtmlFindings, JsonFindings, PdfFindings, ProbeFindings, WorkbookFindings, XmlFindings } from '@opencraw/probe';
8
2
  /**
9
3
  * Renders a probe's findings as text.
10
4
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencraw/cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "main": "./dist/index.esm.js",
6
6
  "module": "./dist/index.esm.js",
@@ -24,11 +24,12 @@
24
24
  "!**/*.js.map"
25
25
  ],
26
26
  "dependencies": {
27
- "@opencraw/core": "^0.1.17",
27
+ "@opencraw/core": "^0.1.19",
28
+ "@opencraw/probe": "^0.0.2",
28
29
  "playwright": "^1.63.0"
29
30
  },
30
31
  "peerDependencies": {
31
- "@opencraw/studio": "^0.0.6"
32
+ "@opencraw/studio": "^0.0.8"
32
33
  },
33
34
  "peerDependenciesMeta": {
34
35
  "@opencraw/studio": {
@@ -1,46 +0,0 @@
1
- import type { DeckDocument } from '@opencraw/core';
2
- /** What a probe shows of a deck (a presentation). */
3
- export interface DeckFindings {
4
- width: number;
5
- height: number;
6
- slides: {
7
- number: number;
8
- title: string;
9
- hidden: boolean;
10
- shapes: number;
11
- tables: number;
12
- charts: number;
13
- }[];
14
- /** The likely header rows of native tables, with the `slide` and `selector` a `table` extract needs. */
15
- headers: {
16
- slide: number;
17
- text: string;
18
- selector: string;
19
- hint?: string;
20
- }[];
21
- charts: {
22
- slide: number;
23
- type: string;
24
- title?: string;
25
- series: {
26
- name: string;
27
- points: number;
28
- }[];
29
- }[];
30
- /** Slides whose short text boxes look like a table: read them with `"shapes": true`. */
31
- grids: {
32
- slide: number;
33
- title: string;
34
- boxes: number;
35
- }[];
36
- }
37
- /**
38
- * Summarises a deck for someone writing a recipe: its slides, where the native
39
- * tables start, what the charts hold, and which slides lay text boxes out as
40
- * a table.
41
- *
42
- * @param document - The read deck.
43
- * @returns The findings.
44
- */
45
- export declare function describeDeck(document: DeckDocument): DeckFindings;
46
- //# sourceMappingURL=deck-findings.mapper.d.ts.map
@@ -1,36 +0,0 @@
1
- /**
2
- * Finds where a fetched page's data lives: JSON-LD blocks, inline JSON objects
3
- * big enough to matter, URLs that look like a data endpoint, script hosts and
4
- * links whose path suggests an API. Pattern-matching only; it never fetches.
5
- */
6
- export interface JsonLdFinding {
7
- /** The `@type` value(s), or `'?'` when the block has none. */
8
- types: string;
9
- /** How many top-level keys the block has. */
10
- keys: number;
11
- }
12
- export interface InlineJsonFinding {
13
- /** Where it was found: a `<script>` with no `type`, or an attribute. */
14
- where: string;
15
- keys: string[];
16
- /** Serialized size, for ranking. */
17
- size: number;
18
- }
19
- export interface ProbeFindings {
20
- jsonLd: JsonLdFinding[];
21
- inlineJson: InlineJsonFinding[];
22
- /** URLs referenced in the page whose path ends in `.json`. */
23
- jsonUrls: string[];
24
- /** Distinct hosts the page loads a `<script src>` from. */
25
- scriptHosts: string[];
26
- /** Links whose path contains `api`. */
27
- apiLinks: string[];
28
- }
29
- /**
30
- * Scans a page's HTML for data-shaped things.
31
- *
32
- * @param html - The document.
33
- * @returns What was found, each list capped at a handful of entries.
34
- */
35
- export declare function findData(html: string): ProbeFindings;
36
- //# sourceMappingURL=find-data.algorithm.d.ts.map
@@ -1,28 +0,0 @@
1
- /** What a probe shows of an HTML document's tables, and of rendered Markdown's sections and front matter. */
2
- export interface HtmlFindings {
3
- /** The likely header rows of the `<table>`s, with the `selector` a `table` extract needs. */
4
- tables: {
5
- table: string;
6
- text: string;
7
- selector: string;
8
- hint?: string;
9
- }[];
10
- /** Rendered Markdown: its headings, each with the selector of its section. */
11
- outline?: {
12
- level: number;
13
- heading: string;
14
- selector: string;
15
- }[];
16
- /** Rendered Markdown: the keys of its front matter. */
17
- frontMatter?: string[];
18
- }
19
- /**
20
- * Summarises an HTML document for a `table` extract, and, when it is rendered
21
- * Markdown or a Word document, its outline (and Markdown's front matter).
22
- *
23
- * @param html - The document.
24
- * @param markdown - Whether it was rendered from Markdown or Word, and so has sections.
25
- * @returns The findings.
26
- */
27
- export declare function describeHtml(html: string, markdown: boolean): HtmlFindings;
28
- //# sourceMappingURL=html-findings.mapper.d.ts.map
@@ -1,31 +0,0 @@
1
- /** What a probe shows of JSON data (a JSON, JSON Lines or YAML body). */
2
- export interface JsonFindings {
3
- /** What the body was read as. */
4
- format: string;
5
- /** `object`, `array (n)`, or a scalar type. */
6
- type: string;
7
- /**
8
- * The document's structure down to four levels, one line per node:
9
- * `$.data.products array (1240) of object`, `$.data.products[*].name string "Pandina"`.
10
- */
11
- tree: string[];
12
- /**
13
- * Every array of objects with two entries or more, largest first: the
14
- * `forEach` or `each` a recipe needs, with the keys all its entries share.
15
- */
16
- lists: {
17
- path: string;
18
- length: number;
19
- keys: string[];
20
- }[];
21
- }
22
- /**
23
- * Summarises JSON data for someone writing a recipe: its shape, and where its
24
- * record lists are, so a `jsonpath` selector can be read off the result.
25
- *
26
- * @param data - The parsed document.
27
- * @param format - What it was read as (`json`, `jsonl`, `yaml`).
28
- * @returns The findings.
29
- */
30
- export declare function describeJson(data: unknown, format: string): JsonFindings;
31
- //# sourceMappingURL=json-findings.mapper.d.ts.map
@@ -1,26 +0,0 @@
1
- import type { PdfDocument } from '@opencraw/core';
2
- /** What a probe shows of a PDF: its rows, and the rows that look like table headers. */
3
- export interface PdfFindings {
4
- pages: number;
5
- /** The first rows, cells separated by " | ". */
6
- rows: {
7
- page: number;
8
- text: string;
9
- }[];
10
- /** Rows of text cells (no digit) with a row of data (a digit) right under them: likely table headers, with a selector that finds them. */
11
- headers: {
12
- page: number;
13
- text: string;
14
- selector: string;
15
- }[];
16
- }
17
- /**
18
- * Summarises a PDF for someone writing a recipe: what the rows look like, and
19
- * where the tables start, so a `table` extract's selector and columns can be
20
- * read off the result instead of guessed.
21
- *
22
- * @param document - The read PDF.
23
- * @returns The findings.
24
- */
25
- export declare function describePdf(document: PdfDocument): PdfFindings;
26
- //# sourceMappingURL=pdf-findings.mapper.d.ts.map
@@ -1,27 +0,0 @@
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
@@ -1,44 +0,0 @@
1
- import type { WorkbookDocument } from '@opencraw/core';
2
- /** What a probe shows of a workbook (a spreadsheet or a CSV). */
3
- export interface WorkbookFindings {
4
- /** How a CSV was read. */
5
- csv?: {
6
- encoding: string;
7
- delimiter: string;
8
- };
9
- sheets: {
10
- name: string;
11
- hidden: boolean;
12
- rows: number;
13
- columns: number;
14
- }[];
15
- /** The first non-empty rows of each visible sheet, cells separated by " | ". */
16
- rows: {
17
- sheet: string;
18
- row: number;
19
- text: string;
20
- }[];
21
- /**
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.
26
- */
27
- headers: {
28
- sheet: string;
29
- row: number;
30
- text: string;
31
- selector: string;
32
- hint?: string;
33
- }[];
34
- }
35
- /**
36
- * Summarises a workbook for someone writing a recipe: its sheets, what their
37
- * rows look like and where the tables start, so a `table` extract's `sheet`,
38
- * `selector` and columns can be read off the result instead of guessed.
39
- *
40
- * @param document - The read workbook.
41
- * @returns The findings.
42
- */
43
- export declare function describeWorkbook(document: WorkbookDocument): WorkbookFindings;
44
- //# sourceMappingURL=workbook-findings.mapper.d.ts.map
@@ -1,30 +0,0 @@
1
- /** What a probe shows of an XML document. */
2
- export interface XmlFindings {
3
- /** The root element's local name. */
4
- root: string;
5
- /** Declared on the root: prefix (`''` for the default namespace) to URI. */
6
- namespaces: Record<string, string>;
7
- /** Elements that repeat under one parent, largest first: the records, with the child names they share. */
8
- lists: {
9
- path: string;
10
- count: number;
11
- children: string[];
12
- }[];
13
- /** One line per distinct element path (local names), with its count, attributes and a sample text. */
14
- tree: string[];
15
- /** For a sitemap (`urlset`, `sitemapindex`): how many locations it lists. */
16
- sitemap?: {
17
- kind: 'urlset' | 'sitemapindex';
18
- locations: number;
19
- };
20
- }
21
- /**
22
- * Summarises an XML document for someone writing a recipe: its namespaces
23
- * (and the `namespaces` a query needs), the elements that repeat (the
24
- * records a `forEach` walks), and its structure.
25
- *
26
- * @param document - A parsed document (`parseXml` from the core), read through the W3C DOM properties.
27
- * @returns The findings.
28
- */
29
- export declare function describeXml(document: unknown): XmlFindings;
30
- //# sourceMappingURL=xml-findings.mapper.d.ts.map