diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a2a31..02cfc90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. +## Unreleased + +### Changed + +* Datasheet visual redesign: hero header with version/DOI/license/size badges, stat cards, pure-SVG AI-readiness donut, carded sections, improved print/PDF styling. Templates are themed via CSS custom properties in `base.html`. +* Evidence graph HTML rebuilt from a 1,000-line Python f-string into a Jinja template (`templates/evidence_graph/`) plus a real JavaScript asset. React/ReactDOM/dagre are now vendored and inlined, so the generated file is fully self-contained and works offline (previously required CDN access). Added a node-type legend, hover tooltips, and a reset-view button. +* Embedded RO-Crate JSON in evidence graph HTML is now `<`-escaped, preventing `` breakout from metadata values. +* RO-Crate root entities are resolved via the `ro-crate-metadata.json` descriptor's `about` reference instead of assuming `@graph[1]`. +* Metadata files are rewritten atomically (temp file + rename), so an interrupted run can no longer corrupt `ro-crate-metadata.json`. +* Subcrate metadata is loaded and validated once per datasheet build instead of three times. +* Datasheet generation errors now go through `logging` instead of bare prints. + +### Fixed + +* `build datasheet --template-dir` was accepted but silently ignored; it is now honored. + ## 0.2.0 (2024-03-28) [GitHub release](https://github.com/fairscape/fairscape-cli/releases/tag/0.2.0) diff --git a/pyproject.toml b/pyproject.toml index 12f6e8b..7e36db2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,9 @@ dependencies = [ "mongomock", "huggingface_hub>=0.20.0", "pyarrow>=17.0.0", + "fairscape_graph_tools", + "pydantic-ai>=0.1.0", + "httpx>=0.28.1", ] [tool.setuptools] @@ -71,6 +74,7 @@ members = [ [tool.uv.sources] fairscape-cli = { workspace = true, editable = true } +fairscape_graph_tools = { path = "../fairscape_graph_tools", editable = true } [dependency-groups] dev = [ diff --git a/src/fairscape_cli/__main__.py b/src/fairscape_cli/__main__.py index 142bd5c..36597be 100644 --- a/src/fairscape_cli/__main__.py +++ b/src/fairscape_cli/__main__.py @@ -1,5 +1,9 @@ +import logging + import click +logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s") + # Import command groups from their new locations from fairscape_cli.commands.rocrate_commands import rocrate_group from fairscape_cli.commands.import_commands import import_group @@ -8,6 +12,7 @@ from fairscape_cli.commands.schema_commands import schema from fairscape_cli.commands.augment_commands import augment_group from fairscape_cli.commands.track import track +from fairscape_cli.commands.interpret import interpret_group @click.group(invoke_without_command=True) @click.pass_context @@ -27,6 +32,7 @@ def cli(ctx): cli.add_command(schema, name='schema') cli.add_command(augment_group, name='augment') cli.add_command(track, name='track') +cli.add_command(interpret_group, name='interpret') if __name__ == "__main__": cli() \ No newline at end of file diff --git a/src/fairscape_cli/commands/augment_commands.py b/src/fairscape_cli/commands/augment_commands.py index 2d3bb8f..a0e819e 100644 --- a/src/fairscape_cli/commands/augment_commands.py +++ b/src/fairscape_cli/commands/augment_commands.py @@ -12,6 +12,14 @@ calculate_inputs_outputs, add_inputs_outputs_to_rocrate ) +from fairscape_cli.entailments.summary_stats import ( + build_summary_dataset, + compute_stats, + human_size, + is_dataset, + is_tabular_entity, + read_table, +) @click.group('augment') def augment_group(): @@ -202,4 +210,115 @@ def add_io_command( ctx.exit(1) except Exception as e: click.echo(f"An unexpected error occurred: {type(e).__name__} - {e}", err=True) - ctx.exit(1) \ No newline at end of file + ctx.exit(1) + + +@augment_group.command('summary-stats') +@click.argument('rocrate-path', type=click.Path(exists=True, path_type=pathlib.Path)) +@click.option('--id', 'entity_id', default=None, help="Compute stats only for this Dataset @id. Default: every tabular Dataset.") +@click.option('--overwrite/--skip-existing', default=False, help="Recompute stats for Datasets that already have hasSummaryStatistics. Default: skip.") +@click.option('--http-timeout', default=60, show_default=True, help="HTTP timeout in seconds for remote contentUrls.") +@click.option('--dry-run', is_flag=True, help="Print what would change without writing ro-crate-metadata.json.") +@click.pass_context +def summary_stats_command( + ctx, + rocrate_path: pathlib.Path, + entity_id: str, + overwrite: bool, + http_timeout: int, + dry_run: bool, +): + """ + Compute row/column counts plus per-column statistics for tabular Datasets + in an RO-Crate. For each matched Dataset: + + * read its contentUrl (local path, file://, or http(s)://) as csv/tsv/parquet + * populate rowCount / columnCount / contentSize / sampleSize on the source + * append a child SummaryStats Dataset (per-column dtype, null counts, and + numeric min/max/mean/std) and link it via hasSummaryStatistics + + For a no-extra-deps row+column counter that works on csv/tsv only, use + `Dataset.add_summary_stats()` from fairscape_models directly. + """ + crate_root = rocrate_path if rocrate_path.is_dir() else rocrate_path.parent + metadata_path = crate_root / "ro-crate-metadata.json" + if not metadata_path.exists(): + click.echo(f"Error: RO-Crate metadata file not found at {metadata_path}", err=True) + ctx.exit(1) + + with metadata_path.open("r") as f: + metadata = json.load(f) + + graph = metadata.get("@graph") or [] + if not graph: + click.echo("Error: RO-Crate metadata has no @graph", err=True) + ctx.exit(1) + + root_dataset = graph[1] if len(graph) > 1 else None + new_entities: list = [] + updated_count = 0 + skipped: list = [] + + for entity in graph: + if not is_dataset(entity): + continue + if entity_id and entity.get("@id") != entity_id: + continue + if entity is root_dataset: + continue + if not is_tabular_entity(entity): + skipped.append((entity.get("@id"), "not tabular")) + continue + if entity.get("hasSummaryStatistics") and not overwrite: + skipped.append((entity.get("@id"), "already has hasSummaryStatistics (use --overwrite)")) + continue + content_url = entity.get("contentUrl") + if isinstance(content_url, list): + content_url = content_url[0] if content_url else None + if not content_url: + skipped.append((entity.get("@id"), "no contentUrl")) + continue + + try: + df, size_bytes, source_desc = read_table(content_url, crate_root, http_timeout=http_timeout) + except Exception as e: + click.echo(f" ! {entity.get('@id')}: failed to read ({type(e).__name__}: {e})", err=True) + skipped.append((entity.get("@id"), f"read failed: {e}")) + continue + + rows, cols = int(df.shape[0]), int(df.shape[1]) + size_str = human_size(size_bytes) + per_column = compute_stats(df) + + entity["rowCount"] = rows + entity["columnCount"] = cols + entity["contentSize"] = size_str + entity.setdefault("sampleSize", rows) + + stats_entity = build_summary_dataset(entity, rows, cols, size_str, per_column) + entity["hasSummaryStatistics"] = {"@id": stats_entity["@id"]} + new_entities.append(stats_entity) + updated_count += 1 + click.echo(f" ✓ {entity['@id']} ← {rows} rows × {cols} cols, {size_str} (source: {source_desc})") + + if not updated_count: + click.echo("No Datasets updated.") + for eid, reason in skipped: + click.echo(f" - {eid}: {reason}") + return + + if new_entities and root_dataset is not None: + root_dataset.setdefault("hasPart", []).extend([{"@id": e["@id"]} for e in new_entities]) + graph.extend(new_entities) + + if dry_run: + click.echo(f"\nDRY RUN — would update {updated_count} dataset(s) and add {len(new_entities)} SummaryStats entit{'y' if len(new_entities) == 1 else 'ies'}.") + return + + with metadata_path.open("w") as f: + json.dump(metadata, f, indent=2) + click.echo(f"\nUpdated {updated_count} dataset(s); appended {len(new_entities)} SummaryStats entit{'y' if len(new_entities) == 1 else 'ies'} to {metadata_path}.") + if skipped: + click.echo("Skipped:") + for eid, reason in skipped: + click.echo(f" - {eid}: {reason}") \ No newline at end of file diff --git a/src/fairscape_cli/commands/build_commands.py b/src/fairscape_cli/commands/build_commands.py index f0881c2..d221d0a 100644 --- a/src/fairscape_cli/commands/build_commands.py +++ b/src/fairscape_cli/commands/build_commands.py @@ -8,8 +8,10 @@ from datetime import datetime from fairscape_cli.datasheet_builder.rocrate.datasheet_generator import DatasheetGenerator -from fairscape_cli.datasheet_builder.evidence_graph.graph_builder import generate_evidence_graph_from_rocrate from fairscape_cli.datasheet_builder.evidence_graph.html_builder import generate_evidence_graph_html +from fairscape_cli.interpret.local_graph import LocalGraphSource +from fairscape_cli.interpret.local_sink import LocalResultSink +from fairscape_graph_tools.evidence_graph_builder import EvidenceGraphBuilder from fairscape_cli.utils.build_utils import ( process_all_subcrates, process_croissant, @@ -28,9 +30,12 @@ collect_subcrate_metadata, collect_subcrate_aggregated_metrics ) +from fairscape_cli.models.rocrate import _extract_content_size_bytes, format_content_size_bytes from fairscape_models.rocrate import ROCrateV1_2, ROCrateMetadataElem -from fairscape_cli.utils.serialization import prune_none +from fairscape_cli.datasheet_builder import get_default_template_dir +from fairscape_cli.utils.serialization import prune_none, write_json_atomic +from fairscape_cli.utils.rocrate_helpers import get_root_entity_dict from fairscape_models.conversion.converter import ROCToTargetConverter from fairscape_models.conversion.mapping.croissant import MAPPING_CONFIGURATION as CROISSANT_MAPPING @@ -236,7 +241,12 @@ def build_release( if citation: parent_params["citation"] = citation if funder: parent_params["funder"] = funder if usage_info: parent_params["usageInfo"] = usage_info - if content_size: parent_params["contentSize"] = content_size + # An explicitly provided release contentSize is authoritative; otherwise + # fill it from the hierarchy-aware sub-crate roll-up. + if content_size: + parent_params["contentSize"] = content_size + elif aggregated_metrics.total_content_size_bytes > 0: + parent_params["contentSize"] = format_content_size_bytes(aggregated_metrics.total_content_size_bytes) if ethical_review: parent_params["ethicalReview"] = ethical_review if has_summary_stats: parent_params["hasSummaryStats"] = has_summary_stats @@ -323,7 +333,8 @@ def build_release( parent_params["evi:computationCount"] = aggregated_metrics.computation_count parent_params["evi:softwareCount"] = aggregated_metrics.software_count parent_params["evi:schemaCount"] = aggregated_metrics.schema_count - parent_params["evi:totalContentSizeBytes"] = aggregated_metrics.total_content_size_bytes + declared_size_bytes = _extract_content_size_bytes(content_size) if content_size else 0 + parent_params["evi:totalContentSizeBytes"] = declared_size_bytes or aggregated_metrics.total_content_size_bytes parent_params["evi:entitiesWithSummaryStats"] = aggregated_metrics.entities_with_summary_stats parent_params["evi:entitiesWithChecksums"] = aggregated_metrics.entities_with_checksums parent_params["evi:totalEntities"] = aggregated_metrics.total_entities @@ -402,8 +413,7 @@ def build_datasheet(ctx, rocrate_path, output, template_dir, published, pdf, ski output_path = output if output else crate_dir / "ro-crate-datasheet.html" - package_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - template_dir = Path(os.path.join(package_dir, 'datasheet_builder', 'templates')) + template_dir = template_dir if template_dir else get_default_template_dir() # Link subcrates if needed click.echo("Checking subcrate links...") @@ -512,10 +522,23 @@ def generate_evidence_graph( # Generate the evidence graph try: click.echo(f"Generating evidence graph for {ark_id} from {metadata_file}...") - evidence_graph = generate_evidence_graph_from_rocrate( - rocrate_path=metadata_file, - output_path=output_file, - node_id=ark_id + + source = LocalGraphSource(primary_path=metadata_file) + resolved = source.find_entity(ark_id) + if resolved is None: + click.echo(f"ERROR: Could not find entity with ID {ark_id} in RO-Crate") + ctx.exit(1) + return + resolved_id = resolved.get("@id", ark_id) + resolved_name = resolved.get("name") or "Unknown" + + sink = LocalResultSink(output_path=output_file) + builder = EvidenceGraphBuilder(source, sink) + builder.build( + resolved_id, + owner_email=resolved_id, + name=f"Evidence Graph - {resolved_name}", + description=f"Evidence graph for {resolved_name}", ) click.echo(f"Evidence graph saved to {output_file}") @@ -532,25 +555,20 @@ def generate_evidence_graph( click.echo("WARNING: generate_evidence_graph_html module not found, skipping visualization") click.echo("To generate visualizations, please install the visualization module.") except Exception as e: - click.echo(f"ERROR generating visualization: {str(e)}")\ - + click.echo(f"ERROR generating visualization: {str(e)}") + try: with open(metadata_file, 'r') as f: metadata = json.load(f) - - i = 0 - for entity in metadata.get('@graph', []): - if i == 1: - entity['localEvidenceGraph'] = { - "@id": str(html_output_path) - } - break - i += 1 - - # Write the updated metadata back to the file - with open(metadata_file, 'w') as f: - json.dump(prune_none(metadata), f, indent=2) - + + root_entity = get_root_entity_dict(metadata.get('@graph', [])) + if root_entity is not None: + root_entity['localEvidenceGraph'] = { + "@id": str(html_output_path) + } + + write_json_atomic(metadata_file, prune_none(metadata)) + click.echo(f"Added hasEvidenceGraph reference to {ark_id} in RO-Crate metadata") except Exception as e: click.echo(f"WARNING: Failed to add hasEvidenceGraph reference: {str(e)}") diff --git a/src/fairscape_cli/commands/import_commands.py b/src/fairscape_cli/commands/import_commands.py index 587d635..a07a8e2 100644 --- a/src/fairscape_cli/commands/import_commands.py +++ b/src/fairscape_cli/commands/import_commands.py @@ -322,6 +322,75 @@ def import_figshare( ctx.exit(1) +@import_group.command('manifest') +@click.argument('manifest-path', type=click.Path(exists=True, dir_okay=False, path_type=pathlib.Path)) +@click.option('--sidecar', required=False, type=click.Path(exists=True, dir_okay=False, path_type=pathlib.Path), + help='Path to the crate.json sidecar. If omitted, looks for .json then ./crate.json.') +@generic_importer_options +@click.pass_context +def import_manifest( + ctx: click.Context, + manifest_path: pathlib.Path, + sidecar: Optional[pathlib.Path], + output_dir: pathlib.Path, + name: Optional[str], + description: Optional[str], + author: Optional[Tuple[str]], + keywords: Optional[Tuple[str]], + license: Optional[str], + publication_date: Optional[str], + doi: Optional[str], + crate_version: str, + organization_name: Optional[str], + project_name: Optional[str], + include_files: bool, +): + """Build an RO-Crate from a generic CSV manifest + JSON sidecar. + + MANIFEST_PATH: Path to a CSV with columns (name, description, contentUrl, [format, md5, sha256, size_bytes, contentSize, datePublished, version, keywords, group]). + Crate-level metadata (title, authors, license, doi, ...) comes from a sibling sidecar JSON. + See wizards/manifest-import-design/DESIGN.md for the full spec. + """ + click.echo(f"Importing manifest {manifest_path}...") + + try: + research_data_instance = ResearchData.from_repository( + repository_type='manifest', + identifier=str(manifest_path), + sidecar_path=sidecar, + include_files=include_files, + ) + click.echo(f"Loaded manifest. Title: {research_data_instance.title}") + click.echo(f" Files in manifest: {len(research_data_instance.files)}") + + if name: research_data_instance.title = name + if description: research_data_instance.description = description + if author: research_data_instance.authors = list(author) + if keywords: research_data_instance.keywords = list(keywords) + if license: research_data_instance.license = license + if publication_date: research_data_instance.publication_date = publication_date + if doi: research_data_instance.doi = doi + + rocrate_guid = research_data_instance.to_rocrate( + output_dir=str(output_dir), + crate_version=crate_version, + organization_name=organization_name, + project_name=project_name, + ) + + click.echo(f"Successfully created RO-Crate from manifest {manifest_path.name}.") + click.echo(f"RO-Crate Root GUID: {rocrate_guid}") + click.echo(f"RO-Crate saved to: {output_dir.resolve()}") + + except ValueError as e: + click.echo(f"ERROR: {e}", err=True) + ctx.exit(1) + except Exception as e: + click.echo(f"An unexpected error occurred: {e}", err=True) + traceback.print_exc() + ctx.exit(1) + + @import_group.command('dataverse') @click.argument('dataset-doi', type=str) @click.option('--server-url', default='https://dataverse.harvard.edu', show_default=True, help='Dataverse server URL.') diff --git a/src/fairscape_cli/commands/interpret.py b/src/fairscape_cli/commands/interpret.py new file mode 100644 index 0000000..3b38ab4 --- /dev/null +++ b/src/fairscape_cli/commands/interpret.py @@ -0,0 +1,155 @@ +"""`fairscape interpret` Click subcommand. + +Wires the four local adapters (LocalGraphSource, LocalResultSink, +InMemoryTaskTracker, LocalSoftwareFetcher) into the shared +`Condenser` + `Interpreter` orchestrators and runs the pipeline +against an on-disk RO-Crate, writing the AnnotatedEvidenceGraph as a +sidecar JSON. +""" + +from __future__ import annotations + +import logging +import pathlib +import re +import sys + +import click + +from fairscape_graph_tools.condenser import Condenser +from fairscape_graph_tools.interpreter import InterpretConfig, Interpreter + +from fairscape_cli.interpret.local_graph import LocalGraphSource +from fairscape_cli.interpret.local_sink import LocalResultSink +from fairscape_cli.interpret.local_software import LocalSoftwareFetcher +from fairscape_cli.interpret.local_tracker import InMemoryTaskTracker + + +@click.group("interpret") +def interpret_group(): + """AI-driven interpretation of a local RO-Crate.""" + pass + + +@interpret_group.command("run") +@click.argument( + "rocrate_path", + type=click.Path(exists=True, file_okay=True, dir_okay=True, path_type=pathlib.Path), +) +@click.option( + "--reference", + "references", + multiple=True, + type=click.Path(exists=True, file_okay=True, dir_okay=True, path_type=pathlib.Path), + help="Additional RO-Crate path whose @graph should be indexed for cross-crate ARK lookups. Repeatable.", +) +@click.option( + "--llm-model", + default="anthropic:claude-haiku-4-5-20251001", + show_default=True, + help="pydantic-ai model identifier used for computation annotation and graph synthesis.", +) +@click.option( + "--temperature", + type=float, + default=0.0, + show_default=True, +) +@click.option( + "--max-workers", + type=int, + default=2, + show_default=True, + help="Maximum parallel LLM annotation workers.", +) +@click.option( + "--output", + "output_path", + type=click.Path(dir_okay=False, path_type=pathlib.Path), + default=None, + help="Where to write the AnnotatedEvidenceGraph sidecar. Default: ./-interpretation.json", +) +@click.option( + "--save-condensed", + "condensed_output_path", + type=click.Path(dir_okay=False, path_type=pathlib.Path), + default=None, + help="Also write the condensed RO-Crate produced by the Condenser pre-step to this path.", +) +@click.option( + "--debug-llm", + is_flag=True, + default=False, + help="Append every raw LLM response to .llm-trace.jsonl.", +) +@click.option("--quiet", is_flag=True, default=False, help="Suppress progress output.") +def interpret_command( + rocrate_path: pathlib.Path, + references: tuple[pathlib.Path, ...], + llm_model: str, + temperature: float, + max_workers: int, + output_path: pathlib.Path | None, + condensed_output_path: pathlib.Path | None, + debug_llm: bool, + quiet: bool, +): + """Run AI interpretation over an RO-Crate on disk.""" + if not quiet: + logging.basicConfig(level=logging.INFO, format="%(message)s") + + try: + graph = LocalGraphSource(rocrate_path, references) + except ValueError as e: + raise click.ClickException(str(e)) + rocrate_id = graph.primary_root_id + rocrate_name = graph.primary_root_name + + if output_path is None: + slug = _slugify(rocrate_name or rocrate_id.rsplit("/", 1)[-1] or "rocrate") + output_path = pathlib.Path.cwd() / f"{slug}-interpretation.json" + + debug_llm_path: pathlib.Path | None = None + if debug_llm: + debug_llm_path = output_path.with_suffix(output_path.suffix + ".llm-trace.jsonl") + + if not quiet: + click.echo(f"Interpreting {rocrate_id}") + click.echo(f" primary crate: {rocrate_path}") + for ref in references: + click.echo(f" reference: {ref}") + click.echo(f" output: {output_path}") + if condensed_output_path is not None: + click.echo(f" condensed out: {condensed_output_path}") + if debug_llm_path is not None: + click.echo(f" llm trace: {debug_llm_path}") + + sink = LocalResultSink(output_path, condensed_output_path) + tracker = InMemoryTaskTracker(quiet=quiet, debug_llm_path=debug_llm_path) + software = LocalSoftwareFetcher(graph) + condenser = Condenser(graph, sink) + config = InterpretConfig( + llm_model=llm_model, + temperature=temperature, + max_workers=max_workers, + ) + interpreter = Interpreter(graph, sink, tracker, software, condenser, config) + + try: + aeg_id = interpreter.run_sync(rocrate_id) + except Exception as e: + click.echo(f"Interpretation failed: {e}", err=True) + sys.exit(1) + + if not quiet: + click.echo(f"\nAnnotatedEvidenceGraph: {aeg_id}") + click.echo(f"Wrote sidecar: {output_path}") + + +def _slugify(value: str) -> str: + """Cheap filesystem-safe slug. Keeps alnum + `-` + `_`, lowercases, + collapses whitespace. Good enough for default output filenames.""" + value = value.strip().lower() + value = re.sub(r"\s+", "-", value) + value = re.sub(r"[^a-z0-9\-_]", "", value) + return value or "rocrate" diff --git a/src/fairscape_cli/commands/rocrate_commands.py b/src/fairscape_cli/commands/rocrate_commands.py index dc5c8c6..9babc7c 100644 --- a/src/fairscape_cli/commands/rocrate_commands.py +++ b/src/fairscape_cli/commands/rocrate_commands.py @@ -30,207 +30,70 @@ def rocrate_group(): """Core operations for local RO-Crate manipulation.""" pass -@rocrate_group.command('init') -@click.option('--guid', required=False, type=str, default="", show_default=False) -@click.option('--name', required=True, type=str) -@click.option('--organization-name', required=True, type=str) -@click.option('--project-name', required=True, type=str) -@click.option('--description', required=True, type=str) -@click.option('--keywords', required=True, multiple=True, type=str) -@click.option('--license', required=False, type=str, default="https://creativecommons.org/licenses/by/4.0/") -@click.option('--date-published', required=False, type=str) -@click.option('--author', required=False, type=str, default="Unknown") -@click.option('--version', required=False, type=str, default="1.0") -@click.option('--associated-publication', required=False, type=str) -@click.option('--conditions-of-access', required=False, type=str) -@click.option('--copyright-notice', required=False, type=str) -@click.option('--completeness', required=False, type=str, help="Completeness of the dataset.") -@click.option('--ethical-review', required=False, type=str, help="Ethical review information.") -@click.option('--human-subject', required=False, type=str, help="Human subject involvement information.") -@click.option('--confidentiality-level', required=False, type=str, help="Confidentiality level.") -@click.option('--irb', required=False, type=str, help="IRB approval information.") -@click.option('--irb-protocol-id', required=False, type=str, help="IRB protocol identifier.") -@click.option('--human-subject-research', required=False, type=str, help="Whether this involves human subject research.") -@click.option('--human-subject-exemptions', required=False, type=str, help="Human subjects exemption category/description.") -@click.option('--deidentified', required=False, type=str, help="Whether samples are de-identified.") -@click.option('--fda-regulated', required=False, type=str, help="Whether the data is FDA regulated.") -@click.option('--data-governance', required=False, type=str, help="Data governance committee information.") -@click.option('--maintenance-plan', required=False, type=str, help="RAI: Versioning, maintainers, and deprecation policies.") -@click.option('--intended-use', required=False, type=str, help="RAI: Recommended dataset uses (e.g., training, validation).") -@click.option('--limitations', required=False, type=str, help="RAI: Known limitations and non-recommended uses.") -@click.option('--potential-sources-of-bias', required=False, type=str, help="RAI: Description of known biases in the dataset.") -@click.option('--prohibited-uses', required=False, type=str, help="Prohibited uses of the dataset.") -@click.option('--rai-data-collection', required=False, type=str, help="RAI: Description of the data collection process.") -@click.option('--rai-data-collection-type', required=False, multiple=True, type=str, help="RAI: Type of data collection (e.g., 'Web Scraping', 'Surveys').") -@click.option('--rai-missing-data-desc', required=False, type=str, help="RAI: Description of missing data in the dataset.") -@click.option('--rai-raw-data-source', required=False, type=str, help="RAI: Description of the raw data source.") -@click.option('--rai-collection-start-date', required=False, type=str, help="RAI: Start date of the data collection process (ISO format).") -@click.option('--rai-collection-end-date', required=False, type=str, help="RAI: End date of the data collection process (ISO format).") -@click.option('--rai-imputation-protocol', required=False, type=str, help="RAI: Description of the data imputation process.") -@click.option('--rai-manipulation-protocol', required=False, type=str, help="RAI: Description of the data manipulation process.") -@click.option('--rai-preprocessing-protocol', required=False, multiple=True, type=str, help="RAI: Steps taken to preprocess the data for ML use.") -@click.option('--rai-annotation-protocol', required=False, type=str, help="RAI: Description of the annotation process (e.g., workforce, tasks).") -@click.option('--rai-annotation-platform', required=False, multiple=True, type=str, help="RAI: Platform or tool used for human annotation.") -@click.option('--rai-annotation-analysis', required=False, multiple=True, type=str, help="RAI: Analysis of annotations (e.g., disagreement resolution).") -@click.option('--rai-sensitive-info', required=False, multiple=True, type=str, help="RAI: Description of any personal or sensitive information.") -@click.option('--rai-social-impact', required=False, type=str, help="RAI: Discussion of the dataset's potential social impact.") -@click.option('--rai-annotations-per-item', required=False, type=str, help="RAI: Number of human labels per dataset item.") -@click.option('--rai-annotator-demographics', required=False, multiple=True, type=str, help="RAI: Demographic specifications about the annotators.") -@click.option('--rai-machine-annotation-tools', required=False, multiple=True, type=str, help="RAI: Software used for automated data annotation.") -@click.option('--custom-properties', required=False, type=str, help='JSON string with additional properties to include') -def init( - guid, name, organization_name, project_name, description, keywords, license, - date_published, author, version, associated_publication, conditions_of_access, - copyright_notice, completeness, ethical_review, human_subject, confidentiality_level, - irb, irb_protocol_id, human_subject_research, human_subject_exemptions, - deidentified, fda_regulated, data_governance, - maintenance_plan, intended_use, limitations, potential_sources_of_bias, - prohibited_uses, rai_data_collection, rai_data_collection_type, rai_missing_data_desc, - rai_raw_data_source, rai_collection_start_date, rai_collection_end_date, - rai_imputation_protocol, rai_manipulation_protocol, rai_preprocessing_protocol, - rai_annotation_protocol, rai_annotation_platform, rai_annotation_analysis, - rai_sensitive_info, rai_social_impact, rai_annotations_per_item, - rai_annotator_demographics, rai_machine_annotation_tools, custom_properties -): - """Initialize an RO-Crate in the current working directory.""" - params = { - "guid": guid, "name": name, "organizationName": organization_name, - "projectName": project_name, "description": description, "keywords": list(keywords), - "license": license, "datePublished": date_published, "author": author, - "version": version, "associatedPublication": associated_publication, - "conditionsOfAccess": conditions_of_access, "copyrightNotice": copyright_notice, - "path": pathlib.Path.cwd() - } - - rai_properties = {} - if limitations: - rai_properties["rai:dataLimitations"] = limitations - if potential_sources_of_bias: - rai_properties["rai:dataBiases"] = potential_sources_of_bias - if intended_use: - rai_properties["rai:dataUseCases"] = intended_use - if maintenance_plan: - rai_properties["rai:dataReleaseMaintenancePlan"] = maintenance_plan - if rai_data_collection: - rai_properties["rai:dataCollection"] = rai_data_collection - if rai_data_collection_type: - rai_properties["rai:dataCollectionType"] = list(rai_data_collection_type) - if rai_missing_data_desc: - rai_properties["rai:dataCollectionMissingData"] = rai_missing_data_desc - if rai_raw_data_source: - rai_properties["rai:dataCollectionRawData"] = rai_raw_data_source - if rai_imputation_protocol: - rai_properties["rai:dataImputationProtocol"] = rai_imputation_protocol - if rai_manipulation_protocol: - rai_properties["rai:dataManipulationProtocol"] = rai_manipulation_protocol - if rai_preprocessing_protocol: - rai_properties["rai:dataPreprocessingProtocol"] = list(rai_preprocessing_protocol) - if rai_annotation_protocol: - rai_properties["rai:dataAnnotationProtocol"] = rai_annotation_protocol - if rai_annotation_platform: - rai_properties["rai:dataAnnotationPlatform"] = list(rai_annotation_platform) - if rai_annotation_analysis: - rai_properties["rai:dataAnnotationAnalysis"] = list(rai_annotation_analysis) - if rai_sensitive_info: - rai_properties["rai:personalSensitiveInformation"] = list(rai_sensitive_info) - if rai_social_impact: - rai_properties["rai:dataSocialImpact"] = rai_social_impact - if rai_annotations_per_item: - rai_properties["rai:annotationsPerItem"] = rai_annotations_per_item - if rai_annotator_demographics: - rai_properties["rai:annotatorDemographics"] = list(rai_annotator_demographics) - if rai_machine_annotation_tools: - rai_properties["rai:machineAnnotationTools"] = list(rai_machine_annotation_tools) - - timeframe = [] - if rai_collection_start_date: - timeframe.append(rai_collection_start_date) - if rai_collection_end_date: - timeframe.append(rai_collection_end_date) - if timeframe: - rai_properties["rai:dataCollectionTimeframe"] = timeframe - - params.update(rai_properties) - - # Compliance fields as direct top-level properties - if completeness: params["completeness"] = completeness - if ethical_review: params["ethicalReview"] = ethical_review - if human_subject: params["humanSubjectResearch"] = human_subject - if confidentiality_level: params["confidentialityLevel"] = confidentiality_level - if prohibited_uses: params["prohibitedUses"] = prohibited_uses - if irb: params["irb"] = irb - if irb_protocol_id: params["irbProtocolId"] = irb_protocol_id - if human_subject_research: params["humanSubjectResearch"] = human_subject_research - if human_subject_exemptions: params["humanSubjectExemption"] = human_subject_exemptions - if deidentified: params["deidentified"] = deidentified - if fda_regulated: params["fdaRegulated"] = fda_regulated - if data_governance: params["dataGovernanceCommittee"] = data_governance - - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - return - filtered_params = {k: v for k, v in params.items() if v is not None} - passed_crate = GenerateROCrate(**filtered_params) - click.echo(passed_crate.get("@id")) - - -@rocrate_group.command('create') -@click.argument('rocrate-path', type=click.Path(exists=False, path_type=pathlib.Path)) -@click.option('--guid', required=False, type=str, default="", show_default=False) -@click.option('--name', required=True, type=str) -@click.option('--organization-name', required=True, type=str) -@click.option('--project-name', required=True, type=str) -@click.option('--description', required=True, type=str) -@click.option('--keywords', required=True, multiple=True, type=str) -@click.option('--license', required=False, type=str, default="https://creativecommons.org/licenses/by/4.0/") -@click.option('--date-published', required=False, type=str) -@click.option('--author', required=False, type=str, default="Unknown") -@click.option('--version', required=False, type=str, default="1.0") -@click.option('--associated-publication', required=False, type=str) -@click.option('--conditions-of-access', required=False, type=str) -@click.option('--copyright-notice', required=False, type=str) -@click.option('--completeness', required=False, type=str, help="Completeness of the dataset.") -@click.option('--ethical-review', required=False, type=str, help="Ethical review information.") -@click.option('--human-subject', required=False, type=str, help="Human subject involvement information.") -@click.option('--confidentiality-level', required=False, type=str, help="Confidentiality level.") -@click.option('--irb', required=False, type=str, help="IRB approval information.") -@click.option('--irb-protocol-id', required=False, type=str, help="IRB protocol identifier.") -@click.option('--human-subject-research', required=False, type=str, help="Whether this involves human subject research.") -@click.option('--human-subject-exemptions', required=False, type=str, help="Human subjects exemption category/description.") -@click.option('--deidentified', required=False, type=str, help="Whether samples are de-identified.") -@click.option('--fda-regulated', required=False, type=str, help="Whether the data is FDA regulated.") -@click.option('--data-governance', required=False, type=str, help="Data governance committee information.") -@click.option('--maintenance-plan', required=False, type=str, help="RAI: Versioning, maintainers, and deprecation policies.") -@click.option('--intended-use', required=False, type=str, help="RAI: Recommended dataset uses (e.g., training, validation).") -@click.option('--limitations', required=False, type=str, help="RAI: Known limitations and non-recommended uses.") -@click.option('--potential-sources-of-bias', required=False, type=str, help="RAI: Description of known biases in the dataset.") -@click.option('--prohibited-uses', required=False, type=str, help="Prohibited uses of the dataset.") -@click.option('--rai-data-collection', required=False, type=str, help="RAI: Description of the data collection process.") -@click.option('--rai-data-collection-type', required=False, multiple=True, type=str, help="RAI: Type of data collection (e.g., 'Web Scraping', 'Surveys').") -@click.option('--rai-missing-data-desc', required=False, type=str, help="RAI: Description of missing data in the dataset.") -@click.option('--rai-raw-data-source', required=False, type=str, help="RAI: Description of the raw data source.") -@click.option('--rai-collection-start-date', required=False, type=str, help="RAI: Start date of the data collection process (ISO format).") -@click.option('--rai-collection-end-date', required=False, type=str, help="RAI: End date of the data collection process (ISO format).") -@click.option('--rai-imputation-protocol', required=False, type=str, help="RAI: Description of the data imputation process.") -@click.option('--rai-manipulation-protocol', required=False, type=str, help="RAI: Description of the data manipulation process.") -@click.option('--rai-preprocessing-protocol', required=False, multiple=True, type=str, help="RAI: Steps taken to preprocess the data for ML use.") -@click.option('--rai-annotation-protocol', required=False, type=str, help="RAI: Description of the annotation process (e.g., workforce, tasks).") -@click.option('--rai-annotation-platform', required=False, multiple=True, type=str, help="RAI: Platform or tool used for human annotation.") -@click.option('--rai-annotation-analysis', required=False, multiple=True, type=str, help="RAI: Analysis of annotations (e.g., disagreement resolution).") -@click.option('--rai-sensitive-info', required=False, multiple=True, type=str, help="RAI: Description of any personal or sensitive information.") -@click.option('--rai-social-impact', required=False, type=str, help="RAI: Discussion of the dataset's potential social impact.") -@click.option('--rai-annotations-per-item', required=False, type=str, help="RAI: Number of human labels per dataset item.") -@click.option('--rai-annotator-demographics', required=False, multiple=True, type=str, help="RAI: Demographic specifications about the annotators.") -@click.option('--rai-machine-annotation-tools', required=False, multiple=True, type=str, help="RAI: Software used for automated data annotation.") -@click.option('--custom-properties', required=False, type=str, help='JSON string with additional properties to include') -def create( - rocrate_path, guid, name, organization_name, project_name, description, keywords, +def add_options(options): + """Apply a list of click options written in top-to-bottom --help order.""" + def wrapper(func): + for option in reversed(options): + func = option(func) + return func + return wrapper + + +# Crate-level metadata options shared by `rocrate init` and `rocrate create`. +ROCRATE_METADATA_OPTIONS = [ + click.option('--guid', required=False, type=str, default="", show_default=False), + click.option('--name', required=True, type=str), + click.option('--organization-name', required=True, type=str), + click.option('--project-name', required=True, type=str), + click.option('--description', required=True, type=str), + click.option('--keywords', required=True, multiple=True, type=str), + click.option('--license', required=False, type=str, default="https://creativecommons.org/licenses/by/4.0/"), + click.option('--date-published', required=False, type=str), + click.option('--author', required=False, type=str, default="Unknown"), + click.option('--version', required=False, type=str, default="1.0"), + click.option('--associated-publication', required=False, type=str), + click.option('--conditions-of-access', required=False, type=str), + click.option('--copyright-notice', required=False, type=str), + click.option('--completeness', required=False, type=str, help="Completeness of the dataset."), + click.option('--ethical-review', required=False, type=str, help="Ethical review information."), + click.option('--human-subject', required=False, type=str, help="Human subject involvement information."), + click.option('--confidentiality-level', required=False, type=str, help="Confidentiality level."), + click.option('--irb', required=False, type=str, help="IRB approval information."), + click.option('--irb-protocol-id', required=False, type=str, help="IRB protocol identifier."), + click.option('--human-subject-research', required=False, type=str, help="Whether this involves human subject research."), + click.option('--human-subject-exemptions', required=False, type=str, help="Human subjects exemption category/description."), + click.option('--deidentified', required=False, type=str, help="Whether samples are de-identified."), + click.option('--fda-regulated', required=False, type=str, help="Whether the data is FDA regulated."), + click.option('--data-governance', required=False, type=str, help="Data governance committee information."), + click.option('--maintenance-plan', required=False, type=str, help="RAI: Versioning, maintainers, and deprecation policies."), + click.option('--intended-use', required=False, type=str, help="RAI: Recommended dataset uses (e.g., training, validation)."), + click.option('--limitations', required=False, type=str, help="RAI: Known limitations and non-recommended uses."), + click.option('--potential-sources-of-bias', required=False, type=str, help="RAI: Description of known biases in the dataset."), + click.option('--prohibited-uses', required=False, type=str, help="Prohibited uses of the dataset."), + click.option('--rai-data-collection', required=False, type=str, help="RAI: Description of the data collection process."), + click.option('--rai-data-collection-type', required=False, multiple=True, type=str, help="RAI: Type of data collection (e.g., 'Web Scraping', 'Surveys')."), + click.option('--rai-missing-data-desc', required=False, type=str, help="RAI: Description of missing data in the dataset."), + click.option('--rai-raw-data-source', required=False, type=str, help="RAI: Description of the raw data source."), + click.option('--rai-collection-start-date', required=False, type=str, help="RAI: Start date of the data collection process (ISO format)."), + click.option('--rai-collection-end-date', required=False, type=str, help="RAI: End date of the data collection process (ISO format)."), + click.option('--rai-imputation-protocol', required=False, type=str, help="RAI: Description of the data imputation process."), + click.option('--rai-manipulation-protocol', required=False, type=str, help="RAI: Description of the data manipulation process."), + click.option('--rai-preprocessing-protocol', required=False, multiple=True, type=str, help="RAI: Steps taken to preprocess the data for ML use."), + click.option('--rai-annotation-protocol', required=False, type=str, help="RAI: Description of the annotation process (e.g., workforce, tasks)."), + click.option('--rai-annotation-platform', required=False, multiple=True, type=str, help="RAI: Platform or tool used for human annotation."), + click.option('--rai-annotation-analysis', required=False, multiple=True, type=str, help="RAI: Analysis of annotations (e.g., disagreement resolution)."), + click.option('--rai-sensitive-info', required=False, multiple=True, type=str, help="RAI: Description of any personal or sensitive information."), + click.option('--rai-social-impact', required=False, type=str, help="RAI: Discussion of the dataset's potential social impact."), + click.option('--rai-annotations-per-item', required=False, type=str, help="RAI: Number of human labels per dataset item."), + click.option('--rai-annotator-demographics', required=False, multiple=True, type=str, help="RAI: Demographic specifications about the annotators."), + click.option('--rai-machine-annotation-tools', required=False, multiple=True, type=str, help="RAI: Software used for automated data annotation."), + click.option('--custom-properties', required=False, type=str, help='JSON string with additional properties to include'), +] + + +def _generate_crate_from_options( + path, guid, name, organization_name, project_name, description, keywords, license, date_published, author, version, associated_publication, conditions_of_access, copyright_notice, completeness, ethical_review, human_subject, confidentiality_level, irb, irb_protocol_id, human_subject_research, human_subject_exemptions, @@ -244,14 +107,13 @@ def create( rai_annotations_per_item, rai_annotator_demographics, rai_machine_annotation_tools, custom_properties ): - """Create an RO-Crate in the specified path.""" params = { "guid": guid, "name": name, "organizationName": organization_name, "projectName": project_name, "description": description, "keywords": list(keywords), "license": license, "datePublished": date_published, "author": author, "version": version, "associatedPublication": associated_publication, "conditionsOfAccess": conditions_of_access, "copyrightNotice": copyright_notice, - "path": rocrate_path + "path": path } rai_properties = {} @@ -293,7 +155,7 @@ def create( rai_properties["rai:annotatorDemographics"] = list(rai_annotator_demographics) if rai_machine_annotation_tools: rai_properties["rai:machineAnnotationTools"] = list(rai_machine_annotation_tools) - + timeframe = [] if rai_collection_start_date: timeframe.append(rai_collection_start_date) @@ -301,7 +163,7 @@ def create( timeframe.append(rai_collection_end_date) if timeframe: rai_properties["rai:dataCollectionTimeframe"] = timeframe - + params.update(rai_properties) # Compliance fields as direct top-level properties @@ -332,6 +194,181 @@ def create( click.echo(passed_crate.get("@id")) +def _read_crate_or_exit(ctx, rocrate_path, err=True): + try: + return ReadROCrateMetadata(rocrate_path) + except Exception as exc: + click.echo(f"ERROR Reading ROCrate: {exc}", err=err) + ctx.exit(code=1) + + +def _merge_custom_properties_or_exit(ctx, params, custom_properties): + if not custom_properties: + return + try: + custom_props = json.loads(custom_properties) + if not isinstance(custom_props, dict): raise ValueError("Custom properties must be a JSON object") + params.update(custom_props) + except Exception as e: + click.echo(f"ERROR processing custom properties: {e}", err=True) + ctx.exit(code=1) + + +def _resolve_file_location_or_exit(ctx, filepath, content_url, embargoed, entity_label): + """Pick the filepath value from the mutually-backstopping location options.""" + if not filepath and not content_url and not embargoed: + click.echo(f"ERROR: Either 'filepath', 'content-url', or 'embargoed' must be provided for {entity_label} registration.", err=True) + ctx.exit(code=1) + if not filepath and not content_url and embargoed: + return "Embargoed" + if not filepath and content_url: + return content_url + return filepath + + +def _require_relative_destination(ctx, destination, option_name): + if destination is not None and destination.is_absolute(): + click.echo(f"ERROR: {option_name} must be a relative path within the RO-Crate: {destination}", err=True) + ctx.exit(code=1) + + +def _copy_into_crate_or_exit(ctx, source_filepath, rocrate_path, destination_filepath): + try: + CopyToROCrate(source_filepath, pathlib.Path(rocrate_path) / destination_filepath) + except Exception as exc: + click.echo(f"ERROR copying file to RO-Crate: {exc}", err=True) + ctx.exit(code=1) + + +def _generate_append_echo_or_exit(ctx, rocrate_path, generate_fn, params, entity_label): + """Generate an entity from params (Nones dropped), append it to the crate, echo its guid.""" + filtered_params = {k: v for k, v in params.items() if v is not None} + try: + instance = generate_fn(**filtered_params) + AppendCrate(cratePath=rocrate_path, elements=[instance]) + click.echo(instance.guid) + except FileNotInCrateException as e: + click.echo(f"ERROR: {e}", err=True) + ctx.exit(code=1) + except ValidationError as e: + click.echo(f"ERROR: {entity_label} Validation Failure\n{e}", err=True) + ctx.exit(code=1) + except Exception as exc: + click.echo(f"ERROR: {exc}", err=True) + ctx.exit(code=1) + + +@rocrate_group.command('init') +@add_options(ROCRATE_METADATA_OPTIONS) +def init(**params): + """Initialize an RO-Crate in the current working directory.""" + _generate_crate_from_options(path=pathlib.Path.cwd(), **params) + + +@rocrate_group.command('create') +@click.argument('rocrate-path', type=click.Path(exists=False, path_type=pathlib.Path)) +@add_options(ROCRATE_METADATA_OPTIONS) +def create(rocrate_path, **params): + """Create an RO-Crate in the specified path.""" + _generate_crate_from_options(path=rocrate_path, **params) + + +def _resolve_metadata_path(rocrate_path: pathlib.Path) -> pathlib.Path: + """Accept either a crate directory or the metadata JSON file itself.""" + if rocrate_path.is_dir(): + return rocrate_path / "ro-crate-metadata.json" + return rocrate_path + + +def _deep_coverage_metrics(release_dir): + """Walk every sub-crate under release_dir and return v2 coverage metrics + keyed by Evidence attribute name. The crate file is NOT modified — these + are passed to the scorer and echoed into the AI-Ready score document, so a + release scores against its full sub-crate content without storing coverage + fields on the RO-Crate itself. + """ + from fairscape_cli.models.rocrate import collect_subcrate_aggregated_metrics + + m = collect_subcrate_aggregated_metrics(release_dir) + return { + "dataset_count": m.dataset_count, + "software_count": m.software_count, + "schema_count": m.schema_count, + "computation_count": m.computation_count, + "experiment_count": m.experiment_count, + "total_entities": m.total_entities, + "good_computations": m.good_computations, + "computation_with_software": m.computations_with_software, + "computation_with_io": m.computations_with_io, + "entities_with_provenance_link": m.entities_with_provenance_link, + "software_with_link": m.software_with_link, + "datasets_with_accession": m.datasets_with_accession, + "datasets_sourced": m.datasets_with_source, + "distribution_link_count": m.distribution_link_count, + "distinct_protocols": sorted(m.distribution_protocols), + "entities_with_hash": m.entities_with_checksums, + "hashable_entities": m.dataset_count + m.software_count, + "tabular_dataset_count": m.tabular_dataset_count, + "tabular_with_schema": m.tabular_with_schema, + "tabular_with_stats": m.tabular_with_stats, + } + + +@rocrate_group.command('score') +@click.argument('rocrate-path', type=click.Path(exists=True, path_type=pathlib.Path)) +@click.option('--grader-version', 'grader_version', type=click.Choice(['v1', 'v2']), default='v1', + show_default=True, help="Which AI-Ready grader to run. v2 is the harsher deterministic grader.") +@click.option('--deep', is_flag=True, default=False, + help="Release crates only: walk every sub-crate from disk to compute fresh coverage metrics before scoring (extra compute, does not modify the crate). Recommended for v2 on a release directory.") +@click.option('--json', 'json_out', type=click.Path(path_type=pathlib.Path), default=None, + help="Write the full score as JSON to this path instead of a summary table.") +def score(rocrate_path, grader_version, deep, json_out): + """Compute the deterministic AI-Ready score for an RO-Crate.""" + ctx = click.get_current_context() + metadata_path = _resolve_metadata_path(rocrate_path) + try: + with metadata_path.open("r") as f: + crate_dict = json.load(f) + except Exception as exc: + click.echo(f"ERROR reading RO-Crate metadata at {metadata_path}: {exc}", err=True) + ctx.exit(code=1) + + aggregate_metrics = None + if deep: + release_dir = rocrate_path if rocrate_path.is_dir() else metadata_path.parent + click.echo(f"Walking sub-crates under {release_dir} for deep coverage metrics ...", err=True) + try: + aggregate_metrics = _deep_coverage_metrics(release_dir) + except Exception as exc: + click.echo(f"WARNING: deep metric collection failed ({exc}); scoring inline graph only.", err=True) + + try: + if grader_version == 'v2': + from fairscape_models.conversion.mapping.AIReadyV2 import score_rocrate_v2 + result = score_rocrate_v2(crate_dict, aggregate_metrics=aggregate_metrics) + else: + from fairscape_models.conversion.mapping.AIReady import score_rocrate + result = score_rocrate(crate_dict) + except Exception as exc: + click.echo(f"ERROR scoring RO-Crate: {exc}", err=True) + ctx.exit(code=1) + + if json_out is not None: + json_out.write_text(result.model_dump_json(indent=2)) + click.echo(f"Wrote {grader_version} AI-Ready score to {json_out}") + return + + if grader_version == 'v2': + click.echo(f"AI-Ready Score (v2): {result.total_earned}/{result.total_possible} ({result.percentage}%)") + for c in result.criteria: + click.echo(f" {c.criterion:28} {c.earned:>2}/{c.possible:<2} ({c.percentage:.0f}%)") + for r in c.rubrics: + hint = "" if r.score == 2 else f" -> {r.gaps[0]}" if r.gaps else "" + click.echo(f" {r.id} {r.sub_criterion:30} {r.score} {r.label}{hint}") + else: + click.echo(result.model_dump_json(indent=2)) + + @rocrate_group.group('register') def register(): """Add a metadata record to the RO-Crate for a Dataset, Software, or Computation (metadata only).""" @@ -380,20 +417,8 @@ def registerSoftware( custom_properties: Optional[str], ): """Register Software metadata with the specified RO-Crate.""" - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {exc}", err=True) - ctx.exit(code=1) - - #Logic to determine the file_path/location - if not filepath and not content_url and not embargoed: - click.echo("ERROR: Either 'filepath', 'content-url', or 'embargoed' must be provided for software registration.", err=True) - ctx.exit(code=1) - if not filepath and not content_url and embargoed: - filepath = "Embargoed" - if not filepath and content_url: - filepath = content_url + _read_crate_or_exit(ctx, rocrate_path) + filepath = _resolve_file_location_or_exit(ctx, filepath, content_url, embargoed, "software") params = { "guid": guid, "name": name, "author": author, "version": version, @@ -404,32 +429,8 @@ def registerSoftware( "additionalDocumentation": additional_documentation, "cratePath": rocrate_path } - - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) - - # Filter None values before passing - filtered_params = {k: v for k, v in params.items() if v is not None} - - try: - software_instance = GenerateSoftware(**filtered_params) - AppendCrate(cratePath=rocrate_path, elements=[software_instance]) - click.echo(software_instance.guid) - except FileNotInCrateException as e: - click.echo(f"ERROR: {e}", err=True) - ctx.exit(code=1) - except ValidationError as e: - click.echo(f"ERROR: Software Validation Failure\n{e}", err=True) - ctx.exit(code=1) - except Exception as exc: - click.echo(f"ERROR: {exc}", err=True) - ctx.exit(code=1) + _merge_custom_properties_or_exit(ctx, params, custom_properties) + _generate_append_echo_or_exit(ctx, rocrate_path, GenerateSoftware, params, "Software") @register.command('dataset') @@ -492,21 +493,9 @@ def registerDataset( fairscape rocrate register dataset ./my-crate --name "My Dataset" ... --custom-properties '{"publisher": "Acme Corp", "license": "CC-BY-4.0"}' """ - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {str(exc)}") - ctx.exit(code=1) - - #Logic to determine the file_path/location - if not filepath and not content_url and not embargoed: - click.echo("ERROR: Either 'filepath', 'content-url', or 'embargoed' must be provided for dataset registration.", err=True) - ctx.exit(code=1) - if not filepath and not content_url and embargoed: - filepath = "Embargoed" - if not filepath and content_url: - filepath = content_url - + _read_crate_or_exit(ctx, rocrate_path, err=False) + filepath = _resolve_file_location_or_exit(ctx, filepath, content_url, embargoed, "dataset") + try: custom_props = {} if custom_properties: @@ -623,11 +612,7 @@ def computation( custom_properties: Optional[str], ): """Register Computation metadata with the specified RO-Crate.""" - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {exc}", err=True) - ctx.exit(code=1) + _read_crate_or_exit(ctx, rocrate_path) params = { "guid": guid, "name": name, "runBy": run_by, "command": command, @@ -638,29 +623,8 @@ def computation( "associatedPublication": associated_publication, "additionalDocumentation": additional_documentation } - - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) - - # Filter None values before passing - filtered_params = {k: v for k, v in params.items() if v is not None} - - try: - computationInstance = GenerateComputation(**filtered_params) - AppendCrate(cratePath=rocrate_path, elements=[computationInstance]) - click.echo(computationInstance.guid) - except ValidationError as e: - click.echo(f"ERROR: Computation Validation Failure\n{e}", err=True) - ctx.exit(code=1) - except Exception as exc: - click.echo(f"ERROR: {exc}", err=True) - ctx.exit(code=1) + _merge_custom_properties_or_exit(ctx, params, custom_properties) + _generate_append_echo_or_exit(ctx, rocrate_path, GenerateComputation, params, "Computation") @register.command('sample') @click.argument('rocrate-path', type=click.Path(exists=True, path_type=pathlib.Path)) @@ -686,40 +650,15 @@ def registerSample( custom_properties: Optional[str], ): """Register Sample metadata with the specified RO-Crate.""" - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {exc}", err=True) - ctx.exit(code=1) + _read_crate_or_exit(ctx, rocrate_path) params = { "guid": guid, "name": name, "author": author, "description": description, "keywords": list(keywords), "filepath": filepath, "cellLineReference": cell_line_reference, "cratePath": rocrate_path } - - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) - - # Filter None values before passing - filtered_params = {k: v for k, v in params.items() if v is not None} - - try: - sample_instance = GenerateSample(**filtered_params) - AppendCrate(cratePath=rocrate_path, elements=[sample_instance]) - click.echo(sample_instance.guid) - except ValidationError as e: - click.echo(f"ERROR: Sample Validation Failure\n{e}", err=True) - ctx.exit(code=1) - except Exception as exc: - click.echo(f"ERROR: {exc}", err=True) - ctx.exit(code=1) + _merge_custom_properties_or_exit(ctx, params, custom_properties) + _generate_append_echo_or_exit(ctx, rocrate_path, GenerateSample, params, "Sample") @register.command('instrument') @@ -750,11 +689,7 @@ def registerInstrument( custom_properties: Optional[str], ): """Register Instrument metadata with the specified RO-Crate.""" - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {exc}", err=True) - ctx.exit(code=1) + _read_crate_or_exit(ctx, rocrate_path) params = { "guid": guid, "name": name, "manufacturer": manufacturer, "model": model, @@ -764,29 +699,8 @@ def registerInstrument( "additionalDocumentation": additional_documentation, "cratePath": rocrate_path } - - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) - - # Filter None values before passing - filtered_params = {k: v for k, v in params.items() if v is not None} - - try: - instrument_instance = GenerateInstrument(**filtered_params) - AppendCrate(cratePath=rocrate_path, elements=[instrument_instance]) - click.echo(instrument_instance.guid) - except ValidationError as e: - click.echo(f"ERROR: Instrument Validation Failure\n{e}", err=True) - ctx.exit(code=1) - except Exception as exc: - click.echo(f"ERROR: {exc}", err=True) - ctx.exit(code=1) + _merge_custom_properties_or_exit(ctx, params, custom_properties) + _generate_append_echo_or_exit(ctx, rocrate_path, GenerateInstrument, params, "Instrument") @register.command('experiment') @@ -825,11 +739,7 @@ def registerExperiment( custom_properties: Optional[str], ): """Register Experiment metadata with the specified RO-Crate.""" - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {exc}", err=True) - ctx.exit(code=1) + _read_crate_or_exit(ctx, rocrate_path) params = { "guid": guid, "name": name, "experimentType": experiment_type, "runBy": run_by, @@ -842,29 +752,8 @@ def registerExperiment( "protocol": protocol, "associatedPublication": associated_publication, } - - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) - - # Filter None values before passing - filtered_params = {k: v for k, v in params.items() if v is not None} - - try: - experiment_instance = GenerateExperiment(**filtered_params) - AppendCrate(cratePath=rocrate_path, elements=[experiment_instance]) - click.echo(experiment_instance.guid) - except ValidationError as e: - click.echo(f"ERROR: Experiment Validation Failure\n{e}", err=True) - ctx.exit(code=1) - except Exception as exc: - click.echo(f"ERROR: {exc}", err=True) - ctx.exit(code=1) + _merge_custom_properties_or_exit(ctx, params, custom_properties) + _generate_append_echo_or_exit(ctx, rocrate_path, GenerateExperiment, params, "Experiment") @register.command('biochementity') @@ -887,11 +776,7 @@ def registerBioChemEntity( custom_properties: Optional[str], ): """Register BioChemEntity metadata with the specified RO-Crate.""" - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {exc}", err=True) - ctx.exit(code=1) + _read_crate_or_exit(ctx, rocrate_path) params = { "guid": guid, "name": name, "description": description, @@ -903,28 +788,9 @@ def registerBioChemEntity( if identifier: params['identifier'] = list(identifier) - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) - - # Filter None values before passing - filtered_params = {k: v for k, v in params.items() if v is not None} - - try: - bioChemEntityInstance = GenerateBioChemEntity(**filtered_params) - AppendCrate(cratePath=rocrate_path, elements=[bioChemEntityInstance]) - click.echo(bioChemEntityInstance.guid) - except ValidationError as e: - click.echo(f"ERROR: Sample Validation Failure\n{e}", err=True) - ctx.exit(code=1) - except Exception as exc: - click.echo(f"ERROR: {exc}", err=True) - ctx.exit(code=1) + _merge_custom_properties_or_exit(ctx, params, custom_properties) + # entity_label "Sample" preserves the historical error message for this command + _generate_append_echo_or_exit(ctx, rocrate_path, GenerateBioChemEntity, params, "Sample") @register.command('model') @click.argument('rocrate-path', type=click.Path(exists=True, path_type=pathlib.Path)) @@ -983,20 +849,9 @@ def registerModel( citation: Optional[str], custom_properties: Optional[str], ): - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {exc}", err=True) - ctx.exit(code=1) - - if not filepath and not content_url and not embargoed: - click.echo("ERROR: Either 'filepath', 'content-url', or 'embargoed' must be provided for model registration.", err=True) - ctx.exit(code=1) - if not filepath and not content_url and embargoed: - filepath = "Embargoed" - if not filepath and content_url: - filepath = content_url - + _read_crate_or_exit(ctx, rocrate_path) + filepath = _resolve_file_location_or_exit(ctx, filepath, content_url, embargoed, "model") + params = { "guid": guid, "name": name, @@ -1034,31 +889,8 @@ def registerModel( if citation: params["citation"] = citation - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): - raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) - - filtered_params = {k: v for k, v in params.items() if v is not None} - - try: - model_instance = GenerateModel(**filtered_params) - AppendCrate(cratePath=rocrate_path, elements=[model_instance]) - click.echo(model_instance.guid) - except FileNotInCrateException as e: - click.echo(f"ERROR: {e}", err=True) - ctx.exit(code=1) - except ValidationError as e: - click.echo(f"ERROR: Model Validation Failure\n{e}", err=True) - ctx.exit(code=1) - except Exception as exc: - click.echo(f"ERROR: {exc}", err=True) - ctx.exit(code=1) + _merge_custom_properties_or_exit(ctx, params, custom_properties) + _generate_append_echo_or_exit(ctx, rocrate_path, GenerateModel, params, "Model") @register.command('hf') @@ -1118,11 +950,7 @@ def registerHuggingFaceModel( fairscape rocrate register hf timm/densenet121.tv_in1k ./my-crate fairscape rocrate register hf timm/densenet121.tv_in1k ./my-crate --generated-by ark:12345/computation-xyz """ - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {exc}", err=True) - ctx.exit(code=1) + _read_crate_or_exit(ctx, rocrate_path) # Fetch metadata from HuggingFace try: @@ -1179,39 +1007,17 @@ def registerHuggingFaceModel( if hf_metadata.get('README'): params["README"] = hf_metadata.get('README') - # Handle custom properties - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): - raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) - - # Filter None values - filtered_params = {k: v for k, v in params.items() if v is not None} + _merge_custom_properties_or_exit(ctx, params, custom_properties) # Validate required fields - if not filtered_params.get('name'): + if not params.get('name'): click.echo("ERROR: Could not determine model name from HuggingFace. Please specify --name", err=True) ctx.exit(code=1) - if not filtered_params.get('description'): + if not params.get('description'): click.echo("ERROR: Could not determine model description from HuggingFace. Please specify --description", err=True) ctx.exit(code=1) - # Register the model - try: - model_instance = GenerateModel(**filtered_params) - AppendCrate(cratePath=rocrate_path, elements=[model_instance]) - click.echo(model_instance.guid) - except ValidationError as e: - click.echo(f"ERROR: Model Validation Failure\n{e}", err=True) - ctx.exit(code=1) - except Exception as exc: - click.echo(f"ERROR: {exc}", err=True) - ctx.exit(code=1) + _generate_append_echo_or_exit(ctx, rocrate_path, GenerateModel, params, "Model") @register.command('subrocrate') @@ -1410,21 +1216,9 @@ def addSoftware( custom_properties: Optional[str], ): """Copy a Software file into the RO-Crate and register its metadata.""" - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {str(exc)}", err=True) - ctx.exit(code=1) - - if destination_filepath.is_absolute(): - click.echo(f"ERROR: --destination-filepath must be a relative path within the RO-Crate: {destination_filepath}", err=True) - ctx.exit(code=1) - - try: - CopyToROCrate(source_filepath, pathlib.Path(rocrate_path) / destination_filepath) - except Exception as exc: - click.echo(f"ERROR copying file to RO-Crate: {str(exc)}", err=True) - ctx.exit(code=1) + _read_crate_or_exit(ctx, rocrate_path) + _require_relative_destination(ctx, destination_filepath, "--destination-filepath") + _copy_into_crate_or_exit(ctx, source_filepath, rocrate_path, destination_filepath) params = { "guid": guid, @@ -1443,14 +1237,7 @@ def addSoftware( "cratePath": rocrate_path } - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) + _merge_custom_properties_or_exit(ctx, params, custom_properties) filtered_params = {k: v for k, v in params.items() if v is not None} @@ -1532,19 +1319,9 @@ def addDataset( fairscape rocrate add dataset ./my-crate --name "My Data" ... --source-filepath /local/data.csv --destination-filepath data/data.csv --summary-statistics-source /local/data_summary.json --summary-statistics-destination data/data_summary.json """ - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {str(exc)}", err=True) - ctx.exit(code=1) - - if destination_filepath.is_absolute(): - click.echo(f"ERROR: --destination-filepath must be a relative path within the RO-Crate: {destination_filepath}", err=True) - ctx.exit(code=1) - - if summary_statistics_destination and summary_statistics_destination.is_absolute(): - click.echo(f"ERROR: --summary-statistics-destination must be a relative path within the RO-Crate: {summary_statistics_destination}", err=True) - ctx.exit(code=1) + _read_crate_or_exit(ctx, rocrate_path) + _require_relative_destination(ctx, destination_filepath, "--destination-filepath") + _require_relative_destination(ctx, summary_statistics_destination, "--summary-statistics-destination") try: CopyToROCrate(source_filepath, pathlib.Path(rocrate_path) / destination_filepath) @@ -1682,21 +1459,9 @@ def addModel( citation: Optional[str], custom_properties: Optional[str], ): - try: - ReadROCrateMetadata(rocrate_path) - except Exception as exc: - click.echo(f"ERROR Reading ROCrate: {exc}", err=True) - ctx.exit(code=1) - - if destination_filepath.is_absolute(): - click.echo(f"ERROR: --destination-filepath must be a relative path within the RO-Crate: {destination_filepath}", err=True) - ctx.exit(code=1) - - try: - CopyToROCrate(source_filepath, pathlib.Path(rocrate_path) / destination_filepath) - except Exception as exc: - click.echo(f"ERROR copying file to RO-Crate: {exc}", err=True) - ctx.exit(code=1) + _read_crate_or_exit(ctx, rocrate_path) + _require_relative_destination(ctx, destination_filepath, "--destination-filepath") + _copy_into_crate_or_exit(ctx, source_filepath, rocrate_path, destination_filepath) params = { "guid": guid, @@ -1735,28 +1500,5 @@ def addModel( if citation: params["citation"] = citation - if custom_properties: - try: - custom_props = json.loads(custom_properties) - if not isinstance(custom_props, dict): - raise ValueError("Custom properties must be a JSON object") - params.update(custom_props) - except Exception as e: - click.echo(f"ERROR processing custom properties: {e}", err=True) - ctx.exit(code=1) - - filtered_params = {k: v for k, v in params.items() if v is not None} - - try: - model_instance = GenerateModel(**filtered_params) - AppendCrate(cratePath=rocrate_path, elements=[model_instance]) - click.echo(model_instance.guid) - except ValidationError as e: - click.echo(f"ERROR: Model Validation Failure\n{e}", err=True) - ctx.exit(code=1) - except FileNotInCrateException as e: - click.echo(f"ERROR: {e}", err=True) - ctx.exit(code=1) - except Exception as exc: - click.echo(f"ERROR: {exc}", err=True) - ctx.exit(code=1) + _merge_custom_properties_or_exit(ctx, params, custom_properties) + _generate_append_echo_or_exit(ctx, rocrate_path, GenerateModel, params, "Model") diff --git a/src/fairscape_cli/data_fetcher/PhysioNetImporter.py b/src/fairscape_cli/data_fetcher/PhysioNetImporter.py index a8b1e67..12ed352 100644 --- a/src/fairscape_cli/data_fetcher/PhysioNetImporter.py +++ b/src/fairscape_cli/data_fetcher/PhysioNetImporter.py @@ -296,47 +296,113 @@ def extract_section_html(section_id: str) -> Optional[str]: metadata['extracted_keywords'] = keywords - doi_version_p = discovery_card_body.find('p', string=lambda text: text and text.strip().startswith('DOI (version')) - if doi_version_p: - doi_link = doi_version_p.find('a') - if doi_link and doi_link.get('href'): - metadata['dataset_identifier'] = doi_link['href'] - - latest_doi_p = discovery_card_body.find('p', string=lambda text: text and text.strip().startswith('DOI (latest version')) - if latest_doi_p and 'dataset_identifier' not in metadata: - latest_doi_link = latest_doi_p.find('a') - if latest_doi_link and latest_doi_link.get('href'): - metadata['dataset_identifier'] = latest_doi_link['href'] + # PhysioNet wraps the DOI in

...

, + # so the parent

has mixed children and BS4's string= predicate never matches. + # Locate the label first and walk up to the enclosing

. + def _doi_from_strong(label_prefix: str) -> Optional[str]: + strong_tag = discovery_card_body.find( + 'strong', + string=lambda text: text and text.strip().startswith(label_prefix) + ) + if not strong_tag: + return None + parent_p = strong_tag.find_parent('p') + if not parent_p: + return None + doi_link = parent_p.find('a', href=True) + return doi_link['href'] if doi_link else None + + versioned_doi = _doi_from_strong('DOI (version') + if versioned_doi: + metadata['dataset_identifier'] = versioned_doi + + if 'dataset_identifier' not in metadata: + latest_doi = _doi_from_strong('DOI (latest version') + if latest_doi: + metadata['dataset_identifier'] = latest_doi access_card_body = None access_header = soup.find('h5', class_='card-header', string='Access') if access_header: access_card_body = access_header.find_next_sibling('div', class_='card-body') if access_card_body: - license_p = access_card_body.find('p', string=lambda text: text and text.strip().startswith('License (for files):')) + # Same mixed-children issue as DOI: locate the label and look up to

. + strong_license = access_card_body.find( + 'strong', + string=lambda text: text and text.strip().startswith('License (for files):') + ) + license_p = strong_license.find_parent('p') if strong_license else None if license_p: - license_link = license_p.find('a') - if license_link and license_link.get('href'): + license_link = license_p.find('a', href=True) + if license_link: metadata['extracted_license'] = license_link['href'] return metadata + def _fetch_checksums(self) -> Tuple[Dict[str, str], Optional[str]]: + """Fetch and parse PhysioNet's checksum manifest for this dataset. + + Returns a (mapping, algorithm) tuple where mapping is {relative_path: hash} + and algorithm is one of "sha256", "md5", or None when no manifest is found. + Each manifest line is ` ` (single space separator). + """ + candidates = [ + ("SHA256SUMS.txt", "sha256"), + ("SHA256SUMS", "sha256"), + ("MD5SUMS.txt", "md5"), + ("MD5SUMS", "md5"), + ] + for filename, algorithm in candidates: + url = f"{self.base_file_download_url}{filename}" + try: + response = requests.get(url, timeout=30) + except requests.exceptions.RequestException as e: + print(f"Warning: Failed to fetch checksum file {url}: {e}", file=sys.stderr) + continue + if response.status_code != 200: + continue + + mapping: Dict[str, str] = {} + for line in response.text.splitlines(): + line = line.strip() + if not line: + continue + # Format: " " — split on first whitespace only, + # since paths may contain spaces. + parts = line.split(None, 1) + if len(parts) != 2: + continue + digest, rel_path = parts[0].strip(), parts[1].strip() + mapping[rel_path] = digest + if mapping: + print(f"Loaded {len(mapping)} {algorithm} checksums from {filename}", file=sys.stderr) + return mapping, algorithm + return {}, None + def _process_file_table_rows(self, table_body_soup: BeautifulSoup, current_subdir_path: str, all_files_list: List[Dict[str, Any]], subdirs_to_explore: List[str], crate_default_date_published: str): rows_to_process = list(table_body_soup.select('tr')) for row in rows_to_process: + row_classes = row.get('class', []) + + # PhysioNet renders a "Parent Directory" navigation row (class="parentdir") + # on every subdirectory page. It has no class="subdir", so without this skip + # it would be misclassified as a file and added as a Dataset per traversed dir. + if 'parentdir' in row_classes: + continue + name_cell_a_tag = row.select_one('td a') if not name_cell_a_tag: continue name = name_cell_a_tag.get_text(strip=True) - - if name in ['.', '..', '']: + + if name in ['.', '..', '', 'Parent Directory']: continue - is_directory = 'subdir' in row.get('class', []) - + is_directory = 'subdir' in row_classes or name_cell_a_tag.has_attr('data-dfp-dir') + item_relative_path: str = "" if is_directory: item_relative_path = name_cell_a_tag.get('data-dfp-dir') @@ -346,9 +412,9 @@ def _process_file_table_rows(self, table_body_soup: BeautifulSoup, current_subdi if item_relative_path not in subdirs_to_explore and item_relative_path != current_subdir_path : print(f"DEBUG: Found directory: {name} (data-dfp-dir: {item_relative_path}) in {current_subdir_path or ''}. Adding to queue.", file=sys.stderr) - subdirs_to_explore.append(item_relative_path) - else: - item_relative_path = os.path.join(current_subdir_path, name).replace('\\', '/') + subdirs_to_explore.append(item_relative_path) + else: + item_relative_path = os.path.join(current_subdir_path, name).replace('\\', '/') if not is_directory: @@ -532,6 +598,8 @@ def to_rocrate( print(f"Total files found after exploring subdirectories: {len(all_file_entries)}", file=sys.stderr) + checksum_map, checksum_algorithm = self._fetch_checksums() + crate_elements_to_add = [] for item_data in all_file_entries: item_path_guid_segment = item_data['relative_path'].replace('/', '_').replace('.', '_').replace(' ', '_').lower() @@ -554,6 +622,10 @@ def to_rocrate( if 'contentSize' in item_data: dataset_params['contentSize'] = item_data['contentSize'] if 'contentUrl' in item_data: dataset_params['contentUrl'] = item_data['contentUrl'] + digest = checksum_map.get(item_data['relative_path']) + if digest and checksum_algorithm: + dataset_params[checksum_algorithm] = digest + try: dataset_entity = GenerateDataset(**dataset_params) crate_elements_to_add.append(dataset_entity) diff --git a/src/fairscape_cli/data_fetcher/generic_data/connectors/dataverse_connector.py b/src/fairscape_cli/data_fetcher/generic_data/connectors/dataverse_connector.py index 605cd54..8710b54 100644 --- a/src/fairscape_cli/data_fetcher/generic_data/connectors/dataverse_connector.py +++ b/src/fairscape_cli/data_fetcher/generic_data/connectors/dataverse_connector.py @@ -213,7 +213,9 @@ def fetch_data(self, doi: str, include_files: bool = True) -> ResearchData: if is_software: software_data.append({ **item_common_data, - "version": latest_version_info.get("versionNumber", "1.0"), # Use dataset version for software + # Dataverse returns versionNumber as a numeric type; the + # Software pydantic model requires a string. + "version": str(latest_version_info.get("versionNumber", "1.0")), "documentation_url": dataset_landing_url }) else: @@ -249,7 +251,7 @@ def fetch_data(self, doi: str, include_files: bool = True) -> ResearchData: "url": f"{self.server_url}/dataset.xhtml?persistentId=doi:{doi}", "contentUrl": f"{self.server_url}/dataset.xhtml?persistentId=doi:{doi}", "publisher": dataset_metadata_full.get("publisher", ""), - "version": latest_version_info.get("versionNumber"), + "version": str(latest_version_info.get("versionNumber")) if latest_version_info.get("versionNumber") is not None else None, } ) diff --git a/src/fairscape_cli/data_fetcher/generic_data/connectors/manifest_connector.py b/src/fairscape_cli/data_fetcher/generic_data/connectors/manifest_connector.py new file mode 100644 index 0000000..0900534 --- /dev/null +++ b/src/fairscape_cli/data_fetcher/generic_data/connectors/manifest_connector.py @@ -0,0 +1,244 @@ +"""Manifest connector. + +Reads a CSV manifest + JSON sidecar describing a published dataset that +doesn't have (or doesn't need) a dedicated repository connector, and returns a +ResearchData instance for `ResearchData.to_rocrate()` to write out. +""" +import csv +import json +import os +import pathlib +from datetime import datetime +from typing import Any, Dict, List, Optional + +from fairscape_cli.data_fetcher.generic_data.research_data import ResearchData + + +SIDECAR_REQUIRED_FIELDS = ("name", "description", "authors") +MANIFEST_REQUIRED_COLUMNS = ("name", "description", "contentUrl") +SIZE_UNITS = ("B", "KB", "MB", "GB", "TB", "PB") + +# Extensions that should default to Software entities rather than Datasets. +# Mirrors fairscape_cli.data_fetcher.generic_data.connectors.dataverse_connector +# with a few additional script suffixes. +SOFTWARE_EXTENSIONS = { + ".py", ".r", ".sh", ".bash", ".ipynb", ".jl", ".m", + ".exe", ".java", ".cpp", ".js", ".jsx", ".ts", ".tsx", ".css", +} + + +def _auto_detect_type(name: str) -> str: + """Return 'software' or 'dataset' based on file extension.""" + ext = os.path.splitext(name)[1].lower() + return "software" if ext in SOFTWARE_EXTENSIONS else "dataset" + + +def _human_size(n: int) -> str: + """Format byte count as a short human-readable string matching the other connectors.""" + if n is None: + return "" + size = float(n) + i = 0 + while size >= 1024 and i < len(SIZE_UNITS) - 1: + size /= 1024.0 + i += 1 + if i == 0: + return f"{int(size)} {SIZE_UNITS[i]}" + return f"{size:.1f} {SIZE_UNITS[i]}" + + +def _ext_format(name: str) -> str: + """Derive a format token from a filename extension. Returns 'unknown' if there's nothing.""" + ext = os.path.splitext(name)[1].lstrip(".") + return ext.lower() if ext else "unknown" + + +def _split_pipe(value: Optional[str]) -> List[str]: + if not value: + return [] + return [piece.strip() for piece in value.split("|") if piece.strip()] + + +def _resolve_sidecar(manifest_path: pathlib.Path, explicit: Optional[pathlib.Path]) -> pathlib.Path: + if explicit is not None: + if not explicit.exists(): + raise ValueError(f"Sidecar not found: {explicit}") + return explicit + + siblings = [ + manifest_path.with_suffix(".json"), + manifest_path.parent / "crate.json", + ] + for candidate in siblings: + if candidate.exists(): + return candidate + + raise ValueError( + "No sidecar found. Looked for " + + ", ".join(str(s) for s in siblings) + + ". Pass --sidecar to point at one explicitly." + ) + + +def _load_sidecar(path: pathlib.Path) -> Dict[str, Any]: + with path.open("r", encoding="utf-8") as f: + data = json.load(f) + missing = [field for field in SIDECAR_REQUIRED_FIELDS if not data.get(field)] + if missing: + raise ValueError( + f"Sidecar {path} is missing required field(s): {', '.join(missing)}" + ) + if not isinstance(data.get("authors"), list): + raise ValueError(f"Sidecar 'authors' must be a list (got {type(data.get('authors')).__name__}).") + return data + + +def _row_to_file_dict(row: Dict[str, str], sidecar: Dict[str, Any]) -> Dict[str, Any]: + """Translate one manifest row into a dict ready for ResearchData. + + The dict carries an extra '_type' key (`'dataset'` or `'software'`) so the + caller can route it into `ResearchData.files` or `ResearchData.software`. + The `_type` is sourced from the row's `type` column (case-insensitive), + falling back to extension-based auto-detection. + """ + name = (row.get("name") or "").strip() + if not name: + raise ValueError(f"Manifest row missing 'name': {row}") + + content_url = (row.get("contentUrl") or "").strip() + if not content_url: + raise ValueError(f"Manifest row '{name}' missing 'contentUrl'.") + + declared_type = (row.get("type") or "").strip().lower() + if declared_type in ("software", "dataset"): + row_type = declared_type + elif declared_type == "": + row_type = _auto_detect_type(name) + else: + raise ValueError( + f"Manifest row '{name}' has unknown type {declared_type!r}; " + f"expected 'dataset', 'software', or blank." + ) + + description = (row.get("description") or "").strip() or sidecar.get("description") or "" + fmt = (row.get("format") or "").strip() or _ext_format(name) + + # Size: prefer explicit byte count (we format it); else accept pre-formatted string; else nothing. + content_size: Optional[str] = None + size_bytes_raw = (row.get("size_bytes") or "").strip() + if size_bytes_raw: + try: + content_size = _human_size(int(size_bytes_raw)) + except ValueError: + raise ValueError(f"Manifest row '{name}' has non-integer size_bytes: {size_bytes_raw!r}") + elif (row.get("contentSize") or "").strip(): + content_size = row["contentSize"].strip() + + file_dict: Dict[str, Any] = { + "name": name, + "description": description, + "contentUrl": content_url, + "url": content_url, + "format": fmt, + "datePublished": (row.get("datePublished") or "").strip() or sidecar.get("publication_date"), + "version": (row.get("version") or "").strip() or "1.0", + } + + md5 = (row.get("md5") or "").strip() + if md5: + file_dict["md5"] = md5 + sha256 = (row.get("sha256") or "").strip() + if sha256: + file_dict["sha256"] = sha256 + if content_size: + file_dict["contentSize"] = content_size + # The Dataverse connector also stuffs raw bytes under 'size'; mirror that + # so anything downstream that reads 'size' keeps working. + if size_bytes_raw: + file_dict["size"] = int(size_bytes_raw) + + per_row_keywords = _split_pipe(row.get("keywords")) + if per_row_keywords: + file_dict["keywords"] = per_row_keywords + + group = (row.get("group") or "").strip() + if group: + # Reserved for future schema-inference grouping; stash it where it'll + # round-trip without breaking anything. + file_dict["group"] = group + + file_dict["_type"] = row_type + return file_dict + + +def _to_software_dict(record: Dict[str, Any]) -> Dict[str, Any]: + """Translate a file-dict (built for ResearchData.files) into the shape + ResearchData.software expects, which `to_rocrate` passes to GenerateSoftware. + + The only meaningful difference is `datePublished` → `dateModified`. + """ + software = dict(record) + if "datePublished" in software: + software["dateModified"] = software.pop("datePublished") + return software + + +class ManifestConnector: + """Reads a CSV manifest + JSON sidecar and produces ResearchData.""" + + def __init__(self, sidecar_path: Optional[pathlib.Path] = None): + self.sidecar_path = pathlib.Path(sidecar_path) if sidecar_path else None + + def fetch_data(self, manifest_path: str, include_files: bool = True) -> ResearchData: + manifest_p = pathlib.Path(manifest_path) + if not manifest_p.exists(): + raise ValueError(f"Manifest not found: {manifest_p}") + + sidecar_p = _resolve_sidecar(manifest_p, self.sidecar_path) + sidecar = _load_sidecar(sidecar_p) + + files_data: List[Dict[str, Any]] = [] + software_data: List[Dict[str, Any]] = [] + if include_files: + with manifest_p.open("r", encoding="utf-8-sig", newline="") as f: + reader = csv.DictReader(f) + missing_cols = [c for c in MANIFEST_REQUIRED_COLUMNS if c not in (reader.fieldnames or [])] + if missing_cols: + raise ValueError( + f"Manifest {manifest_p} is missing required column(s): {', '.join(missing_cols)}. " + f"Found columns: {reader.fieldnames}" + ) + for row in reader: + if not any((row.get(c) or "").strip() for c in row): + continue # skip blank lines + record = _row_to_file_dict(row, sidecar) + row_type = record.pop("_type") + if row_type == "software": + software_data.append(_to_software_dict(record)) + else: + files_data.append(record) + + publication_date = sidecar.get("publication_date") or datetime.utcnow().date().isoformat() + + metadata: Dict[str, Any] = {} + if sidecar.get("url"): + metadata["url"] = sidecar["url"] + if sidecar.get("associated_publication"): + metadata["contentUrl"] = sidecar["associated_publication"] + if sidecar.get("version"): + metadata["version"] = sidecar["version"] + + return ResearchData( + repository_name=sidecar.get("repository_name") or "Manifest", + project_id=sidecar.get("project_id") or sidecar.get("doi") or manifest_p.stem, + title=sidecar["name"], + description=sidecar["description"], + authors=list(sidecar["authors"]), + license=sidecar.get("license"), + keywords=list(sidecar.get("keywords") or []), + publication_date=publication_date, + doi=sidecar.get("doi"), + files=files_data, + software=software_data, + metadata=metadata, + ) diff --git a/src/fairscape_cli/data_fetcher/generic_data/research_data.py b/src/fairscape_cli/data_fetcher/generic_data/research_data.py index fc29888..6934c12 100644 --- a/src/fairscape_cli/data_fetcher/generic_data/research_data.py +++ b/src/fairscape_cli/data_fetcher/generic_data/research_data.py @@ -90,12 +90,14 @@ def to_rocrate(self, output_dir: str, **kwargs) -> str: } if file_info.get("md5"): dataset_params["md5"] = file_info["md5"] + if file_info.get("sha256"): + dataset_params["sha256"] = file_info["sha256"] if file_info.get("contentSize"): dataset_params["contentSize"] = file_info["contentSize"] - + dataset = GenerateDataset(**dataset_params) elements_to_append.append(dataset) - + for software_info in self.software: software_version = software_info.get("version", "0.1.0") @@ -105,8 +107,8 @@ def to_rocrate(self, output_dir: str, **kwargs) -> str: software_author = self.authors[0] if self.authors else "Unknown" - software_item = GenerateSoftware( - guid=None, + software_kwargs = dict( + guid=None, name=software_info.get("name", "Unnamed software"), author=software_author, dateModified=software_info.get("dateModified", datetime.now().isoformat()), @@ -114,10 +116,18 @@ def to_rocrate(self, output_dir: str, **kwargs) -> str: description=software_description, associatedPublication=self.doi or None, additionalDocumentation=software_info.get("documentation_url", None), - format=software_info.get("format", "application/zip"), + format=software_info.get("format", "application/zip"), usedByComputation=[], - contentUrl=software_info.get("contentUrl", software_info.get("url", "")) + contentUrl=software_info.get("contentUrl", software_info.get("url", "")), ) + if software_info.get("md5"): + software_kwargs["md5"] = software_info["md5"] + if software_info.get("sha256"): + software_kwargs["sha256"] = software_info["sha256"] + if software_info.get("contentSize"): + software_kwargs["contentSize"] = software_info["contentSize"] + + software_item = GenerateSoftware(**software_kwargs) elements_to_append.append(software_item) if elements_to_append: @@ -138,6 +148,10 @@ def from_repository(cls, repository_type: str, identifier: str, **kwargs) -> 'Re api_token=kwargs.get('token') ) return connector.fetch_data(identifier, include_files=kwargs.get('include_files', True)) + elif repository_type.lower() == 'manifest': + from fairscape_cli.data_fetcher.generic_data.connectors.manifest_connector import ManifestConnector + connector = ManifestConnector(sidecar_path=kwargs.get('sidecar_path')) + return connector.fetch_data(identifier, include_files=kwargs.get('include_files', True)) else: raise ValueError(f"Unsupported repository type: {repository_type}") diff --git a/src/fairscape_cli/datasheet_builder/__init__.py b/src/fairscape_cli/datasheet_builder/__init__.py index 97e0706..56635fb 100644 --- a/src/fairscape_cli/datasheet_builder/__init__.py +++ b/src/fairscape_cli/datasheet_builder/__init__.py @@ -2,6 +2,14 @@ Datasheet Builder module for RO-Crate metadata visualization and documentation. """ +from pathlib import Path + from fairscape_cli.datasheet_builder.rocrate.datasheet_generator import DatasheetGenerator -__all__ = ['DatasheetGenerator'] \ No newline at end of file + +def get_default_template_dir() -> Path: + """Return the Jinja template directory shipped with the package.""" + return Path(__file__).parent / 'templates' + + +__all__ = ['DatasheetGenerator', 'get_default_template_dir'] diff --git a/src/fairscape_cli/datasheet_builder/evidence_graph/graph_builder.py b/src/fairscape_cli/datasheet_builder/evidence_graph/graph_builder.py deleted file mode 100644 index 62842c8..0000000 --- a/src/fairscape_cli/datasheet_builder/evidence_graph/graph_builder.py +++ /dev/null @@ -1,242 +0,0 @@ -from typing import Dict, List, Optional, Any, Union, Set -import json -import pathlib -from pathlib import Path -import click - -class EvidenceNode: - def __init__(self, id: str, type: str): - self.id = id - self.type = type - # For Computation nodes - self.usedSoftware: Optional[List[str]] = None - self.usedDataset: Optional[List[str]] = None - self.usedSample: Optional[List[str]] = None - self.usedInstrument: Optional[List[str]] = None - # For Dataset/Sample/Instrument nodes - self.generatedBy: Optional[str] = None - -class EvidenceGraphJSON: - def __init__(self, guid: str, owner: str, description: str, name: str = "Evidence Graph"): - self.metadataType = "evi:EvidenceGraph" - self.guid = guid - self.owner = owner - self.description = description - self.name = name - self.graph = None - - def build_graph(self, node_id: str, json_data: Dict[str, Any]): - processed = set() - self.graph = self._build_graph_recursive(node_id, json_data, processed) - - def _build_graph_recursive(self, node_id: str, json_data: Dict[str, Any], processed: Set[str]) -> Dict: - if node_id in processed: - return {"@id": node_id} - - # Find node in json data graph - node = None - for entity in json_data.get("@graph", []): - if entity.get("@id") == node_id: - node = entity - break - - if not node: - return {"@id": node_id} - - processed.add(node_id) - result = self._build_base_node(node) - - # Determine node type based on @type - node_type = None - if isinstance(node.get("@type"), list): - for type_entry in node["@type"]: - if "Dataset" in type_entry: - node_type = "Dataset" - break - elif "Computation" in type_entry: - node_type = "Computation" - break - elif "Sample" in type_entry: - node_type = "Sample" - break - elif "Instrument" in type_entry: - node_type = "Instrument" - break - elif "Experiment" in type_entry: - node_type = "Experiment" - break - elif isinstance(node.get("@type"), str): - type_str = node.get("@type") - if "Dataset" in type_str: - node_type = "Dataset" - elif "Computation" in type_str: - node_type = "Computation" - elif "Sample" in type_str: - node_type = "Sample" - elif "Instrument" in type_str: - node_type = "Instrument" - elif "Experiment" in type_str: - node_type = "Experiment" - - if node_type in ["Dataset", "Sample", "Instrument"]: - if "generatedBy" in node: - result["generatedBy"] = self._build_computation_node(node, json_data, processed) - elif node_type in ["Computation", "Experiment"]: - if "usedDataset" in node: - result["usedDataset"] = self._build_used_resources(node["usedDataset"], json_data, processed) - if "usedSoftware" in node: - result["usedSoftware"] = self._build_software_reference(node["usedSoftware"], json_data) - if "usedSample" in node: - result["usedSample"] = self._build_used_resources(node["usedSample"], json_data, processed) - if "usedInstrument" in node: - result["usedInstrument"] = self._build_used_resources(node["usedInstrument"], json_data, processed) - - return result - - def _build_base_node(self, node: Dict) -> Dict: - return { - "@id": node["@id"], - "@type": node.get("@type"), - "name": node.get("name"), - "description": node.get("description") - } - - def _build_computation_node(self, parent_node: Dict, json_data: Dict[str, Any], processed: Set[str]) -> Dict: - # If generatedBy is an empty list, don't add anything - if isinstance(parent_node["generatedBy"], list) and len(parent_node["generatedBy"]) == 0: - return {} - - comp_id = None - if isinstance(parent_node["generatedBy"], list): - if parent_node["generatedBy"] and isinstance(parent_node["generatedBy"][0], dict): - comp_id = parent_node["generatedBy"][0].get("@id") - elif isinstance(parent_node["generatedBy"], dict): - comp_id = parent_node["generatedBy"].get("@id") - - if not comp_id: - return {"@id": "unknown-computation"} - - comp = None - for entity in json_data.get("@graph", []): - if entity.get("@id") == comp_id: - comp = entity - break - - if not comp: - return {"@id": comp_id} - - return self._build_graph_recursive(comp_id, json_data, processed) - - def _build_used_resources(self, used_resources: Union[Dict, List], json_data: Dict[str, Any], processed: Set[str]) -> List: - if isinstance(used_resources, dict): - resource_id = used_resources.get("@id") - if resource_id: - return [self._build_graph_recursive(resource_id, json_data, processed)] - return [] - - if isinstance(used_resources, list): - resources = [] - for resource in used_resources: - if isinstance(resource, dict) and resource.get("@id"): - resources.append(self._build_graph_recursive(resource.get("@id"), json_data, processed)) - return resources - - return [] - - def _build_software_reference(self, used_software: Union[Dict, List], json_data: Dict[str, Any]) -> Union[Dict, List[Dict]]: - if isinstance(used_software, list): - if not used_software: - return [] - - software_refs = [] - for sw in used_software: - if isinstance(sw, dict) and sw.get("@id"): - software_id = sw.get("@id") - software = None - for entity in json_data.get("@graph", []): - if entity.get("@id") == software_id: - software = entity - break - - if software: - software_refs.append(self._build_base_node(software)) - else: - software_refs.append({"@id": software_id}) - - return software_refs - - elif isinstance(used_software, dict) and used_software.get("@id"): - software_id = used_software.get("@id") - software = None - for entity in json_data.get("@graph", []): - if entity.get("@id") == software_id: - software = entity - break - - if software: - return self._build_base_node(software) - return {"@id": software_id} - - return {"@id": "unknown-software"} - - def to_dict(self): - return { - "@type": self.metadataType, - "@id": self.guid, - "owner": self.owner, - "description": self.description, - "name": self.name, - "@graph": self.graph - } - - def save_to_file(self, file_path: Union[str, pathlib.Path]): - with open(file_path, 'w') as f: - json.dump(self.to_dict(), f, indent=2) - -def generate_evidence_graph_from_rocrate( - rocrate_path: Union[str, pathlib.Path], - output_path: Optional[Union[str, pathlib.Path]] = None, - node_id: str = "" -) -> Dict[str, Any]: - """ - Generate an evidence graph from an RO-Crate JSON file for a specific node ID - - Args: - rocrate_path: Path to the RO-Crate metadata JSON file - output_path: Optional path to save the evidence graph JSON - node_id: ID of the node to start building the graph from - - Returns: - The generated evidence graph as a dictionary - """ - # Load RO-Crate data - with open(rocrate_path, 'r') as f: - rocrate_data = json.load(f) - - # Find the root entity - root_entity = None - for entity in rocrate_data.get("@graph", []): - if entity.get("@id") == node_id: - root_entity = entity - break - - if not root_entity: - raise ValueError(f"Could not find entity with ID {node_id} in RO-Crate") - - # Create evidence graph - graph_id = f"{node_id}-evidence-graph" - graph = EvidenceGraphJSON( - guid=graph_id, - owner=node_id, - description=f"Evidence graph for {root_entity.get('name', 'Unknown')}", - name=f"Evidence Graph - {root_entity.get('name', 'Unknown')}" - ) - - # Build the graph - graph.build_graph(node_id, rocrate_data) - - # Save to file if output path provided - if output_path: - graph.save_to_file(output_path) - - return graph.to_dict() diff --git a/src/fairscape_cli/datasheet_builder/evidence_graph/html_builder.py b/src/fairscape_cli/datasheet_builder/evidence_graph/html_builder.py index fd272db..1635ab9 100644 --- a/src/fairscape_cli/datasheet_builder/evidence_graph/html_builder.py +++ b/src/fairscape_cli/datasheet_builder/evidence_graph/html_builder.py @@ -1,33 +1,57 @@ -import json -import os +"""Generate a standalone, self-contained HTML visualization of an evidence graph. + +The HTML shell lives in templates/evidence_graph/evidence_graph.html.j2 and the +application JavaScript in evidence_graph.js next to it. React, ReactDOM and +dagre are vendored under templates/evidence_graph/vendor/ and inlined into the +output, so the generated file is a single offline-shareable HTML document with +no CDN dependency. +""" import argparse +import json +import logging from pathlib import Path +from jinja2 import Environment, FileSystemLoader + +logger = logging.getLogger(__name__) + +TEMPLATE_DIR = Path(__file__).parent.parent / 'templates' / 'evidence_graph' +VENDOR_FILES = ( + 'vendor/react.production.min.js', + 'vendor/react-dom.production.min.js', + 'vendor/dagre.min.js', +) + + +def _inline_script_safe(js: str) -> str: + """Make JS safe to inline in a ' breakout).""" + return js.replace(' - - - - - Evidence Graph Visualization - - - - - - -

- - - - - """ + vendor_js = ';\n'.join( + (TEMPLATE_DIR / name).read_text(encoding='utf-8') for name in VENDOR_FILES + ) + app_js = (TEMPLATE_DIR / 'evidence_graph.js').read_text(encoding='utf-8') + + # Escaping '<' keeps the JSON valid JS while preventing a '' (or + # ' - +
-

{{ title }}

-
Version: {{ version }}
+
+ {% if version %} + Version {{ version }} + {% endif %} + {% if doi %} + DOI ↗ + {% endif %} + {% if license_value %} + License ↗ + {% endif %} + {% if content_size %} + {{ content_size }} + {% endif %} + {% if release_date %} + Released {{ release_date }} + {% endif %} +
{{ summary_section | safe }} -
{{ overview_section | safe }}
-
{{ use_cases_section | safe }}
-

Composition (Datasets {{ subcrate_count }})

@@ -750,17 +1116,168 @@

Composition (Datasets {{ subcrate_count }})

>). - diff --git a/src/fairscape_cli/datasheet_builder/templates/evidence_graph/evidence_graph.html.j2 b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/evidence_graph.html.j2 new file mode 100644 index 0000000..eea851b --- /dev/null +++ b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/evidence_graph.html.j2 @@ -0,0 +1,205 @@ + + + + + + {{ title }} + + + +
+ +
+
Dataset / Sample
+
Computation / Experiment
+
Software / Instrument
+
Dataset Collection / Group
+
Expandable — click to expand
+
+ + + + + + diff --git a/src/fairscape_cli/datasheet_builder/templates/evidence_graph/evidence_graph.js b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/evidence_graph.js new file mode 100644 index 0000000..20f7c0d --- /dev/null +++ b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/evidence_graph.js @@ -0,0 +1,848 @@ +const evidenceGraphData = window.__EVIDENCE_GRAPH_DATA__; + const MAX_LABEL_LENGTH = 50; + const NODE_WIDTH = 180; + const NODE_HEIGHT = 90; + + function getEntityType(typeUri) { + if (!typeUri) return "Unknown"; + const typeString = Array.isArray(typeUri) ? typeUri[typeUri.length - 1] : typeUri; + const simpleType = typeString.split(/[#\/]/).pop() || "Unknown"; + const eviPrefix = "evi:"; + const specificType = (Array.isArray(typeUri) ? typeUri : [typeUri]) + .map(t => t.split(/[#\/]/).pop()) + .find(t => t && t.startsWith(eviPrefix)); + return specificType ? specificType.substring(eviPrefix.length) : simpleType; + } + + + function abbreviateName(name, maxLength = MAX_LABEL_LENGTH) { + if (!name) return ""; + if (name.length <= maxLength) return name; + return name.substring(0, maxLength - 3) + "..."; + } + + function createEvidenceNode(entityData) { + if (!entityData || !entityData["@id"]) return null; + const id = entityData["@id"]; + const type = getEntityType(entityData["@type"]); + let label = entityData.name || entityData.label || id; + const description = entityData.description || ""; + + const hasGeneratedBy = !!entityData.generatedBy; + const hasUsedSoftware = !!entityData.usedSoftware && (!Array.isArray(entityData.usedSoftware) || entityData.usedSoftware.length > 0); + const hasUsedDataset = !!entityData.usedDataset && (!Array.isArray(entityData.usedDataset) || entityData.usedDataset.length > 0); + const hasUsedSample = !!entityData.usedSample && (!Array.isArray(entityData.usedSample) || entityData.usedSample.length > 0); + const hasUsedInstrument = !!entityData.usedInstrument && (!Array.isArray(entityData.usedInstrument) || entityData.usedInstrument.length > 0); + // DatasetGroup is a UI condensation node emitted by EvidenceGraphBuilder when + // many Datasets share identical provenance. It carries `evi:representativeDataset` + // (a pointer to one member) and `evi:memberCount`/`evi:memberIds`. The graph + // should not dead-end at the group — clicking it (or auto-expansion) should + // follow the representative onward. + const hasGroupRepresentative = !!(entityData["evi:representativeDataset"] + && (typeof entityData["evi:representativeDataset"] === "string" + || entityData["evi:representativeDataset"]["@id"])); + + let isExpandable = false; + if (type === "Dataset" && hasGeneratedBy) { isExpandable = true; } + if ((type === "Computation" || type === "Experiment") && (hasUsedSoftware || hasUsedDataset || hasUsedSample || hasUsedInstrument)) { isExpandable = true; } + if (type === "DatasetGroup" && hasGroupRepresentative) { isExpandable = true; } + + if (type === "DatasetGroup") { + const memberCount = entityData["evi:memberCount"] + || (Array.isArray(entityData["evi:memberIds"]) ? entityData["evi:memberIds"].length : 0); + if (memberCount) { + label = `${label} — ${memberCount} members`; + } + } + const displayName = abbreviateName(label); + + if (isExpandable && type !== "DatasetGroup") { + const checkRelation = (prop) => { + const value = entityData[prop]; + if (!value) return false; + if (Array.isArray(value)) return value.some(item => item && (typeof item === 'string' || (typeof item === 'object' && item['@id']))); + return typeof value === 'string' || (typeof value === 'object' && value['@id']); + }; + isExpandable = checkRelation('generatedBy') || checkRelation('usedSoftware') || checkRelation('usedDataset') || checkRelation('usedSample') || checkRelation('usedInstrument'); + } + + const nodeData = { + id: id, + type: type, + label: label, + displayName: displayName, + description: description, + expandable: isExpandable, + _sourceData: { ...entityData }, + _expanded: false, + x: 0, + y: 0, + width: NODE_WIDTH, + height: NODE_HEIGHT + }; + return nodeData; + } + + function createDatasetCollectionNode(computationId, datasets) { + const collectionId = `${computationId}_dataset_collection_${Date.now()}`; + const validDatasets = datasets.filter(ds => ds && (typeof ds === "string" || (typeof ds === "object" && ds["@id"]))) + .map(ds => typeof ds === "string" ? { "@id": ds, "@type": "evi:Dataset" } : ds); + + const count = validDatasets.length; + if (count === 0) return null; + + const label = `Input Datasets (${count})`; + const displayName = `Datasets (${count})`; + + const nodeData = { + id: collectionId, + type: "DatasetCollection", + label: label, + displayName: displayName, + description: `A collection of ${count} datasets used by ${computationId.split(/[/#]/).pop()}`, + expandable: count > 0, + _sourceData: { "@id": collectionId, "@type": "evi:DatasetCollection", name: label, count: count }, + _remainingDatasets: [...validDatasets], + _expandedCount: 0, + _expanded: false, + x: 0, + y: 0, + width: NODE_WIDTH, + height: NODE_HEIGHT + }; + return nodeData; + } + + + function getGraphEntities(graphData) { + if (!graphData || !graphData["@graph"]) return {}; + const graphField = graphData["@graph"]; + + let entities; + if (Array.isArray(graphField)) { + entities = graphField; + } else if (typeof graphField === "object" && graphField["@id"]) { + entities = [graphField]; + } else if (typeof graphField === "object") { + // EvidenceGraphBuilder format: @graph is a dict keyed by entity @id + entities = Object.values(graphField); + } else { + entities = []; + } + + const entityMap = new Map(); + entities.forEach(e => { + if (e && e["@id"]) { + entityMap.set(e["@id"], e); + } + }); + + let rootEntityId = null; + if (Array.isArray(graphData.outputs) && graphData.outputs.length > 0) { + const firstOut = graphData.outputs[0]; + rootEntityId = typeof firstOut === "string" + ? firstOut + : (firstOut && firstOut["@id"]) || null; + } + if (!rootEntityId) { + const rootEntity = entities.find(e => e["@id"] === "./" || e["@id"] === "ro-crate-metadata.json") || entities[0]; + rootEntityId = rootEntity ? rootEntity["@id"] : null; + } + + return { rootEntityId, entityMap }; + } + + + // Default expansion depth covers the full chain when a DatasetGroup is in the path: + // root Dataset → Computation → (Software, DatasetGroup → representative → Experiment → Sample/Instrument). + function getInitialGraphState(graphData, initialExpansionDepth = 6) { + const { rootEntityId, entityMap } = getGraphEntities(graphData); + if (!rootEntityId || !entityMap.has(rootEntityId)) { + console.error("Could not determine root entity or find it in the graph data."); + return { nodes: [], edges: [] }; + } + + let nodes = []; + let edges = []; + let nodesToExpand = new Set(); + const addedNodeIds = new Set(); + + const rootEntityData = entityMap.get(rootEntityId); + const rootNode = createEvidenceNode(rootEntityData); + + if (!rootNode) { + console.error("Failed to create root node."); + return { nodes: [], edges: [] }; + } + + nodes.push(rootNode); + addedNodeIds.add(rootNode.id); + if (rootNode.expandable) { + nodesToExpand.add(rootNode.id); + } + + let currentLevelIds = new Set([rootNode.id]); + for (let level = 0; level < initialExpansionDepth; level++) { + let nextLevelIds = new Set(); + if (currentLevelIds.size === 0) break; + + currentLevelIds.forEach(nodeId => { + const nodeIndex = nodes.findIndex(n => n.id === nodeId); + if (nodeIndex === -1) return; + const node = nodes[nodeIndex]; + + if (!node || !node.expandable || node._expanded) return; + + let expansionResult; + if (node.type === "DatasetCollection") { + expansionResult = expandDatasetCollectionNodeInternal(node, nodes, edges, entityMap); + } else { + expansionResult = expandNodeInternal(node, nodes, edges, entityMap); + } + const { newNodes, newEdges, updatedCollectionData } = expansionResult; + + if (newNodes.length > 0 || newEdges.length > 0 || updatedCollectionData) { + + let updatedNode = {...node}; + if (updatedCollectionData) { + updatedNode = {...updatedNode, ...updatedCollectionData}; + } else { + updatedNode._expanded = true; + updatedNode.expandable = false; + } + nodes[nodeIndex] = updatedNode; + + + expansionResult.newNodes.forEach(newNode => { + if (!addedNodeIds.has(newNode.id)) { + nodes.push(newNode); + addedNodeIds.add(newNode.id); + if (newNode.expandable) { + nextLevelIds.add(newNode.id); + } + } + }); + expansionResult.newEdges.forEach(newEdge => { + if (!edges.some(e => e.id === newEdge.id)) { + edges.push(newEdge); + } + }); + } else { + nodes[nodeIndex] = {...node, expandable: false}; + } + }); + currentLevelIds = nextLevelIds; + } + + nodes.forEach((node, index) => { + if (!node._expanded) { + const checkAgain = createEvidenceNode(node._sourceData); + let stillExpandable = checkAgain ? checkAgain.expandable : false; + if (node.type === 'DatasetCollection') { + stillExpandable = node._remainingDatasets && node._remainingDatasets.length > 0; + } + if (nodes[index].expandable !== stillExpandable) { + nodes[index] = {...node, expandable: stillExpandable}; + } + } + }); + + + return { nodes, edges }; + } + + function expandNodeInternal(node, currentNodes, currentEdges, entityMap) { + const nodeId = node.id; + const nodeType = node.type; + const sourceData = node._sourceData; + const result = { newNodes: [], newEdges: [] }; + + const nodeExists = (id) => currentNodes.some(n => n.id === id) || result.newNodes.some(n => n.id === id); + const addNodeAndEdge = (targetEntityRef, relationshipLabel, relationshipType) => { + if (!targetEntityRef) return; + + let targetEntityData = null; + let targetId = null; + + if (typeof targetEntityRef === 'string') { + targetId = targetEntityRef; + targetEntityData = entityMap.get(targetId); + } else if (typeof targetEntityRef === 'object' && targetEntityRef['@id']) { + targetId = targetEntityRef['@id']; + targetEntityData = entityMap.has(targetId) ? { ...entityMap.get(targetId), ...targetEntityRef } : targetEntityRef; + } + + if (!targetId) { + console.warn(`Invalid target entity reference for ${relationshipType}:`, targetEntityRef); + return; + } + + if (!targetEntityData && entityMap.has(targetId)) { + targetEntityData = entityMap.get(targetId); + } + + if (!targetEntityData) { + console.warn(`Could not find entity data for ID: ${targetId} (relation: ${relationshipType} from ${nodeId})`); + return; + } + + const targetNode = createEvidenceNode(targetEntityData); + if (targetNode) { + if (!nodeExists(targetNode.id)) { + result.newNodes.push(targetNode); + } + const edgeId = `${nodeId}_${relationshipType}_${targetNode.id}`.replace(/[^a-zA-Z0-9_]/g, '-'); + const edgeExists = currentEdges.some(e => e.id === edgeId) || result.newEdges.some(e => e.id === edgeId); + if (!edgeExists) { + result.newEdges.push({ + id: edgeId, + source: nodeId, + target: targetNode.id, + label: relationshipLabel, + animated: false + }); + } + } + }; + + if (nodeType === "Dataset" && sourceData.generatedBy) { + addNodeAndEdge(sourceData.generatedBy, "generated by", "generatedBy"); + } + + if (nodeType === "DatasetGroup" && sourceData["evi:representativeDataset"]) { + // Show the representative member; downstream `generatedBy` expansion will + // continue the chain (representative.generatedBy → Experiment → Sample/Instrument). + // The other 858+ members are intentionally collapsed: they share the same + // provenance shape, that's why the builder grouped them in the first place. + addNodeAndEdge(sourceData["evi:representativeDataset"], "representative member", "representativeMember"); + } + + if ((nodeType === "Computation" || nodeType === "Experiment")) { + const relations = [ + { prop: 'usedSoftware', label: 'used software', type: 'uses_sw' }, + { prop: 'usedDataset', label: 'used dataset', type: 'uses_ds' }, + { prop: 'usedSample', label: 'used sample', type: 'uses_sample' }, + { prop: 'usedInstrument', label: 'used instrument', type: 'uses_instrument' } + ]; + + relations.forEach(rel => { + const items = sourceData[rel.prop]; + if (items) { + const itemList = Array.isArray(items) ? items : [items]; + + if (rel.prop === 'usedDataset' && itemList.length > 1) { + const validDatasetRefs = itemList.filter(ds => ds && (typeof ds === "string" || (typeof ds === "object" && ds["@id"]))); + if (validDatasetRefs.length > 1) { + const collectionNode = createDatasetCollectionNode(nodeId, validDatasetRefs); + if (collectionNode) { + if (!nodeExists(collectionNode.id)) { + result.newNodes.push(collectionNode); + } + const edgeId = `${nodeId}_${rel.type}_coll_${collectionNode.id}`.replace(/[^a-zA-Z0-9_]/g, '-'); + const edgeExists = currentEdges.some(e => e.id === edgeId) || result.newEdges.some(e => e.id === edgeId); + if (!edgeExists) { + result.newEdges.push({ id: edgeId, source: nodeId, target: collectionNode.id, label: rel.label, animated: false }); + } + } + return; + } + } + + itemList.forEach(itemRef => { + addNodeAndEdge(itemRef, rel.label, rel.type); + }); + } + }); + } + + return result; + } + + function expandDatasetCollectionNodeInternal(collectionNode, currentNodes, currentEdges, entityMap) { + const nodeId = collectionNode.id; + const result = { newNodes: [], newEdges: [], updatedCollectionData: null }; + const originalData = collectionNode; + const remaining = originalData._remainingDatasets; + + if (!remaining || remaining.length === 0) { + return { ...result, updatedCollectionData: { expandable: false, label: `Input Datasets (All Shown)`, displayName: `Datasets (All)` } }; + } + + const datasetToExpandRef = remaining[0]; + let datasetNode = null; + + const nodeExists = (id) => currentNodes.some(n => n.id === id) || result.newNodes.some(n => n.id === id); + + let datasetEntityData = null; + let datasetId = null; + if (typeof datasetToExpandRef === 'string') { + datasetId = datasetToExpandRef; + datasetEntityData = entityMap.get(datasetId); + } else if (typeof datasetToExpandRef === 'object' && datasetToExpandRef['@id']) { + datasetId = datasetToExpandRef['@id']; + datasetEntityData = entityMap.has(datasetId) ? { ...entityMap.get(datasetId), ...datasetToExpandRef } : datasetToExpandRef; + } + + if (datasetId && datasetEntityData) { + datasetNode = createEvidenceNode(datasetEntityData); + if (datasetNode) { + if (!nodeExists(datasetNode.id)) { + result.newNodes.push(datasetNode); + } + const edgeId = `${nodeId}_contains_${datasetNode.id}_${originalData._expandedCount || 0}`.replace(/[^a-zA-Z0-9_]/g, '-'); + const edgeExists = currentEdges.some(e => e.id === edgeId) || result.newEdges.some(e => e.id === edgeId); + if (!edgeExists) { + result.newEdges.push({ id: edgeId, source: nodeId, target: datasetNode.id, label: "contains", animated: false }); + } + } else { + console.warn("Failed to create node for dataset in collection:", datasetEntityData); + } + } else { + console.warn(`Could not find entity data for dataset ID in collection: ${datasetId || JSON.stringify(datasetToExpandRef)}`); + } + + const nextRemaining = remaining.slice(1); + const nextExpandedCount = (originalData._expandedCount || 0) + 1; + const nextExpandable = nextRemaining.length > 0; + const nextLabel = nextExpandable ? `Input Datasets (${nextRemaining.length} more)` : `Input Datasets (All Shown)`; + const nextDisplayName = nextExpandable ? `Datasets (${nextRemaining.length})` : `Datasets (All)`; + + result.updatedCollectionData = { + _remainingDatasets: nextRemaining, + _expandedCount: nextExpandedCount, + expandable: nextExpandable, + label: nextLabel, + displayName: nextDisplayName + }; + + return result; + } + + + function getLayoutedElements(nodes, edges, direction = "LR") { + if (!nodes || nodes.length === 0) { + console.log("Layout skipped: No nodes provided."); + return { nodes, edges, width: 0, height: 0 }; + } + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + + const NODE_SEP = 50; + const RANK_SEP = 80; + const MARGIN_X = 50; + const MARGIN_Y = 50; + + dagreGraph.setGraph({ + rankdir: direction, + nodesep: NODE_SEP, + ranksep: RANK_SEP, + marginx: MARGIN_X, + marginy: MARGIN_Y, + align: 'DL' + }); + + nodes.forEach(node => { + dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT, label: node.displayName }); + }); + + edges.forEach(edge => { + if (nodes.some(n => n.id === edge.source) && nodes.some(n => n.id === edge.target)) { + dagreGraph.setEdge(edge.source, edge.target); + } else { + console.warn("Skipping edge due to missing node:", edge.id, edge.source, edge.target); + } + }); + + try { + dagre.layout(dagreGraph); + } catch (e) { + console.error("Dagre layout calculation failed:", e); + const layoutedNodes = nodes.map(node => ({ ...node, x: node.x || Math.random()*500, y: node.y || Math.random()*500 })); + return { nodes: layoutedNodes, edges, width: 800, height: 600 }; + } + + let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; + + const layoutedNodes = nodes.map(node => { + const nodeWithPosition = dagreGraph.node(node.id); + if (nodeWithPosition) { + const x = nodeWithPosition.x - NODE_WIDTH / 2; + const y = nodeWithPosition.y - NODE_HEIGHT / 2; + + minX = Math.min(minX, x); + maxX = Math.max(maxX, x + NODE_WIDTH); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y + NODE_HEIGHT); + + return { ...node, x: x, y: y, width: NODE_WIDTH, height: NODE_HEIGHT }; + } else { + console.warn(`Node ${node.id} not found in Dagre layout results. Keeping previous position or placing randomly.`); + const x = node.x || Math.random() * 500; + const y = node.y || Math.random() * 500; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x + NODE_WIDTH); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y + NODE_HEIGHT); + return { ...node, x: x, y: y, width: NODE_WIDTH, height: NODE_HEIGHT }; + } + }); + + const graphWidth = Math.max(1500, maxX - minX + 2 * MARGIN_X); + const graphHeight = Math.max(1000, maxY - minY + 2 * MARGIN_Y); + + const offsetX = MARGIN_X - minX; + const offsetY = MARGIN_Y - minY; + const adjustedNodes = layoutedNodes.map(n => ({...n, x: n.x + offsetX, y: n.y + offsetY })); + + return { nodes: adjustedNodes, edges, width: graphWidth, height: graphHeight }; + } + + function getNodeColor(type) { + switch (type) { + case "Dataset": case "Sample": return "#8AE68A"; + case "Computation": case "Experiment": return "#FD9A9A"; + case "Software": case "Instrument": return "#FFC107"; + case "DatasetCollection": case "DatasetGroup": return "#B5DEFF"; + default: return "#E0E0E0"; + } + } + + const { createElement, useState, useCallback, useEffect, useRef, memo } = React; + + const EvidenceNode = memo(({ nodeData, onClick }) => { + const { id, type, label, displayName, description, expandable, x, y, width, height } = nodeData; + const nodeColor = getNodeColor(type); + const tooltip = [label, `Type: ${type}`, `ID: ${id}`, description].filter(Boolean).join('\n'); + + const handleClick = useCallback((event) => { + event.stopPropagation(); + if (expandable && onClick) { + onClick(id); + } + }, [id, expandable, onClick]); + + const style = { + left: `${x}px`, + top: `${y}px`, + width: `${width}px`, + height: `${height}px`, + }; + + const containerClasses = ['node-container']; + if (expandable) { + containerClasses.push('expandable'); + } + + return createElement( + 'div', + { style: style, className: 'node-wrapper', title: tooltip, onClick: handleClick, onMouseDown: (e) => e.stopPropagation() }, + createElement('div', { className: containerClasses.join(' ') }, [ + createElement('div', { key: 'header', className: 'node-header', style: { backgroundColor: nodeColor } }, type), + createElement('div', { key: 'content', className: 'node-content' }, + createElement('div', { style: { width: '100%', textAlign: 'center' } }, displayName) + ) + ]) + ); + }); + + const Edge = memo(({ edgeData, sourceNode, targetNode }) => { + if (!sourceNode || !targetNode || sourceNode.x === undefined || targetNode.x === undefined) { + return null; + } + + const sourceX = sourceNode.x + sourceNode.width; + const sourceY = sourceNode.y + sourceNode.height / 2; + const targetX = targetNode.x; + const targetY = targetNode.y + targetNode.height / 2; + + const dx = targetX - sourceX; + const dy = targetY - sourceY; + const dist = Math.sqrt(dx * dx + dy * dy); + + let pathD; + if (dist < 100) { + pathD = `M ${sourceX} ${sourceY} L ${targetX} ${targetY}`; + } else { + const midX = (sourceX + targetX) / 2; + const midY = (sourceY + targetY) / 2; + const c1X = sourceX + dx * 0.3; + const c1Y = sourceY; + const c2X = targetX - dx * 0.3; + const c2Y = targetY; + pathD = `M ${sourceX} ${sourceY} C ${c1X},${c1Y} ${c2X},${c2Y} ${targetX},${targetY}`; + } + + + const labelX = (sourceX + targetX) / 2; + const labelY = (sourceY + targetY) / 2; + + return createElement( + 'g', + { key: edgeData.id }, + [ + createElement('path', { + className: 'edge-path', + d: pathD, + markerEnd: 'url(#arrowhead)' + }), + createElement('rect', { + key: `${edgeData.id}-bg`, + className: 'edge-label-bg', + x: labelX - 25, + y: labelY - 8, + width: 50, + height: 16, + rx: 3, + }), + createElement('text', { + key: `${edgeData.id}-text`, + className: 'edge-label-text', + x: labelX, + y: labelY, + dy: "0.em" + }, edgeData.label || '') + ] + ); + }); + + + const GraphRenderer = ({ graphData }) => { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [graphWidth, setGraphWidth] = useState(1500); + const [graphHeight, setGraphHeight] = useState(1000); + const entityMapRef = useRef(new Map()); + const [scale, setScale] = useState(1); + const [translate, setTranslate] = useState({ x: 0, y: 0 }); + const [isDragging, setIsDragging] = useState(false); + const [startDragPos, setStartDragPos] = useState({ x: 0, y: 0 }); + const rootRef = useRef(null); + + + const applyLayout = useCallback((nodesToLayout, edgesToLayout) => { + setIsLoading(true); + setTimeout(() => { + try { + const nodesWithDims = nodesToLayout.map(n => ({ ...n, width: NODE_WIDTH, height: NODE_HEIGHT })); + const { nodes: layoutedNodes, edges: finalEdges, width, height } = getLayoutedElements(nodesWithDims, edgesToLayout, "LR"); + setNodes(layoutedNodes); + setEdges(finalEdges); + setGraphWidth(width); + setGraphHeight(height); + } catch (error) { + console.error("Layout application failed:", error); + setNodes(nodesToLayout); + setEdges(edgesToLayout); + } finally { + setIsLoading(false); + } + }, 10); + }, []); + + useEffect(() => { + if (!graphData) { + setIsLoading(false); + return; + } + const { rootEntityId, entityMap } = getGraphEntities(graphData); + if (!rootEntityId) { + console.error("No root entity found on initial load."); + setIsLoading(false); + return; + } + entityMapRef.current = entityMap; + + // Initial expansion depth is now handled by the default parameter in getInitialGraphState + const { nodes: initialNodes, edges: initialEdges } = getInitialGraphState(graphData); + + if (initialNodes.length > 0) { + applyLayout(initialNodes, initialEdges); + } else { + setNodes([]); + setEdges([]); + setIsLoading(false); + } + + }, [graphData, applyLayout]); + + + const handleNodeClick = useCallback((nodeId) => { + const clickedNodeIndex = nodes.findIndex(n => n.id === nodeId); + if (clickedNodeIndex === -1) return; + + const clickedNode = nodes[clickedNodeIndex]; + if (!clickedNode.expandable) return; + + setIsLoading(true); + + let expansionResult; + if (clickedNode.type === "DatasetCollection") { + expansionResult = expandDatasetCollectionNodeInternal(clickedNode, nodes, edges, entityMapRef.current); + } else { + expansionResult = expandNodeInternal(clickedNode, nodes, edges, entityMapRef.current); + } + + const { newNodes, newEdges, updatedCollectionData } = expansionResult; + + let nextNodes = [...nodes]; + let nodeUpdated = false; + + const originalNode = nextNodes[clickedNodeIndex]; + let updatedNodeData = { ...originalNode }; + + if (updatedCollectionData) { + updatedNodeData = { ...updatedNodeData, ...updatedCollectionData }; + nodeUpdated = true; + } else if (newNodes.length > 0 || newEdges.length > 0) { + updatedNodeData._expanded = true; + updatedNodeData.expandable = false; + nodeUpdated = true; + } else { + updatedNodeData.expandable = false; + nodeUpdated = true; + } + + if (nodeUpdated) { + nextNodes[clickedNodeIndex] = updatedNodeData; + } + + const nodesToAdd = newNodes.filter(newNode => !nextNodes.some(existingNode => existingNode.id === newNode.id)); + nextNodes = [...nextNodes, ...nodesToAdd]; + + const combinedEdges = [...edges]; + newEdges.forEach(newEdge => { + if (!combinedEdges.some(existingEdge => existingEdge.id === newEdge.id)) { + combinedEdges.push(newEdge); + } + }); + + applyLayout(nextNodes, combinedEdges); + + }, [nodes, edges, applyLayout]); + + + const nodeMap = new Map(nodes.map(node => [node.id, node])); + + const zoomIn = useCallback(() => setScale(s => Math.min(s * 1.2, 3)), []); + const zoomOut = useCallback(() => setScale(s => Math.max(s / 1.2, 0.2)), []); + const resetView = useCallback(() => { + setScale(1); + setTranslate({ x: 0, y: 0 }); + }, []); + + const handleMouseDown = useCallback((event) => { + if (event.button !== 0) return; + if (event.target.closest('.controls-container') || event.target.closest('.node-wrapper')) { + return; + } + setIsDragging(true); + setStartDragPos({ x: event.clientX - translate.x, y: event.clientY - translate.y }); + if(rootRef.current) rootRef.current.classList.add('dragging'); + event.preventDefault(); + }, [translate.x, translate.y]); + + const handleMouseMove = useCallback((event) => { + if (!isDragging) return; + const newX = event.clientX - startDragPos.x; + const newY = event.clientY - startDragPos.y; + setTranslate({ x: newX, y: newY }); + }, [isDragging, startDragPos]); + + const handleMouseUp = useCallback(() => { + if (!isDragging) return; + setIsDragging(false); + if(rootRef.current) rootRef.current.classList.remove('dragging'); + }, [isDragging]); + + const handleMouseLeave = useCallback(() => { + if (isDragging) { + setIsDragging(false); + if(rootRef.current) rootRef.current.classList.remove('dragging'); + } + }, [isDragging]); + + + useEffect(() => { + const currentRoot = rootRef.current; + if (currentRoot) { + currentRoot.addEventListener('mousedown', handleMouseDown); + currentRoot.addEventListener('mousemove', handleMouseMove); + currentRoot.addEventListener('mouseup', handleMouseUp); + currentRoot.addEventListener('mouseleave', handleMouseLeave); + + return () => { + currentRoot.removeEventListener('mousedown', handleMouseDown); + currentRoot.removeEventListener('mousemove', handleMouseMove); + currentRoot.removeEventListener('mouseup', handleMouseUp); + currentRoot.removeEventListener('mouseleave', handleMouseLeave); + }; + } + }, [handleMouseDown, handleMouseMove, handleMouseUp, handleMouseLeave]); + + + return React.createElement( + 'div', + { ref: rootRef, style:{ width: '100%', height: '100%', overflow: 'hidden', position: 'relative'} }, + [ + createElement('div', { + key: 'viewport', + className: 'graph-viewport', + style: { + transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})`, + } + }, + createElement( + 'div', + { className: 'graph-container', style: { width: `${graphWidth}px`, height: `${graphHeight}px` } }, + [ + createElement('svg', { key: 'svg-layer', className: 'svg-layer', width: graphWidth, height: graphHeight }, [ + createElement('defs', {key: 'defs'}, + createElement('marker', { + id: 'arrowhead', + viewBox: '0 -5 10 10', + refX: 8, + refY: 0, + markerWidth: 6, + markerHeight: 6, + orient: 'auto' + }, createElement('path', { d: 'M0,-5L10,0L0,5', fill: '#888' })) + ), + createElement('g', { key: 'edges-group' }, + edges.map(edge => createElement(Edge, { + key: edge.id, + edgeData: edge, + sourceNode: nodeMap.get(edge.source), + targetNode: nodeMap.get(edge.target) + })) + ) + ]), + + createElement('div', { key: 'nodes-layer', style: { position: 'relative', width: '100%', height: '100%' } }, + nodes.map(node => createElement(EvidenceNode, { + key: node.id, + nodeData: node, + onClick: handleNodeClick + })) + ) + ] + ) + ), + isLoading && createElement('div', { key: 'loading', className: 'loading-overlay' }, 'Loading...'), + + createElement('div', {key: 'controls', className: 'controls-container'}, [ + createElement('button', { key: 'zoom-in', className: 'control-button', onClick: zoomIn }, '+'), + createElement('button', { key: 'zoom-out', className: 'control-button', onClick: zoomOut }, '-'), + createElement('button', { key: 'reset-view', className: 'control-button', onClick: resetView }, 'Reset') + ]) + ] + ); + }; + + const App = () => { + return React.createElement(GraphRenderer, { graphData: evidenceGraphData }); + }; + + const root = ReactDOM.createRoot(document.getElementById('root')); + root.render(React.createElement(App)); + diff --git a/src/fairscape_cli/datasheet_builder/templates/evidence_graph/vendor/README.md b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/vendor/README.md new file mode 100644 index 0000000..ba3dfb2 --- /dev/null +++ b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/vendor/README.md @@ -0,0 +1,13 @@ +# Vendored JS libraries for the evidence graph visualization + +These files are inlined into the generated evidence-graph HTML so the output is +a fully self-contained, offline-shareable single file (no CDN dependency). + +| File | Package | Version | Source | License | +|------|---------|---------|--------|---------| +| `react.production.min.js` | react | 18.3.1 | https://unpkg.com/react@18.3.1/umd/react.production.min.js | MIT | +| `react-dom.production.min.js` | react-dom | 18.3.1 | https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js | MIT | +| `dagre.min.js` | @dagrejs/dagre | 1.0.2 | https://cdn.jsdelivr.net/npm/@dagrejs/dagre@1.0.2/dist/dagre.min.js | MIT | + +To update, download the new UMD build from the URL above (bump the version) and +update this table. License headers are kept in the files themselves. diff --git a/src/fairscape_cli/datasheet_builder/templates/evidence_graph/vendor/dagre.min.js b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/vendor/dagre.min.js new file mode 100644 index 0000000..f7a7949 --- /dev/null +++ b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/vendor/dagre.min.js @@ -0,0 +1,801 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.dagre=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;iswapWidthHeightOne(g.node(v)));g.edges().forEach(e=>swapWidthHeightOne(g.edge(e)))}function swapWidthHeightOne(attrs){var w=attrs.width;attrs.width=attrs.height;attrs.height=w}function reverseY(g){g.nodes().forEach(v=>reverseYOne(g.node(v)));g.edges().forEach(function(e){var edge=g.edge(e);edge.points.forEach(reverseYOne);if(edge.hasOwnProperty("y")){reverseYOne(edge)}})}function reverseYOne(attrs){attrs.y=-attrs.y}function swapXY(g){g.nodes().forEach(v=>swapXYOne(g.node(v)));g.edges().forEach(function(e){var edge=g.edge(e);edge.points.forEach(swapXYOne);if(edge.hasOwnProperty("x")){swapXYOne(edge)}})}function swapXYOne(attrs){var x=attrs.x;attrs.x=attrs.y;attrs.y=x}},{}],5:[function(require,module,exports){ +/* + * Simple doubly linked list implementation derived from Cormen, et al., + * "Introduction to Algorithms". + */ +module.exports=List;function List(){var sentinel={};sentinel._next=sentinel._prev=sentinel;this._sentinel=sentinel}List.prototype.dequeue=function(){var sentinel=this._sentinel;var entry=sentinel._prev;if(entry!==sentinel){unlink(entry);return entry}};List.prototype.enqueue=function(entry){var sentinel=this._sentinel;if(entry._prev&&entry._next){unlink(entry)}entry._next=sentinel._next;sentinel._next._prev=entry;sentinel._next=entry;entry._prev=sentinel};List.prototype.toString=function(){var strs=[];var sentinel=this._sentinel;var curr=sentinel._prev;while(curr!==sentinel){strs.push(JSON.stringify(curr,filterOutLinks));curr=curr._prev}return"["+strs.join(", ")+"]"};function unlink(entry){entry._prev._next=entry._next;entry._next._prev=entry._prev;delete entry._next;delete entry._prev}function filterOutLinks(k,v){if(k!=="_next"&&k!=="_prev"){return v}}},{}],6:[function(require,module,exports){var util=require("./util");var Graph=require("@dagrejs/graphlib").Graph;module.exports={debugOrdering:debugOrdering}; +/* istanbul ignore next */function debugOrdering(g){var layerMatrix=util.buildLayerMatrix(g);var h=new Graph({compound:true,multigraph:true}).setGraph({});g.nodes().forEach(function(v){h.setNode(v,{label:v});h.setParent(v,"layer"+g.node(v).rank)});g.edges().forEach(function(e){h.setEdge(e.v,e.w,{},e.name)});layerMatrix.forEach(function(layer,i){var layerV="layer"+i;h.setNode(layerV,{rank:"same"});layer.reduce(function(u,v){h.setEdge(u,v,{style:"invis"});return v})});return h}},{"./util":27,"@dagrejs/graphlib":29}],7:[function(require,module,exports){var Graph=require("@dagrejs/graphlib").Graph;var List=require("./data/list"); +/* + * A greedy heuristic for finding a feedback arc set for a graph. A feedback + * arc set is a set of edges that can be removed to make a graph acyclic. + * The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, "A fast and + * effective heuristic for the feedback arc set problem." This implementation + * adjusts that from the paper to allow for weighted edges. + */module.exports=greedyFAS;var DEFAULT_WEIGHT_FN=()=>1;function greedyFAS(g,weightFn){if(g.nodeCount()<=1){return[]}var state=buildState(g,weightFn||DEFAULT_WEIGHT_FN);var results=doGreedyFAS(state.graph,state.buckets,state.zeroIdx); +// Expand multi-edges +return results.flatMap(e=>g.outEdges(e.v,e.w))}function doGreedyFAS(g,buckets,zeroIdx){var results=[];var sources=buckets[buckets.length-1];var sinks=buckets[0];var entry;while(g.nodeCount()){while(entry=sinks.dequeue()){removeNode(g,buckets,zeroIdx,entry)}while(entry=sources.dequeue()){removeNode(g,buckets,zeroIdx,entry)}if(g.nodeCount()){for(var i=buckets.length-2;i>0;--i){entry=buckets[i].dequeue();if(entry){results=results.concat(removeNode(g,buckets,zeroIdx,entry,true));break}}}}return results}function removeNode(g,buckets,zeroIdx,entry,collectPredecessors){var results=collectPredecessors?[]:undefined;g.inEdges(entry.v).forEach(function(edge){var weight=g.edge(edge);var uEntry=g.node(edge.v);if(collectPredecessors){results.push({v:edge.v,w:edge.w})}uEntry.out-=weight;assignBucket(buckets,zeroIdx,uEntry)});g.outEdges(entry.v).forEach(function(edge){var weight=g.edge(edge);var w=edge.w;var wEntry=g.node(w);wEntry["in"]-=weight;assignBucket(buckets,zeroIdx,wEntry)});g.removeNode(entry.v);return results}function buildState(g,weightFn){var fasGraph=new Graph;var maxIn=0;var maxOut=0;g.nodes().forEach(function(v){fasGraph.setNode(v,{v:v,in:0,out:0})}); +// Aggregate weights on nodes, but also sum the weights across multi-edges +// into a single edge for the fasGraph. +g.edges().forEach(function(e){var prevWeight=fasGraph.edge(e.v,e.w)||0;var weight=weightFn(e);var edgeWeight=prevWeight+weight;fasGraph.setEdge(e.v,e.w,edgeWeight);maxOut=Math.max(maxOut,fasGraph.node(e.v).out+=weight);maxIn=Math.max(maxIn,fasGraph.node(e.w)["in"]+=weight)});var buckets=range(maxOut+maxIn+3).map(()=>new List);var zeroIdx=maxIn+1;fasGraph.nodes().forEach(function(v){assignBucket(buckets,zeroIdx,fasGraph.node(v))});return{graph:fasGraph,buckets:buckets,zeroIdx:zeroIdx}}function assignBucket(buckets,zeroIdx,entry){if(!entry.out){buckets[0].enqueue(entry)}else if(!entry["in"]){buckets[buckets.length-1].enqueue(entry)}else{buckets[entry.out-entry["in"]+zeroIdx].enqueue(entry)}}function range(limit){const range=[];for(let i=0;i{var inputLabel=inputGraph.node(v);var layoutLabel=layoutGraph.node(v);if(inputLabel){inputLabel.x=layoutLabel.x;inputLabel.y=layoutLabel.y;inputLabel.rank=layoutLabel.rank;if(layoutGraph.children(v).length){inputLabel.width=layoutLabel.width;inputLabel.height=layoutLabel.height}}});inputGraph.edges().forEach(e=>{var inputLabel=inputGraph.edge(e);var layoutLabel=layoutGraph.edge(e);inputLabel.points=layoutLabel.points;if(layoutLabel.hasOwnProperty("x")){inputLabel.x=layoutLabel.x;inputLabel.y=layoutLabel.y}});inputGraph.graph().width=layoutGraph.graph().width;inputGraph.graph().height=layoutGraph.graph().height}var graphNumAttrs=["nodesep","edgesep","ranksep","marginx","marginy"];var graphDefaults={ranksep:50,edgesep:20,nodesep:50,rankdir:"tb"};var graphAttrs=["acyclicer","ranker","rankdir","align"];var nodeNumAttrs=["width","height"];var nodeDefaults={width:0,height:0};var edgeNumAttrs=["minlen","weight","width","height","labeloffset"];var edgeDefaults={minlen:1,weight:1,width:0,height:0,labeloffset:10,labelpos:"r"};var edgeAttrs=["labelpos"]; +/* + * Constructs a new graph from the input graph, which can be used for layout. + * This process copies only whitelisted attributes from the input graph to the + * layout graph. Thus this function serves as a good place to determine what + * attributes can influence layout. + */function buildLayoutGraph(inputGraph){var g=new Graph({multigraph:true,compound:true});var graph=canonicalize(inputGraph.graph());g.setGraph(Object.assign({},graphDefaults,selectNumberAttrs(graph,graphNumAttrs),util.pick(graph,graphAttrs)));inputGraph.nodes().forEach(v=>{var node=canonicalize(inputGraph.node(v));const newNode=selectNumberAttrs(node,nodeNumAttrs);Object.keys(nodeDefaults).forEach(k=>{if(newNode[k]===undefined){newNode[k]=nodeDefaults[k]}});g.setNode(v,newNode);g.setParent(v,inputGraph.parent(v))});inputGraph.edges().forEach(e=>{var edge=canonicalize(inputGraph.edge(e));g.setEdge(e,Object.assign({},edgeDefaults,selectNumberAttrs(edge,edgeNumAttrs),util.pick(edge,edgeAttrs)))});return g} +/* + * This idea comes from the Gansner paper: to account for edge labels in our + * layout we split each rank in half by doubling minlen and halving ranksep. + * Then we can place labels at these mid-points between nodes. + * + * We also add some minimal padding to the width to push the label for the edge + * away from the edge itself a bit. + */function makeSpaceForEdgeLabels(g){var graph=g.graph();graph.ranksep/=2;g.edges().forEach(e=>{var edge=g.edge(e);edge.minlen*=2;if(edge.labelpos.toLowerCase()!=="c"){if(graph.rankdir==="TB"||graph.rankdir==="BT"){edge.width+=edge.labeloffset}else{edge.height+=edge.labeloffset}}})} +/* + * Creates temporary dummy nodes that capture the rank in which each edge's + * label is going to, if it has one of non-zero width and height. We do this + * so that we can safely remove empty ranks while preserving balance for the + * label's position. + */function injectEdgeLabelProxies(g){g.edges().forEach(e=>{var edge=g.edge(e);if(edge.width&&edge.height){var v=g.node(e.v);var w=g.node(e.w);var label={rank:(w.rank-v.rank)/2+v.rank,e:e};util.addDummyNode(g,"edge-proxy",label,"_ep")}})}function assignRankMinMax(g){var maxRank=0;g.nodes().forEach(v=>{var node=g.node(v);if(node.borderTop){node.minRank=g.node(node.borderTop).rank;node.maxRank=g.node(node.borderBottom).rank;maxRank=Math.max(maxRank,node.maxRank)}});g.graph().maxRank=maxRank}function removeEdgeLabelProxies(g){g.nodes().forEach(v=>{var node=g.node(v);if(node.dummy==="edge-proxy"){g.edge(node.e).labelRank=node.rank;g.removeNode(v)}})}function translateGraph(g){var minX=Number.POSITIVE_INFINITY;var maxX=0;var minY=Number.POSITIVE_INFINITY;var maxY=0;var graphLabel=g.graph();var marginX=graphLabel.marginx||0;var marginY=graphLabel.marginy||0;function getExtremes(attrs){var x=attrs.x;var y=attrs.y;var w=attrs.width;var h=attrs.height;minX=Math.min(minX,x-w/2);maxX=Math.max(maxX,x+w/2);minY=Math.min(minY,y-h/2);maxY=Math.max(maxY,y+h/2)}g.nodes().forEach(v=>getExtremes(g.node(v)));g.edges().forEach(e=>{var edge=g.edge(e);if(edge.hasOwnProperty("x")){getExtremes(edge)}});minX-=marginX;minY-=marginY;g.nodes().forEach(v=>{var node=g.node(v);node.x-=minX;node.y-=minY});g.edges().forEach(e=>{var edge=g.edge(e);edge.points.forEach(p=>{p.x-=minX;p.y-=minY});if(edge.hasOwnProperty("x")){edge.x-=minX}if(edge.hasOwnProperty("y")){edge.y-=minY}});graphLabel.width=maxX-minX+marginX;graphLabel.height=maxY-minY+marginY}function assignNodeIntersects(g){g.edges().forEach(e=>{var edge=g.edge(e);var nodeV=g.node(e.v);var nodeW=g.node(e.w);var p1,p2;if(!edge.points){edge.points=[];p1=nodeW;p2=nodeV}else{p1=edge.points[0];p2=edge.points[edge.points.length-1]}edge.points.unshift(util.intersectRect(nodeV,p1));edge.points.push(util.intersectRect(nodeW,p2))})}function fixupEdgeLabelCoords(g){g.edges().forEach(e=>{var edge=g.edge(e);if(edge.hasOwnProperty("x")){if(edge.labelpos==="l"||edge.labelpos==="r"){edge.width-=edge.labeloffset}switch(edge.labelpos){case"l":edge.x-=edge.width/2+edge.labeloffset;break;case"r":edge.x+=edge.width/2+edge.labeloffset;break}}})}function reversePointsForReversedEdges(g){g.edges().forEach(e=>{var edge=g.edge(e);if(edge.reversed){edge.points.reverse()}})}function removeBorderNodes(g){g.nodes().forEach(v=>{if(g.children(v).length){var node=g.node(v);var t=g.node(node.borderTop);var b=g.node(node.borderBottom);var l=g.node(node.borderLeft[node.borderLeft.length-1]);var r=g.node(node.borderRight[node.borderRight.length-1]);node.width=Math.abs(r.x-l.x);node.height=Math.abs(b.y-t.y);node.x=l.x+node.width/2;node.y=t.y+node.height/2}});g.nodes().forEach(v=>{if(g.node(v).dummy==="border"){g.removeNode(v)}})}function removeSelfEdges(g){g.edges().forEach(e=>{if(e.v===e.w){var node=g.node(e.v);if(!node.selfEdges){node.selfEdges=[]}node.selfEdges.push({e:e,label:g.edge(e)});g.removeEdge(e)}})}function insertSelfEdges(g){var layers=util.buildLayerMatrix(g);layers.forEach(layer=>{var orderShift=0;layer.forEach((v,i)=>{var node=g.node(v);node.order=i+orderShift;(node.selfEdges||[]).forEach(selfEdge=>{util.addDummyNode(g,"selfedge",{width:selfEdge.label.width,height:selfEdge.label.height,rank:node.rank,order:i+ ++orderShift,e:selfEdge.e,label:selfEdge.label},"_se")});delete node.selfEdges})})}function positionSelfEdges(g){g.nodes().forEach(v=>{var node=g.node(v);if(node.dummy==="selfedge"){var selfNode=g.node(node.e.v);var x=selfNode.x+selfNode.width/2;var y=selfNode.y;var dx=node.x-x;var dy=selfNode.height/2;g.setEdge(node.e,node.label);g.removeNode(v);node.label.points=[{x:x+2*dx/3,y:y-dy},{x:x+5*dx/6,y:y-dy},{x:x+dx,y:y},{x:x+5*dx/6,y:y+dy},{x:x+2*dx/3,y:y+dy}];node.label.x=node.x;node.label.y=node.y}})}function selectNumberAttrs(obj,attrs){return util.mapValues(util.pick(obj,attrs),Number)}function canonicalize(attrs){var newAttrs={};if(attrs){Object.entries(attrs).forEach(([k,v])=>{if(typeof k==="string"){k=k.toLowerCase()}newAttrs[k]=v})}return newAttrs}},{"./acyclic":2,"./add-border-segments":3,"./coordinate-system":4,"./nesting-graph":9,"./normalize":10,"./order":15,"./parent-dummy-chains":20,"./position":22,"./rank":24,"./util":27,"@dagrejs/graphlib":29}],9:[function(require,module,exports){var util=require("./util");module.exports={run:run,cleanup:cleanup}; +/* + * A nesting graph creates dummy nodes for the tops and bottoms of subgraphs, + * adds appropriate edges to ensure that all cluster nodes are placed between + * these boundries, and ensures that the graph is connected. + * + * In addition we ensure, through the use of the minlen property, that nodes + * and subgraph border nodes to not end up on the same rank. + * + * Preconditions: + * + * 1. Input graph is a DAG + * 2. Nodes in the input graph has a minlen attribute + * + * Postconditions: + * + * 1. Input graph is connected. + * 2. Dummy nodes are added for the tops and bottoms of subgraphs. + * 3. The minlen attribute for nodes is adjusted to ensure nodes do not + * get placed on the same rank as subgraph border nodes. + * + * The nesting graph idea comes from Sander, "Layout of Compound Directed + * Graphs." + */function run(g){var root=util.addDummyNode(g,"root",{},"_root");var depths=treeDepths(g);var height=Math.max(...Object.values(depths))-1;// Note: depths is an Object not an array +var nodeSep=2*height+1;g.graph().nestingRoot=root; +// Multiply minlen by nodeSep to align nodes on non-border ranks. +g.edges().forEach(e=>g.edge(e).minlen*=nodeSep); +// Calculate a weight that is sufficient to keep subgraphs vertically compact +var weight=sumWeights(g)+1; +// Create border nodes and link them up +g.children().forEach(function(child){dfs(g,root,nodeSep,weight,height,depths,child)}); +// Save the multiplier for node layers for later removal of empty border +// layers. +g.graph().nodeRankFactor=nodeSep}function dfs(g,root,nodeSep,weight,height,depths,v){var children=g.children(v);if(!children.length){if(v!==root){g.setEdge(root,v,{weight:0,minlen:nodeSep})}return}var top=util.addBorderNode(g,"_bt");var bottom=util.addBorderNode(g,"_bb");var label=g.node(v);g.setParent(top,v);label.borderTop=top;g.setParent(bottom,v);label.borderBottom=bottom;children.forEach(function(child){dfs(g,root,nodeSep,weight,height,depths,child);var childNode=g.node(child);var childTop=childNode.borderTop?childNode.borderTop:child;var childBottom=childNode.borderBottom?childNode.borderBottom:child;var thisWeight=childNode.borderTop?weight:2*weight;var minlen=childTop!==childBottom?1:height-depths[v]+1;g.setEdge(top,childTop,{weight:thisWeight,minlen:minlen,nestingEdge:true});g.setEdge(childBottom,bottom,{weight:thisWeight,minlen:minlen,nestingEdge:true})});if(!g.parent(v)){g.setEdge(root,top,{weight:0,minlen:height+depths[v]})}}function treeDepths(g){var depths={};function dfs(v,depth){var children=g.children(v);if(children&&children.length){children.forEach(child=>dfs(child,depth+1))}depths[v]=depth}g.children().forEach(v=>dfs(v,1));return depths}function sumWeights(g){return g.edges().reduce((acc,e)=>acc+g.edge(e).weight,0)}function cleanup(g){var graphLabel=g.graph();g.removeNode(graphLabel.nestingRoot);delete graphLabel.nestingRoot;g.edges().forEach(e=>{var edge=g.edge(e);if(edge.nestingEdge){g.removeEdge(e)}})}},{"./util":27}],10:[function(require,module,exports){"use strict";var util=require("./util");module.exports={run:run,undo:undo}; +/* + * Breaks any long edges in the graph into short segments that span 1 layer + * each. This operation is undoable with the denormalize function. + * + * Pre-conditions: + * + * 1. The input graph is a DAG. + * 2. Each node in the graph has a "rank" property. + * + * Post-condition: + * + * 1. All edges in the graph have a length of 1. + * 2. Dummy nodes are added where edges have been split into segments. + * 3. The graph is augmented with a "dummyChains" attribute which contains + * the first dummy in each chain of dummy nodes produced. + */function run(g){g.graph().dummyChains=[];g.edges().forEach(edge=>normalizeEdge(g,edge))}function normalizeEdge(g,e){var v=e.v;var vRank=g.node(v).rank;var w=e.w;var wRank=g.node(w).rank;var name=e.name;var edgeLabel=g.edge(e);var labelRank=edgeLabel.labelRank;if(wRank===vRank+1)return;g.removeEdge(e);var dummy,attrs,i;for(i=0,++vRank;vRank{var inV=g.inEdges(v);if(!inV.length){return{v:v}}else{var result=inV.reduce((acc,e)=>{var edge=g.edge(e),nodeU=g.node(e.v);return{sum:acc.sum+edge.weight*nodeU.order,weight:acc.weight+edge.weight}},{sum:0,weight:0});return{v:v,barycenter:result.sum/result.weight,weight:result.weight}}})}},{}],13:[function(require,module,exports){var Graph=require("@dagrejs/graphlib").Graph;var util=require("../util");module.exports=buildLayerGraph; +/* + * Constructs a graph that can be used to sort a layer of nodes. The graph will + * contain all base and subgraph nodes from the request layer in their original + * hierarchy and any edges that are incident on these nodes and are of the type + * requested by the "relationship" parameter. + * + * Nodes from the requested rank that do not have parents are assigned a root + * node in the output graph, which is set in the root graph attribute. This + * makes it easy to walk the hierarchy of movable nodes during ordering. + * + * Pre-conditions: + * + * 1. Input graph is a DAG + * 2. Base nodes in the input graph have a rank attribute + * 3. Subgraph nodes in the input graph has minRank and maxRank attributes + * 4. Edges have an assigned weight + * + * Post-conditions: + * + * 1. Output graph has all nodes in the movable rank with preserved + * hierarchy. + * 2. Root nodes in the movable layer are made children of the node + * indicated by the root attribute of the graph. + * 3. Non-movable nodes incident on movable nodes, selected by the + * relationship parameter, are included in the graph (without hierarchy). + * 4. Edges incident on movable nodes, selected by the relationship + * parameter, are added to the output graph. + * 5. The weights for copied edges are aggregated as need, since the output + * graph is not a multi-graph. + */function buildLayerGraph(g,rank,relationship){var root=createRootNode(g),result=new Graph({compound:true}).setGraph({root:root}).setDefaultNodeLabel(function(v){return g.node(v)});g.nodes().forEach(function(v){var node=g.node(v),parent=g.parent(v);if(node.rank===rank||node.minRank<=rank&&rank<=node.maxRank){result.setNode(v);result.setParent(v,parent||root); +// This assumes we have only short edges! +g[relationship](v).forEach(function(e){var u=e.v===v?e.w:e.v,edge=result.edge(u,v),weight=edge!==undefined?edge.weight:0;result.setEdge(u,v,{weight:g.edge(e).weight+weight})});if(node.hasOwnProperty("minRank")){result.setNode(v,{borderLeft:node.borderLeft[rank],borderRight:node.borderRight[rank]})}}});return result}function createRootNode(g){var v;while(g.hasNode(v=util.uniqueId("_root")));return v}},{"../util":27,"@dagrejs/graphlib":29}],14:[function(require,module,exports){"use strict";var zipObject=require("../util").zipObject;module.exports=crossCount; +/* + * A function that takes a layering (an array of layers, each with an array of + * ordererd nodes) and a graph and returns a weighted crossing count. + * + * Pre-conditions: + * + * 1. Input graph must be simple (not a multigraph), directed, and include + * only simple edges. + * 2. Edges in the input graph must have assigned weights. + * + * Post-conditions: + * + * 1. The graph and layering matrix are left unchanged. + * + * This algorithm is derived from Barth, et al., "Bilayer Cross Counting." + */function crossCount(g,layering){var cc=0;for(var i=1;ii));var southEntries=northLayer.flatMap(v=>{return g.outEdges(v).map(e=>{return{pos:southPos[e.w],weight:g.edge(e).weight}}).sort((a,b)=>a.pos-b.pos)}); +// Build the accumulator tree +var firstIndex=1;while(firstIndex{var index=entry.pos+firstIndex;tree[index]+=entry.weight;var weightSum=0;while(index>0){if(index%2){weightSum+=tree[index+1]}index=index-1>>1;tree[index]+=entry.weight}cc+=entry.weight*weightSum});return cc}},{"../util":27}],15:[function(require,module,exports){"use strict";var initOrder=require("./init-order");var crossCount=require("./cross-count");var sortSubgraph=require("./sort-subgraph");var buildLayerGraph=require("./build-layer-graph");var addSubgraphConstraints=require("./add-subgraph-constraints");var Graph=require("@dagrejs/graphlib").Graph;var util=require("../util");module.exports=order; +/* + * Applies heuristics to minimize edge crossings in the graph and sets the best + * order solution as an order attribute on each node. + * + * Pre-conditions: + * + * 1. Graph must be DAG + * 2. Graph nodes must be objects with a "rank" attribute + * 3. Graph edges must have the "weight" attribute + * + * Post-conditions: + * + * 1. Graph nodes will have an "order" attribute based on the results of the + * algorithm. + */function order(g){var maxRank=util.maxRank(g),downLayerGraphs=buildLayerGraphs(g,util.range(1,maxRank+1),"inEdges"),upLayerGraphs=buildLayerGraphs(g,util.range(maxRank-1,-1,-1),"outEdges");var layering=initOrder(g);assignOrder(g,layering);var bestCC=Number.POSITIVE_INFINITY,best;for(var i=0,lastBest=0;lastBest<4;++i,++lastBest){sweepLayerGraphs(i%2?downLayerGraphs:upLayerGraphs,i%4>=2);layering=util.buildLayerMatrix(g);var cc=crossCount(g,layering);if(cclg.node(v).order=i);addSubgraphConstraints(lg,cg,sorted.vs)})}function assignOrder(g,layering){Object.values(layering).forEach(layer=>layer.forEach((v,i)=>g.node(v).order=i))}},{"../util":27,"./add-subgraph-constraints":11,"./build-layer-graph":13,"./cross-count":14,"./init-order":16,"./sort-subgraph":18,"@dagrejs/graphlib":29}],16:[function(require,module,exports){"use strict";var util=require("../util");module.exports=initOrder; +/* + * Assigns an initial order value for each node by performing a DFS search + * starting from nodes in the first rank. Nodes are assigned an order in their + * rank as they are first visited. + * + * This approach comes from Gansner, et al., "A Technique for Drawing Directed + * Graphs." + * + * Returns a layering matrix with an array per layer and each layer sorted by + * the order of its nodes. + */function initOrder(g){var visited={};var simpleNodes=g.nodes().filter(v=>!g.children(v).length);var maxRank=Math.max(...simpleNodes.map(v=>g.node(v).rank));var layers=util.range(maxRank+1).map(()=>[]);function dfs(v){if(visited[v])return;visited[v]=true;var node=g.node(v);layers[node.rank].push(v);g.successors(v).forEach(dfs)}var orderedVs=simpleNodes.sort((a,b)=>g.node(a).rank-g.node(b).rank);orderedVs.forEach(dfs);return layers}},{"../util":27}],17:[function(require,module,exports){"use strict";var util=require("../util");module.exports=resolveConflicts; +/* + * Given a list of entries of the form {v, barycenter, weight} and a + * constraint graph this function will resolve any conflicts between the + * constraint graph and the barycenters for the entries. If the barycenters for + * an entry would violate a constraint in the constraint graph then we coalesce + * the nodes in the conflict into a new node that respects the contraint and + * aggregates barycenter and weight information. + * + * This implementation is based on the description in Forster, "A Fast and + * Simple Hueristic for Constrained Two-Level Crossing Reduction," thought it + * differs in some specific details. + * + * Pre-conditions: + * + * 1. Each entry has the form {v, barycenter, weight}, or if the node has + * no barycenter, then {v}. + * + * Returns: + * + * A new list of entries of the form {vs, i, barycenter, weight}. The list + * `vs` may either be a singleton or it may be an aggregation of nodes + * ordered such that they do not violate constraints from the constraint + * graph. The property `i` is the lowest original index of any of the + * elements in `vs`. + */function resolveConflicts(entries,cg){var mappedEntries={};entries.forEach((entry,i)=>{var tmp=mappedEntries[entry.v]={indegree:0,in:[],out:[],vs:[entry.v],i:i};if(entry.barycenter!==undefined){tmp.barycenter=entry.barycenter;tmp.weight=entry.weight}});cg.edges().forEach(e=>{var entryV=mappedEntries[e.v];var entryW=mappedEntries[e.w];if(entryV!==undefined&&entryW!==undefined){entryW.indegree++;entryV.out.push(mappedEntries[e.w])}});var sourceSet=Object.values(mappedEntries).filter(entry=>!entry.indegree);return doResolveConflicts(sourceSet)}function doResolveConflicts(sourceSet){var entries=[];function handleIn(vEntry){return function(uEntry){if(uEntry.merged){return}if(uEntry.barycenter===undefined||vEntry.barycenter===undefined||uEntry.barycenter>=vEntry.barycenter){mergeEntries(vEntry,uEntry)}}}function handleOut(vEntry){return function(wEntry){wEntry["in"].push(vEntry);if(--wEntry.indegree===0){sourceSet.push(wEntry)}}}while(sourceSet.length){var entry=sourceSet.pop();entries.push(entry);entry["in"].reverse().forEach(handleIn(entry));entry.out.forEach(handleOut(entry))}return entries.filter(entry=>!entry.merged).map(entry=>{return util.pick(entry,["vs","i","barycenter","weight"])})}function mergeEntries(target,source){var sum=0;var weight=0;if(target.weight){sum+=target.barycenter*target.weight;weight+=target.weight}if(source.weight){sum+=source.barycenter*source.weight;weight+=source.weight}target.vs=source.vs.concat(target.vs);target.barycenter=sum/weight;target.weight=weight;target.i=Math.min(source.i,target.i);source.merged=true}},{"../util":27}],18:[function(require,module,exports){var barycenter=require("./barycenter");var resolveConflicts=require("./resolve-conflicts");var sort=require("./sort");module.exports=sortSubgraph;function sortSubgraph(g,v,cg,biasRight){var movable=g.children(v);var node=g.node(v);var bl=node?node.borderLeft:undefined;var br=node?node.borderRight:undefined;var subgraphs={};if(bl){movable=movable.filter(w=>w!==bl&&w!==br)}var barycenters=barycenter(g,movable);barycenters.forEach(function(entry){if(g.children(entry.v).length){var subgraphResult=sortSubgraph(g,entry.v,cg,biasRight);subgraphs[entry.v]=subgraphResult;if(subgraphResult.hasOwnProperty("barycenter")){mergeBarycenters(entry,subgraphResult)}}});var entries=resolveConflicts(barycenters,cg);expandSubgraphs(entries,subgraphs);var result=sort(entries,biasRight);if(bl){result.vs=[bl,result.vs,br].flat(true);if(g.predecessors(bl).length){var blPred=g.node(g.predecessors(bl)[0]),brPred=g.node(g.predecessors(br)[0]);if(!result.hasOwnProperty("barycenter")){result.barycenter=0;result.weight=0}result.barycenter=(result.barycenter*result.weight+blPred.order+brPred.order)/(result.weight+2);result.weight+=2}}return result}function expandSubgraphs(entries,subgraphs){entries.forEach(function(entry){entry.vs=entry.vs.flatMap(function(v){if(subgraphs[v]){return subgraphs[v].vs}return v})})}function mergeBarycenters(target,other){if(target.barycenter!==undefined){target.barycenter=(target.barycenter*target.weight+other.barycenter*other.weight)/(target.weight+other.weight);target.weight+=other.weight}else{target.barycenter=other.barycenter;target.weight=other.weight}}},{"./barycenter":12,"./resolve-conflicts":17,"./sort":19}],19:[function(require,module,exports){var util=require("../util");module.exports=sort;function sort(entries,biasRight){var parts=util.partition(entries,function(entry){return entry.hasOwnProperty("barycenter")});var sortable=parts.lhs,unsortable=parts.rhs.sort((a,b)=>b.i-a.i),vs=[],sum=0,weight=0,vsIndex=0;sortable.sort(compareWithBias(!!biasRight));vsIndex=consumeUnsortable(vs,unsortable,vsIndex);sortable.forEach(function(entry){vsIndex+=entry.vs.length;vs.push(entry.vs);sum+=entry.barycenter*entry.weight;weight+=entry.weight;vsIndex=consumeUnsortable(vs,unsortable,vsIndex)});var result={vs:vs.flat(true)};if(weight){result.barycenter=sum/weight;result.weight=weight}return result}function consumeUnsortable(vs,unsortable,index){var last;while(unsortable.length&&(last=unsortable[unsortable.length-1]).i<=index){unsortable.pop();vs.push(last.vs);index++}return index}function compareWithBias(bias){return function(entryV,entryW){if(entryV.barycenterentryW.barycenter){return 1}return!bias?entryV.i-entryW.i:entryW.i-entryV.i}}},{"../util":27}],20:[function(require,module,exports){module.exports=parentDummyChains;function parentDummyChains(g){var postorderNums=postorder(g);g.graph().dummyChains.forEach(function(v){var node=g.node(v);var edgeObj=node.edgeObj;var pathData=findPath(g,postorderNums,edgeObj.v,edgeObj.w);var path=pathData.path;var lca=pathData.lca;var pathIdx=0;var pathV=path[pathIdx];var ascending=true;while(v!==edgeObj.w){node=g.node(v);if(ascending){while((pathV=path[pathIdx])!==lca&&g.node(pathV).maxRanklow||lim>postorderNums[parent].lim));lca=parent; +// Traverse from w to LCA +parent=w;while((parent=g.parent(parent))!==lca){wPath.push(parent)}return{path:vPath.concat(wPath.reverse()),lca:lca}}function postorder(g){var result={};var lim=0;function dfs(v){var low=lim;g.children(v).forEach(dfs);result[v]={low:low,lim:lim++}}g.children().forEach(dfs);return result}},{}],21:[function(require,module,exports){"use strict";var Graph=require("@dagrejs/graphlib").Graph;var util=require("../util"); +/* + * This module provides coordinate assignment based on Brandes and Köpf, "Fast + * and Simple Horizontal Coordinate Assignment." + */module.exports={positionX:positionX,findType1Conflicts:findType1Conflicts,findType2Conflicts:findType2Conflicts,addConflict:addConflict,hasConflict:hasConflict,verticalAlignment:verticalAlignment,horizontalCompaction:horizontalCompaction,alignCoordinates:alignCoordinates,findSmallestWidthAlignment:findSmallestWidthAlignment,balance:balance}; +/* + * Marks all edges in the graph with a type-1 conflict with the "type1Conflict" + * property. A type-1 conflict is one where a non-inner segment crosses an + * inner segment. An inner segment is an edge with both incident nodes marked + * with the "dummy" property. + * + * This algorithm scans layer by layer, starting with the second, for type-1 + * conflicts between the current layer and the previous layer. For each layer + * it scans the nodes from left to right until it reaches one that is incident + * on an inner segment. It then scans predecessors to determine if they have + * edges that cross that inner segment. At the end a final scan is done for all + * nodes on the current rank to see if they cross the last visited inner + * segment. + * + * This algorithm (safely) assumes that a dummy node will only be incident on a + * single node in the layers being scanned. + */function findType1Conflicts(g,layering){var conflicts={};function visitLayer(prevLayer,layer){var +// last visited node in the previous layer that is incident on an inner +// segment. +k0=0, +// Tracks the last node in this layer scanned for crossings with a type-1 +// segment. +scanPos=0,prevLayerLength=prevLayer.length,lastNode=layer[layer.length-1];layer.forEach(function(v,i){var w=findOtherInnerSegmentNode(g,v),k1=w?g.node(w).order:prevLayerLength;if(w||v===lastNode){layer.slice(scanPos,i+1).forEach(function(scanNode){g.predecessors(scanNode).forEach(function(u){var uLabel=g.node(u),uPos=uLabel.order;if((uPosnextNorthBorder)){addConflict(conflicts,u,v)}})}})}function visitLayer(north,south){var prevNorthPos=-1,nextNorthPos,southPos=0;south.forEach(function(v,southLookahead){if(g.node(v).dummy==="border"){var predecessors=g.predecessors(v);if(predecessors.length){nextNorthPos=g.node(predecessors[0]).order;scan(south,southPos,southLookahead,prevNorthPos,nextNorthPos);southPos=southLookahead;prevNorthPos=nextNorthPos}}scan(south,southPos,south.length,nextNorthPos,north.length)});return south}layering.reduce(visitLayer);return conflicts}function findOtherInnerSegmentNode(g,v){if(g.node(v).dummy){return g.predecessors(v).find(u=>g.node(u).dummy)}}function addConflict(conflicts,v,w){if(v>w){var tmp=v;v=w;w=tmp}var conflictsV=conflicts[v];if(!conflictsV){conflicts[v]=conflictsV={}}conflictsV[w]=true}function hasConflict(conflicts,v,w){if(v>w){var tmp=v;v=w;w=tmp}return!!conflicts[v]&&conflicts[v].hasOwnProperty(w)} +/* + * Try to align nodes into vertical "blocks" where possible. This algorithm + * attempts to align a node with one of its median neighbors. If the edge + * connecting a neighbor is a type-1 conflict then we ignore that possibility. + * If a previous node has already formed a block with a node after the node + * we're trying to form a block with, we also ignore that possibility - our + * blocks would be split in that scenario. + */function verticalAlignment(g,layering,conflicts,neighborFn){var root={},align={},pos={}; +// We cache the position here based on the layering because the graph and +// layering may be out of sync. The layering matrix is manipulated to +// generate different extreme alignments. +layering.forEach(function(layer){layer.forEach(function(v,order){root[v]=v;align[v]=v;pos[v]=order})});layering.forEach(function(layer){var prevIdx=-1;layer.forEach(function(v){var ws=neighborFn(v);if(ws.length){ws=ws.sort((a,b)=>pos[a]-pos[b]);var mp=(ws.length-1)/2;for(var i=Math.floor(mp),il=Math.ceil(mp);i<=il;++i){var w=ws[i];if(align[v]===v&&prevIdxxs[v]=xs[root[v]]);return xs}function buildBlockGraph(g,layering,root,reverseSep){var blockGraph=new Graph,graphLabel=g.graph(),sepFn=sep(graphLabel.nodesep,graphLabel.edgesep,reverseSep);layering.forEach(function(layer){var u;layer.forEach(function(v){var vRoot=root[v];blockGraph.setNode(vRoot);if(u){var uRoot=root[u],prevMax=blockGraph.edge(uRoot,vRoot);blockGraph.setEdge(uRoot,vRoot,Math.max(sepFn(g,v,u),prevMax||0))}u=v})});return blockGraph} +/* + * Returns the alignment that has the smallest width of the given alignments. + */function findSmallestWidthAlignment(g,xss){return Object.values(xss).reduce((currentMinAndXs,xs)=>{var max=Number.NEGATIVE_INFINITY;var min=Number.POSITIVE_INFINITY;Object.entries(xs).forEach(([v,x])=>{var halfWidth=width(g,v)/2;max=Math.max(x+halfWidth,max);min=Math.min(x-halfWidth,min)});const newMin=max-min;if(newMinx+delta)}})})}function balance(xss,align){return util.mapValues(xss.ul,function(num,v){if(align){return xss[align.toLowerCase()][v]}else{var xs=Object.values(xss).map(xs=>xs[v]).sort((a,b)=>a-b);return(xs[1]+xs[2])/2}})}function positionX(g){var layering=util.buildLayerMatrix(g);var conflicts=Object.assign(findType1Conflicts(g,layering),findType2Conflicts(g,layering));var xss={};var adjustedLayering;["u","d"].forEach(function(vert){adjustedLayering=vert==="u"?layering:Object.values(layering).reverse();["l","r"].forEach(function(horiz){if(horiz==="r"){adjustedLayering=adjustedLayering.map(inner=>{return Object.values(inner).reverse()})}var neighborFn=(vert==="u"?g.predecessors:g.successors).bind(g);var align=verticalAlignment(g,adjustedLayering,conflicts,neighborFn);var xs=horizontalCompaction(g,adjustedLayering,align.root,align.align,horiz==="r");if(horiz==="r"){xs=util.mapValues(xs,x=>-x)}xss[vert+horiz]=xs})});var smallestWidth=findSmallestWidthAlignment(g,xss);alignCoordinates(xss,smallestWidth);return balance(xss,g.graph().align)}function sep(nodeSep,edgeSep,reverseSep){return function(g,v,w){var vLabel=g.node(v);var wLabel=g.node(w);var sum=0;var delta;sum+=vLabel.width/2;if(vLabel.hasOwnProperty("labelpos")){switch(vLabel.labelpos.toLowerCase()){case"l":delta=-vLabel.width/2;break;case"r":delta=vLabel.width/2;break}}if(delta){sum+=reverseSep?delta:-delta}delta=0;sum+=(vLabel.dummy?edgeSep:nodeSep)/2;sum+=(wLabel.dummy?edgeSep:nodeSep)/2;sum+=wLabel.width/2;if(wLabel.hasOwnProperty("labelpos")){switch(wLabel.labelpos.toLowerCase()){case"l":delta=wLabel.width/2;break;case"r":delta=-wLabel.width/2;break}}if(delta){sum+=reverseSep?delta:-delta}delta=0;return sum}}function width(g,v){return g.node(v).width}},{"../util":27,"@dagrejs/graphlib":29}],22:[function(require,module,exports){"use strict";var util=require("../util");var positionX=require("./bk").positionX;module.exports=position;function position(g){g=util.asNonCompoundGraph(g);positionY(g);Object.entries(positionX(g)).forEach(([v,x])=>g.node(v).x=x)}function positionY(g){var layering=util.buildLayerMatrix(g);var rankSep=g.graph().ranksep;var prevY=0;layering.forEach(function(layer){const maxHeight=layer.reduce((acc,v)=>{const height=g.node(v).height;if(acc>height){return acc}else{return height}},0);layer.forEach(v=>g.node(v).y=prevY+maxHeight/2);prevY+=maxHeight+rankSep})}},{"../util":27,"./bk":21}],23:[function(require,module,exports){"use strict";var Graph=require("@dagrejs/graphlib").Graph;var slack=require("./util").slack;module.exports=feasibleTree; +/* + * Constructs a spanning tree with tight edges and adjusted the input node's + * ranks to achieve this. A tight edge is one that is has a length that matches + * its "minlen" attribute. + * + * The basic structure for this function is derived from Gansner, et al., "A + * Technique for Drawing Directed Graphs." + * + * Pre-conditions: + * + * 1. Graph must be a DAG. + * 2. Graph must be connected. + * 3. Graph must have at least one node. + * 5. Graph nodes must have been previously assigned a "rank" property that + * respects the "minlen" property of incident edges. + * 6. Graph edges must have a "minlen" property. + * + * Post-conditions: + * + * - Graph nodes will have their rank adjusted to ensure that all edges are + * tight. + * + * Returns a tree (undirected graph) that is constructed using only "tight" + * edges. + */function feasibleTree(g){var t=new Graph({directed:false}); +// Choose arbitrary node from which to start our tree +var start=g.nodes()[0];var size=g.nodeCount();t.setNode(start,{});var edge,delta;while(tightTree(t,g){let edgeSlack=Number.POSITIVE_INFINITY;if(t.hasNode(edge.v)!==t.hasNode(edge.w)){edgeSlack=slack(g,edge)}if(edgeSlackg.node(v).rank+=delta)}},{"./util":26,"@dagrejs/graphlib":29}],24:[function(require,module,exports){"use strict";var rankUtil=require("./util");var longestPath=rankUtil.longestPath;var feasibleTree=require("./feasible-tree");var networkSimplex=require("./network-simplex");module.exports=rank; +/* + * Assigns a rank to each node in the input graph that respects the "minlen" + * constraint specified on edges between nodes. + * + * This basic structure is derived from Gansner, et al., "A Technique for + * Drawing Directed Graphs." + * + * Pre-conditions: + * + * 1. Graph must be a connected DAG + * 2. Graph nodes must be objects + * 3. Graph edges must have "weight" and "minlen" attributes + * + * Post-conditions: + * + * 1. Graph nodes will have a "rank" attribute based on the results of the + * algorithm. Ranks can start at any index (including negative), we'll + * fix them up later. + */function rank(g){switch(g.graph().ranker){case"network-simplex":networkSimplexRanker(g);break;case"tight-tree":tightTreeRanker(g);break;case"longest-path":longestPathRanker(g);break;default:networkSimplexRanker(g)}} +// A fast and simple ranker, but results are far from optimal. +var longestPathRanker=longestPath;function tightTreeRanker(g){longestPath(g);feasibleTree(g)}function networkSimplexRanker(g){networkSimplex(g)}},{"./feasible-tree":23,"./network-simplex":25,"./util":26}],25:[function(require,module,exports){"use strict";var feasibleTree=require("./feasible-tree");var slack=require("./util").slack;var initRank=require("./util").longestPath;var preorder=require("@dagrejs/graphlib").alg.preorder;var postorder=require("@dagrejs/graphlib").alg.postorder;var simplify=require("../util").simplify;module.exports=networkSimplex; +// Expose some internals for testing purposes +networkSimplex.initLowLimValues=initLowLimValues;networkSimplex.initCutValues=initCutValues;networkSimplex.calcCutValue=calcCutValue;networkSimplex.leaveEdge=leaveEdge;networkSimplex.enterEdge=enterEdge;networkSimplex.exchangeEdges=exchangeEdges; +/* + * The network simplex algorithm assigns ranks to each node in the input graph + * and iteratively improves the ranking to reduce the length of edges. + * + * Preconditions: + * + * 1. The input graph must be a DAG. + * 2. All nodes in the graph must have an object value. + * 3. All edges in the graph must have "minlen" and "weight" attributes. + * + * Postconditions: + * + * 1. All nodes in the graph will have an assigned "rank" attribute that has + * been optimized by the network simplex algorithm. Ranks start at 0. + * + * + * A rough sketch of the algorithm is as follows: + * + * 1. Assign initial ranks to each node. We use the longest path algorithm, + * which assigns ranks to the lowest position possible. In general this + * leads to very wide bottom ranks and unnecessarily long edges. + * 2. Construct a feasible tight tree. A tight tree is one such that all + * edges in the tree have no slack (difference between length of edge + * and minlen for the edge). This by itself greatly improves the assigned + * rankings by shorting edges. + * 3. Iteratively find edges that have negative cut values. Generally a + * negative cut value indicates that the edge could be removed and a new + * tree edge could be added to produce a more compact graph. + * + * Much of the algorithms here are derived from Gansner, et al., "A Technique + * for Drawing Directed Graphs." The structure of the file roughly follows the + * structure of the overall algorithm. + */function networkSimplex(g){g=simplify(g);initRank(g);var t=feasibleTree(g);initLowLimValues(t);initCutValues(t,g);var e,f;while(e=leaveEdge(t)){f=enterEdge(t,g,e);exchangeEdges(t,g,e,f)}} +/* + * Initializes cut values for all edges in the tree. + */function initCutValues(t,g){var vs=postorder(t,t.nodes());vs=vs.slice(0,vs.length-1);vs.forEach(v=>assignCutValue(t,g,v))}function assignCutValue(t,g,child){var childLab=t.node(child);var parent=childLab.parent;t.edge(child,parent).cutvalue=calcCutValue(t,g,child)} +/* + * Given the tight tree, its graph, and a child in the graph calculate and + * return the cut value for the edge between the child and its parent. + */function calcCutValue(t,g,child){var childLab=t.node(child);var parent=childLab.parent; +// True if the child is on the tail end of the edge in the directed graph +var childIsTail=true; +// The graph's view of the tree edge we're inspecting +var graphEdge=g.edge(child,parent); +// The accumulated cut value for the edge between this node and its parent +var cutValue=0;if(!graphEdge){childIsTail=false;graphEdge=g.edge(parent,child)}cutValue=graphEdge.weight;g.nodeEdges(child).forEach(function(e){var isOutEdge=e.v===child,other=isOutEdge?e.w:e.v;if(other!==parent){var pointsToHead=isOutEdge===childIsTail,otherWeight=g.edge(e).weight;cutValue+=pointsToHead?otherWeight:-otherWeight;if(isTreeEdge(t,child,other)){var otherCutValue=t.edge(child,other).cutvalue;cutValue+=pointsToHead?-otherCutValue:otherCutValue}}});return cutValue}function initLowLimValues(tree,root){if(arguments.length<2){root=tree.nodes()[0]}dfsAssignLowLim(tree,{},1,root)}function dfsAssignLowLim(tree,visited,nextLim,v,parent){var low=nextLim;var label=tree.node(v);visited[v]=true;tree.neighbors(v).forEach(function(w){if(!visited.hasOwnProperty(w)){nextLim=dfsAssignLowLim(tree,visited,nextLim,w,v)}});label.low=low;label.lim=nextLim++;if(parent){label.parent=parent}else{ +// TODO should be able to remove this when we incrementally update low lim +delete label.parent}return nextLim}function leaveEdge(tree){return tree.edges().find(e=>tree.edge(e).cutvalue<0)}function enterEdge(t,g,edge){var v=edge.v;var w=edge.w; +// For the rest of this function we assume that v is the tail and w is the +// head, so if we don't have this edge in the graph we should flip it to +// match the correct orientation. +if(!g.hasEdge(v,w)){v=edge.w;w=edge.v}var vLabel=t.node(v);var wLabel=t.node(w);var tailLabel=vLabel;var flip=false; +// If the root is in the tail of the edge then we need to flip the logic that +// checks for the head and tail nodes in the candidates function below. +if(vLabel.lim>wLabel.lim){tailLabel=wLabel;flip=true}var candidates=g.edges().filter(function(edge){return flip===isDescendant(t,t.node(edge.v),tailLabel)&&flip!==isDescendant(t,t.node(edge.w),tailLabel)});return candidates.reduce((acc,edge)=>{if(slack(g,edge)!g.node(v).parent);var vs=preorder(t,root);vs=vs.slice(1);vs.forEach(function(v){var parent=t.node(v).parent,edge=g.edge(v,parent),flipped=false;if(!edge){edge=g.edge(parent,v);flipped=true}g.node(v).rank=g.node(parent).rank+(flipped?edge.minlen:-edge.minlen)})} +/* + * Returns true if the edge is in the tree. + */function isTreeEdge(tree,u,v){return tree.hasEdge(u,v)} +/* + * Returns true if the specified node is descendant of the root node per the + * assigned low and lim attributes in the tree. + */function isDescendant(tree,vLabel,rootLabel){return rootLabel.low<=vLabel.lim&&vLabel.lim<=rootLabel.lim}},{"../util":27,"./feasible-tree":23,"./util":26,"@dagrejs/graphlib":29}],26:[function(require,module,exports){"use strict";module.exports={longestPath:longestPath,slack:slack}; +/* + * Initializes ranks for the input graph using the longest path algorithm. This + * algorithm scales well and is fast in practice, it yields rather poor + * solutions. Nodes are pushed to the lowest layer possible, leaving the bottom + * ranks wide and leaving edges longer than necessary. However, due to its + * speed, this algorithm is good for getting an initial ranking that can be fed + * into other algorithms. + * + * This algorithm does not normalize layers because it will be used by other + * algorithms in most cases. If using this algorithm directly, be sure to + * run normalize at the end. + * + * Pre-conditions: + * + * 1. Input graph is a DAG. + * 2. Input graph node labels can be assigned properties. + * + * Post-conditions: + * + * 1. Each node will be assign an (unnormalized) "rank" property. + */function longestPath(g){var visited={};function dfs(v){var label=g.node(v);if(visited.hasOwnProperty(v)){return label.rank}visited[v]=true;var rank=Math.min(...g.outEdges(v).map(e=>{if(e==null){return Number.POSITIVE_INFINITY}return dfs(e.w)-g.edge(e).minlen}));if(rank===Number.POSITIVE_INFINITY){rank=0}return label.rank=rank}g.sources().forEach(dfs)} +/* + * Returns the amount of slack for the given edge. The slack is defined as the + * difference between the length of the edge and its minimum length. + */function slack(g,e){return g.node(e.w).rank-g.node(e.v).rank-g.edge(e).minlen}},{}],27:[function(require,module,exports){ +/* eslint "no-console": off */ +"use strict";var Graph=require("@dagrejs/graphlib").Graph;module.exports={addBorderNode:addBorderNode,addDummyNode:addDummyNode,asNonCompoundGraph:asNonCompoundGraph,buildLayerMatrix:buildLayerMatrix,intersectRect:intersectRect,mapValues:mapValues,maxRank:maxRank,normalizeRanks:normalizeRanks,notime:notime,partition:partition,pick:pick,predecessorWeights:predecessorWeights,range:range,removeEmptyRanks:removeEmptyRanks,simplify:simplify,successorWeights:successorWeights,time:time,uniqueId:uniqueId,zipObject:zipObject}; +/* + * Adds a dummy node to the graph and return v. + */function addDummyNode(g,type,attrs,name){var v;do{v=uniqueId(name)}while(g.hasNode(v));attrs.dummy=type;g.setNode(v,attrs);return v} +/* + * Returns a new graph with only simple edges. Handles aggregation of data + * associated with multi-edges. + */function simplify(g){var simplified=(new Graph).setGraph(g.graph());g.nodes().forEach(v=>simplified.setNode(v,g.node(v)));g.edges().forEach(e=>{var simpleLabel=simplified.edge(e.v,e.w)||{weight:0,minlen:1};var label=g.edge(e);simplified.setEdge(e.v,e.w,{weight:simpleLabel.weight+label.weight,minlen:Math.max(simpleLabel.minlen,label.minlen)})});return simplified}function asNonCompoundGraph(g){var simplified=new Graph({multigraph:g.isMultigraph()}).setGraph(g.graph());g.nodes().forEach(v=>{if(!g.children(v).length){simplified.setNode(v,g.node(v))}});g.edges().forEach(e=>{simplified.setEdge(e,g.edge(e))});return simplified}function successorWeights(g){var weightMap=g.nodes().map(v=>{var sucs={};g.outEdges(v).forEach(e=>{sucs[e.w]=(sucs[e.w]||0)+g.edge(e).weight});return sucs});return zipObject(g.nodes(),weightMap)}function predecessorWeights(g){var weightMap=g.nodes().map(v=>{var preds={};g.inEdges(v).forEach(e=>{preds[e.v]=(preds[e.v]||0)+g.edge(e).weight});return preds});return zipObject(g.nodes(),weightMap)} +/* + * Finds where a line starting at point ({x, y}) would intersect a rectangle + * ({x, y, width, height}) if it were pointing at the rectangle's center. + */function intersectRect(rect,point){var x=rect.x;var y=rect.y; +// Rectangle intersection algorithm from: +// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes +var dx=point.x-x;var dy=point.y-y;var w=rect.width/2;var h=rect.height/2;if(!dx&&!dy){throw new Error("Not possible to find intersection inside of the rectangle")}var sx,sy;if(Math.abs(dy)*w>Math.abs(dx)*h){ +// Intersection is top or bottom of rect. +if(dy<0){h=-h}sx=h*dx/dy;sy=h}else{ +// Intersection is left or right of rect. +if(dx<0){w=-w}sx=w;sy=w*dy/dx}return{x:x+sx,y:y+sy}} +/* + * Given a DAG with each node assigned "rank" and "order" properties, this + * function will produce a matrix with the ids of each node. + */function buildLayerMatrix(g){var layering=range(maxRank(g)+1).map(()=>[]);g.nodes().forEach(v=>{var node=g.node(v);var rank=node.rank;if(rank!==undefined){layering[rank][node.order]=v}});return layering} +/* + * Adjusts the ranks for all nodes in the graph such that all nodes v have + * rank(v) >= 0 and at least one node w has rank(w) = 0. + */function normalizeRanks(g){var min=Math.min(...g.nodes().map(v=>{var rank=g.node(v).rank;if(rank===undefined){return Number.MAX_VALUE}return rank}));g.nodes().forEach(v=>{var node=g.node(v);if(node.hasOwnProperty("rank")){node.rank-=min}})}function removeEmptyRanks(g){ +// Ranks may not start at 0, so we need to offset them +var offset=Math.min(...g.nodes().map(v=>g.node(v).rank));var layers=[];g.nodes().forEach(v=>{var rank=g.node(v).rank-offset;if(!layers[rank]){layers[rank]=[]}layers[rank].push(v)});var delta=0;var nodeRankFactor=g.graph().nodeRankFactor;Array.from(layers).forEach((vs,i)=>{if(vs===undefined&&i%nodeRankFactor!==0){--delta}else if(vs!==undefined&&delta){vs.forEach(v=>g.node(v).rank+=delta)}})}function addBorderNode(g,prefix,rank,order){var node={width:0,height:0};if(arguments.length>=4){node.rank=rank;node.order=order}return addDummyNode(g,"border",node,prefix)}function maxRank(g){return Math.max(...g.nodes().map(v=>{var rank=g.node(v).rank;if(rank===undefined){return Number.MIN_VALUE}return rank}))} +/* + * Partition a collection into two groups: `lhs` and `rhs`. If the supplied + * function returns true for an entry it goes into `lhs`. Otherwise it goes + * into `rhs. + */function partition(collection,fn){var result={lhs:[],rhs:[]};collection.forEach(value=>{if(fn(value)){result.lhs.push(value)}else{result.rhs.push(value)}});return result} +/* + * Returns a new function that wraps `fn` with a timer. The wrapper logs the + * time it takes to execute the function. + */function time(name,fn){var start=Date.now();try{return fn()}finally{console.log(name+" time: "+(Date.now()-start)+"ms")}}function notime(name,fn){return fn()}let idCounter=0;function uniqueId(prefix){var id=++idCounter;return toString(prefix)+id}function range(start,limit,step=1){if(limit==null){limit=start;start=0}let endCon=i=>ilimitval[funcOrProp]}return Object.entries(obj).reduce((acc,[k,v])=>{acc[k]=func(v,k);return acc},{})}function zipObject(props,values){return props.reduce((acc,key,i)=>{acc[key]=values[i];return acc},{})}},{"@dagrejs/graphlib":29}],28:[function(require,module,exports){module.exports="1.0.2"},{}],29:[function(require,module,exports){ +/** + * Copyright (c) 2014, Chris Pettitt + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +var lib=require("./lib");module.exports={Graph:lib.Graph,json:require("./lib/json"),alg:require("./lib/alg"),version:lib.version}},{"./lib":45,"./lib/alg":36,"./lib/json":46}],30:[function(require,module,exports){module.exports=components;function components(g){var visited={};var cmpts=[];var cmpt;function dfs(v){if(visited.hasOwnProperty(v))return;visited[v]=true;cmpt.push(v);g.successors(v).forEach(dfs);g.predecessors(v).forEach(dfs)}g.nodes().forEach(function(v){cmpt=[];dfs(v);if(cmpt.length){cmpts.push(cmpt)}});return cmpts}},{}],31:[function(require,module,exports){module.exports=dfs; +/* + * A helper that preforms a pre- or post-order traversal on the input graph + * and returns the nodes in the order they were visited. If the graph is + * undirected then this algorithm will navigate using neighbors. If the graph + * is directed then this algorithm will navigate using successors. + * + * If the order is not "post", it will be treated as "pre". + */function dfs(g,vs,order){if(!Array.isArray(vs)){vs=[vs]}var navigation=g.isDirected()?v=>g.successors(v):v=>g.neighbors(v);var orderFunc=order==="post"?postOrderDfs:preOrderDfs;var acc=[];var visited={};vs.forEach(v=>{if(!g.hasNode(v)){throw new Error("Graph does not have node: "+v)}orderFunc(v,navigation,visited,acc)});return acc}function postOrderDfs(v,navigation,visited,acc){var stack=[[v,false]];while(stack.length>0){var curr=stack.pop();if(curr[1]){acc.push(curr[0])}else{if(!visited.hasOwnProperty(curr[0])){visited[curr[0]]=true;stack.push([curr[0],true]);forEachRight(navigation(curr[0]),w=>stack.push([w,false]))}}}}function preOrderDfs(v,navigation,visited,acc){var stack=[v];while(stack.length>0){var curr=stack.pop();if(!visited.hasOwnProperty(curr)){visited[curr]=true;acc.push(curr);forEachRight(navigation(curr),w=>stack.push(w))}}}function forEachRight(array,iteratee){var length=array.length;while(length--){iteratee(array[length],length,array)}return array}},{}],32:[function(require,module,exports){var dijkstra=require("./dijkstra");module.exports=dijkstraAll;function dijkstraAll(g,weightFunc,edgeFunc){return g.nodes().reduce(function(acc,v){acc[v]=dijkstra(g,v,weightFunc,edgeFunc);return acc},{})}},{"./dijkstra":33}],33:[function(require,module,exports){var PriorityQueue=require("../data/priority-queue");module.exports=dijkstra;var DEFAULT_WEIGHT_FUNC=()=>1;function dijkstra(g,source,weightFn,edgeFn){return runDijkstra(g,String(source),weightFn||DEFAULT_WEIGHT_FUNC,edgeFn||function(v){return g.outEdges(v)})}function runDijkstra(g,source,weightFn,edgeFn){var results={};var pq=new PriorityQueue;var v,vEntry;var updateNeighbors=function(edge){var w=edge.v!==v?edge.v:edge.w;var wEntry=results[w];var weight=weightFn(edge);var distance=vEntry.distance+weight;if(weight<0){throw new Error("dijkstra does not allow negative edge weights. "+"Bad edge: "+edge+" Weight: "+weight)}if(distance0){v=pq.removeMin();vEntry=results[v];if(vEntry.distance===Number.POSITIVE_INFINITY){break}edgeFn(v).forEach(updateNeighbors)}return results}},{"../data/priority-queue":43}],34:[function(require,module,exports){var tarjan=require("./tarjan");module.exports=findCycles;function findCycles(g){return tarjan(g).filter(function(cmpt){return cmpt.length>1||cmpt.length===1&&g.hasEdge(cmpt[0],cmpt[0])})}},{"./tarjan":41}],35:[function(require,module,exports){module.exports=floydWarshall;var DEFAULT_WEIGHT_FUNC=()=>1;function floydWarshall(g,weightFn,edgeFn){return runFloydWarshall(g,weightFn||DEFAULT_WEIGHT_FUNC,edgeFn||function(v){return g.outEdges(v)})}function runFloydWarshall(g,weightFn,edgeFn){var results={};var nodes=g.nodes();nodes.forEach(function(v){results[v]={};results[v][v]={distance:0};nodes.forEach(function(w){if(v!==w){results[v][w]={distance:Number.POSITIVE_INFINITY}}});edgeFn(v).forEach(function(edge){var w=edge.v===v?edge.w:edge.v;var d=weightFn(edge);results[v][w]={distance:d,predecessor:v}})});nodes.forEach(function(k){var rowK=results[k];nodes.forEach(function(i){var rowI=results[i];nodes.forEach(function(j){var ik=rowI[k];var kj=rowK[j];var ij=rowI[j];var altDistance=ik.distance+kj.distance;if(altDistance0){v=pq.removeMin();if(parents.hasOwnProperty(v)){result.setEdge(v,parents[v])}else if(init){throw new Error("Input graph is not connected: "+g)}else{init=true}g.nodeEdges(v).forEach(updateNeighbors)}return result}},{"../data/priority-queue":43,"../graph":44}],41:[function(require,module,exports){module.exports=tarjan;function tarjan(g){var index=0;var stack=[];var visited={};// node id -> { onStack, lowlink, index } +var results=[];function dfs(v){var entry=visited[v]={onStack:true,lowlink:index,index:index++};stack.push(v);g.successors(v).forEach(function(w){if(!visited.hasOwnProperty(w)){dfs(w);entry.lowlink=Math.min(entry.lowlink,visited[w].lowlink)}else if(visited[w].onStack){entry.lowlink=Math.min(entry.lowlink,visited[w].index)}});if(entry.lowlink===entry.index){var cmpt=[];var w;do{w=stack.pop();visited[w].onStack=false;cmpt.push(w)}while(v!==w);results.push(cmpt)}}g.nodes().forEach(function(v){if(!visited.hasOwnProperty(v)){dfs(v)}});return results}},{}],42:[function(require,module,exports){function topsort(g){var visited={};var stack={};var results=[];function visit(node){if(stack.hasOwnProperty(node)){throw new CycleException}if(!visited.hasOwnProperty(node)){stack[node]=true;visited[node]=true;g.predecessors(node).forEach(visit);delete stack[node];results.push(node)}}g.sinks().forEach(visit);if(Object.keys(visited).length!==g.nodeCount()){throw new CycleException}return results}class CycleException extends Error{constructor(){super(...arguments)}}module.exports=topsort;topsort.CycleException=CycleException},{}],43:[function(require,module,exports){ +/** + * A min-priority queue data structure. This algorithm is derived from Cormen, + * et al., "Introduction to Algorithms". The basic idea of a min-priority + * queue is that you can efficiently (in O(1) time) get the smallest key in + * the queue. Adding and removing elements takes O(log n) time. A key can + * have its priority decreased in O(log n) time. + */ +class PriorityQueue{#arr=[];#keyIndices={}; +/** + * Returns the number of elements in the queue. Takes `O(1)` time. + */size(){return this.#arr.length} +/** + * Returns the keys that are in the queue. Takes `O(n)` time. + */keys(){return this.#arr.map(function(x){return x.key})} +/** + * Returns `true` if **key** is in the queue and `false` if not. + */has(key){return this.#keyIndices.hasOwnProperty(key)} +/** + * Returns the priority for **key**. If **key** is not present in the queue + * then this function returns `undefined`. Takes `O(1)` time. + * + * @param {Object} key + */priority(key){var index=this.#keyIndices[key];if(index!==undefined){return this.#arr[index].priority}} +/** + * Returns the key for the minimum element in this queue. If the queue is + * empty this function throws an Error. Takes `O(1)` time. + */min(){if(this.size()===0){throw new Error("Queue underflow")}return this.#arr[0].key} +/** + * Inserts a new key into the priority queue. If the key already exists in + * the queue this function returns `false`; otherwise it will return `true`. + * Takes `O(n)` time. + * + * @param {Object} key the key to add + * @param {Number} priority the initial priority for the key + */add(key,priority){var keyIndices=this.#keyIndices;key=String(key);if(!keyIndices.hasOwnProperty(key)){var arr=this.#arr;var index=arr.length;keyIndices[key]=index;arr.push({key:key,priority:priority});this.#decrease(index);return true}return false} +/** + * Removes and returns the smallest key in the queue. Takes `O(log n)` time. + */removeMin(){this.#swap(0,this.#arr.length-1);var min=this.#arr.pop();delete this.#keyIndices[min.key];this.#heapify(0);return min.key} +/** + * Decreases the priority for **key** to **priority**. If the new priority is + * greater than the previous priority, this function will throw an Error. + * + * @param {Object} key the key for which to raise priority + * @param {Number} priority the new priority for the key + */decrease(key,priority){var index=this.#keyIndices[key];if(priority>this.#arr[index].priority){throw new Error("New priority is greater than current priority. "+"Key: "+key+" Old: "+this.#arr[index].priority+" New: "+priority)}this.#arr[index].priority=priority;this.#decrease(index)}#heapify(i){var arr=this.#arr;var l=2*i;var r=l+1;var largest=i;if(l>1;if(arr[parent].priorityundefined; +// Defaults to be set when creating a new edge +#defaultEdgeLabelFn=()=>undefined; +// v -> label +#nodes={}; +// v -> edgeObj +#in={}; +// u -> v -> Number +#preds={}; +// v -> edgeObj +#out={}; +// v -> w -> Number +#sucs={}; +// e -> edgeObj +#edgeObjs={}; +// e -> label +#edgeLabels={}; +/* Number of nodes in the graph. Should only be changed by the implementation. */#nodeCount=0; +/* Number of edges in the graph. Should only be changed by the implementation. */#edgeCount=0;#parent;#children;constructor(opts){if(opts){this.#isDirected=opts.hasOwnProperty("directed")?opts.directed:true;this.#isMultigraph=opts.hasOwnProperty("multigraph")?opts.multigraph:false;this.#isCompound=opts.hasOwnProperty("compound")?opts.compound:false}if(this.#isCompound){ +// v -> parent +this.#parent={}; +// v -> children +this.#children={};this.#children[GRAPH_NODE]={}}} +/* === Graph functions ========= */ +/** + * Whether graph was created with 'directed' flag set to true or not. + */isDirected(){return this.#isDirected} +/** + * Whether graph was created with 'multigraph' flag set to true or not. + */isMultigraph(){return this.#isMultigraph} +/** + * Whether graph was created with 'compound' flag set to true or not. + */isCompound(){return this.#isCompound} +/** + * Sets the label of the graph. + */setGraph(label){this.#label=label;return this} +/** + * Gets the graph label. + */graph(){return this.#label} +/* === Node functions ========== */ +/** + * Sets the default node label. If newDefault is a function, it will be + * invoked ach time when setting a label for a node. Otherwise, this label + * will be assigned as default label in case if no label was specified while + * setting a node. + * Complexity: O(1). + */setDefaultNodeLabel(newDefault){this.#defaultNodeLabelFn=newDefault;if(typeof newDefault!=="function"){this.#defaultNodeLabelFn=()=>newDefault}return this} +/** + * Gets the number of nodes in the graph. + * Complexity: O(1). + */nodeCount(){return this.#nodeCount} +/** + * Gets all nodes of the graph. Note, the in case of compound graph subnodes are + * not included in list. + * Complexity: O(1). + */nodes(){return Object.keys(this.#nodes)} +/** + * Gets list of nodes without in-edges. + * Complexity: O(|V|). + */sources(){var self=this;return this.nodes().filter(v=>Object.keys(self.#in[v]).length===0)} +/** + * Gets list of nodes without out-edges. + * Complexity: O(|V|). + */sinks(){var self=this;return this.nodes().filter(v=>Object.keys(self.#out[v]).length===0)} +/** + * Invokes setNode method for each node in names list. + * Complexity: O(|names|). + */setNodes(vs,value){var args=arguments;var self=this;vs.forEach(function(v){if(args.length>1){self.setNode(v,value)}else{self.setNode(v)}});return this} +/** + * Creates or updates the value for the node v in the graph. If label is supplied + * it is set as the value for the node. If label is not supplied and the node was + * created by this call then the default node label will be assigned. + * Complexity: O(1). + */setNode(v,value){if(this.#nodes.hasOwnProperty(v)){if(arguments.length>1){this.#nodes[v]=value}return this}this.#nodes[v]=arguments.length>1?value:this.#defaultNodeLabelFn(v);if(this.#isCompound){this.#parent[v]=GRAPH_NODE;this.#children[v]={};this.#children[GRAPH_NODE][v]=true}this.#in[v]={};this.#preds[v]={};this.#out[v]={};this.#sucs[v]={};++this.#nodeCount;return this} +/** + * Gets the label of node with specified name. + * Complexity: O(|V|). + */node(v){return this.#nodes[v]} +/** + * Detects whether graph has a node with specified name or not. + */hasNode(v){return this.#nodes.hasOwnProperty(v)} +/** + * Remove the node with the name from the graph or do nothing if the node is not in + * the graph. If the node was removed this function also removes any incident + * edges. + * Complexity: O(1). + */removeNode(v){var self=this;if(this.#nodes.hasOwnProperty(v)){var removeEdge=e=>self.removeEdge(self.#edgeObjs[e]);delete this.#nodes[v];if(this.#isCompound){this.#removeFromParentsChildList(v);delete this.#parent[v];this.children(v).forEach(function(child){self.setParent(child)});delete this.#children[v]}Object.keys(this.#in[v]).forEach(removeEdge);delete this.#in[v];delete this.#preds[v];Object.keys(this.#out[v]).forEach(removeEdge);delete this.#out[v];delete this.#sucs[v];--this.#nodeCount}return this} +/** + * Sets node p as a parent for node v if it is defined, or removes the + * parent for v if p is undefined. Method throws an exception in case of + * invoking it in context of noncompound graph. + * Average-case complexity: O(1). + */setParent(v,parent){if(!this.#isCompound){throw new Error("Cannot set parent in a non-compound graph")}if(parent===undefined){parent=GRAPH_NODE}else{ +// Coerce parent to string +parent+="";for(var ancestor=parent;ancestor!==undefined;ancestor=this.parent(ancestor)){if(ancestor===v){throw new Error("Setting "+parent+" as parent of "+v+" would create a cycle")}}this.setNode(parent)}this.setNode(v);this.#removeFromParentsChildList(v);this.#parent[v]=parent;this.#children[parent][v]=true;return this}#removeFromParentsChildList(v){delete this.#children[this.#parent[v]][v]} +/** + * Gets parent node for node v. + * Complexity: O(1). + */parent(v){if(this.#isCompound){var parent=this.#parent[v];if(parent!==GRAPH_NODE){return parent}}} +/** + * Gets list of direct children of node v. + * Complexity: O(1). + */children(v=GRAPH_NODE){if(this.#isCompound){var children=this.#children[v];if(children){return Object.keys(children)}}else if(v===GRAPH_NODE){return this.nodes()}else if(this.hasNode(v)){return[]}} +/** + * Return all nodes that are predecessors of the specified node or undefined if node v is not in + * the graph. Behavior is undefined for undirected graphs - use neighbors instead. + * Complexity: O(|V|). + */predecessors(v){var predsV=this.#preds[v];if(predsV){return Object.keys(predsV)}} +/** + * Return all nodes that are successors of the specified node or undefined if node v is not in + * the graph. Behavior is undefined for undirected graphs - use neighbors instead. + * Complexity: O(|V|). + */successors(v){var sucsV=this.#sucs[v];if(sucsV){return Object.keys(sucsV)}} +/** + * Return all nodes that are predecessors or successors of the specified node or undefined if + * node v is not in the graph. + * Complexity: O(|V|). + */neighbors(v){var preds=this.predecessors(v);if(preds){const union=new Set(preds);for(var succ of this.successors(v)){union.add(succ)}return Array.from(union.values())}}isLeaf(v){var neighbors;if(this.isDirected()){neighbors=this.successors(v)}else{neighbors=this.neighbors(v)}return neighbors.length===0} +/** + * Creates new graph with nodes filtered via filter. Edges incident to rejected node + * are also removed. In case of compound graph, if parent is rejected by filter, + * than all its children are rejected too. + * Average-case complexity: O(|E|+|V|). + */filterNodes(filter){var copy=new this.constructor({directed:this.#isDirected,multigraph:this.#isMultigraph,compound:this.#isCompound});copy.setGraph(this.graph());var self=this;Object.entries(this.#nodes).forEach(function([v,value]){if(filter(v)){copy.setNode(v,value)}});Object.values(this.#edgeObjs).forEach(function(e){if(copy.hasNode(e.v)&©.hasNode(e.w)){copy.setEdge(e,self.edge(e))}});var parents={};function findParent(v){var parent=self.parent(v);if(parent===undefined||copy.hasNode(parent)){parents[v]=parent;return parent}else if(parent in parents){return parents[parent]}else{return findParent(parent)}}if(this.#isCompound){copy.nodes().forEach(v=>copy.setParent(v,findParent(v)))}return copy} +/* === Edge functions ========== */ +/** + * Sets the default edge label or factory function. This label will be + * assigned as default label in case if no label was specified while setting + * an edge or this function will be invoked each time when setting an edge + * with no label specified and returned value * will be used as a label for edge. + * Complexity: O(1). + */setDefaultEdgeLabel(newDefault){this.#defaultEdgeLabelFn=newDefault;if(typeof newDefault!=="function"){this.#defaultEdgeLabelFn=()=>newDefault}return this} +/** + * Gets the number of edges in the graph. + * Complexity: O(1). + */edgeCount(){return this.#edgeCount} +/** + * Gets edges of the graph. In case of compound graph subgraphs are not considered. + * Complexity: O(|E|). + */edges(){return Object.values(this.#edgeObjs)} +/** + * Establish an edges path over the nodes in nodes list. If some edge is already + * exists, it will update its label, otherwise it will create an edge between pair + * of nodes with label provided or default label if no label provided. + * Complexity: O(|nodes|). + */setPath(vs,value){var self=this;var args=arguments;vs.reduce(function(v,w){if(args.length>1){self.setEdge(v,w,value)}else{self.setEdge(v,w)}return w});return this} +/** + * Creates or updates the label for the edge (v, w) with the optionally supplied + * name. If label is supplied it is set as the value for the edge. If label is not + * supplied and the edge was created by this call then the default edge label will + * be assigned. The name parameter is only useful with multigraphs. + */setEdge(){var v,w,name,value;var valueSpecified=false;var arg0=arguments[0];if(typeof arg0==="object"&&arg0!==null&&"v"in arg0){v=arg0.v;w=arg0.w;name=arg0.name;if(arguments.length===2){value=arguments[1];valueSpecified=true}}else{v=arg0;w=arguments[1];name=arguments[3];if(arguments.length>2){value=arguments[2];valueSpecified=true}}v=""+v;w=""+w;if(name!==undefined){name=""+name}var e=edgeArgsToId(this.#isDirected,v,w,name);if(this.#edgeLabels.hasOwnProperty(e)){if(valueSpecified){this.#edgeLabels[e]=value}return this}if(name!==undefined&&!this.#isMultigraph){throw new Error("Cannot set a named edge when isMultigraph = false")} +// It didn't exist, so we need to create it. +// First ensure the nodes exist. +this.setNode(v);this.setNode(w);this.#edgeLabels[e]=valueSpecified?value:this.#defaultEdgeLabelFn(v,w,name);var edgeObj=edgeArgsToObj(this.#isDirected,v,w,name); +// Ensure we add undirected edges in a consistent way. +v=edgeObj.v;w=edgeObj.w;Object.freeze(edgeObj);this.#edgeObjs[e]=edgeObj;incrementOrInitEntry(this.#preds[w],v);incrementOrInitEntry(this.#sucs[v],w);this.#in[w][e]=edgeObj;this.#out[v][e]=edgeObj;this.#edgeCount++;return this} +/** + * Gets the label for the specified edge. + * Complexity: O(1). + */edge(v,w,name){var e=arguments.length===1?edgeObjToId(this.#isDirected,arguments[0]):edgeArgsToId(this.#isDirected,v,w,name);return this.#edgeLabels[e]} +/** + * Gets the label for the specified edge and converts it to an object. + * Complexity: O(1) + */edgeAsObj(){const edge=this.edge(...arguments);if(typeof edge!=="object"){return{label:edge}}return edge} +/** + * Detects whether the graph contains specified edge or not. No subgraphs are considered. + * Complexity: O(1). + */hasEdge(v,w,name){var e=arguments.length===1?edgeObjToId(this.#isDirected,arguments[0]):edgeArgsToId(this.#isDirected,v,w,name);return this.#edgeLabels.hasOwnProperty(e)} +/** + * Removes the specified edge from the graph. No subgraphs are considered. + * Complexity: O(1). + */removeEdge(v,w,name){var e=arguments.length===1?edgeObjToId(this.#isDirected,arguments[0]):edgeArgsToId(this.#isDirected,v,w,name);var edge=this.#edgeObjs[e];if(edge){v=edge.v;w=edge.w;delete this.#edgeLabels[e];delete this.#edgeObjs[e];decrementOrRemoveEntry(this.#preds[w],v);decrementOrRemoveEntry(this.#sucs[v],w);delete this.#in[w][e];delete this.#out[v][e];this.#edgeCount--}return this} +/** + * Return all edges that point to the node v. Optionally filters those edges down to just those + * coming from node u. Behavior is undefined for undirected graphs - use nodeEdges instead. + * Complexity: O(|E|). + */inEdges(v,u){var inV=this.#in[v];if(inV){var edges=Object.values(inV);if(!u){return edges}return edges.filter(edge=>edge.v===u)}} +/** + * Return all edges that are pointed at by node v. Optionally filters those edges down to just + * those point to w. Behavior is undefined for undirected graphs - use nodeEdges instead. + * Complexity: O(|E|). + */outEdges(v,w){var outV=this.#out[v];if(outV){var edges=Object.values(outV);if(!w){return edges}return edges.filter(edge=>edge.w===w)}} +/** + * Returns all edges to or from node v regardless of direction. Optionally filters those edges + * down to just those between nodes v and w regardless of direction. + * Complexity: O(|E|). + */nodeEdges(v,w){var inEdges=this.inEdges(v,w);if(inEdges){return inEdges.concat(this.outEdges(v,w))}}}function incrementOrInitEntry(map,k){if(map[k]){map[k]++}else{map[k]=1}}function decrementOrRemoveEntry(map,k){if(!--map[k]){delete map[k]}}function edgeArgsToId(isDirected,v_,w_,name){var v=""+v_;var w=""+w_;if(!isDirected&&v>w){var tmp=v;v=w;w=tmp}return v+EDGE_KEY_DELIM+w+EDGE_KEY_DELIM+(name===undefined?DEFAULT_EDGE_NAME:name)}function edgeArgsToObj(isDirected,v_,w_,name){var v=""+v_;var w=""+w_;if(!isDirected&&v>w){var tmp=v;v=w;w=tmp}var edgeObj={v:v,w:w};if(name){edgeObj.name=name}return edgeObj}function edgeObjToId(isDirected,edgeObj){return edgeArgsToId(isDirected,edgeObj.v,edgeObj.w,edgeObj.name)}module.exports=Graph},{}],45:[function(require,module,exports){ +// Includes only the "core" of graphlib +module.exports={Graph:require("./graph"),version:require("./version")}},{"./graph":44,"./version":47}],46:[function(require,module,exports){var Graph=require("./graph");module.exports={write:write,read:read}; +/** + * Creates a JSON representation of the graph that can be serialized to a string with + * JSON.stringify. The graph can later be restored using json.read. + */function write(g){var json={options:{directed:g.isDirected(),multigraph:g.isMultigraph(),compound:g.isCompound()},nodes:writeNodes(g),edges:writeEdges(g)};if(g.graph()!==undefined){json.value=structuredClone(g.graph())}return json}function writeNodes(g){return g.nodes().map(function(v){var nodeValue=g.node(v);var parent=g.parent(v);var node={v:v};if(nodeValue!==undefined){node.value=nodeValue}if(parent!==undefined){node.parent=parent}return node})}function writeEdges(g){return g.edges().map(function(e){var edgeValue=g.edge(e);var edge={v:e.v,w:e.w};if(e.name!==undefined){edge.name=e.name}if(edgeValue!==undefined){edge.value=edgeValue}return edge})} +/** + * Takes JSON as input and returns the graph representation. + * + * @example + * var g2 = graphlib.json.read(JSON.parse(str)); + * g2.nodes(); + * // ['a', 'b'] + * g2.edges() + * // [ { v: 'a', w: 'b' } ] + */function read(json){var g=new Graph(json.options).setGraph(json.value);json.nodes.forEach(function(entry){g.setNode(entry.v,entry.value);if(entry.parent){g.setParent(entry.v,entry.parent)}});json.edges.forEach(function(entry){g.setEdge({v:entry.v,w:entry.w,name:entry.name},entry.value)});return g}},{"./graph":44}],47:[function(require,module,exports){module.exports="2.1.13"},{}]},{},[1])(1)}); diff --git a/src/fairscape_cli/datasheet_builder/templates/evidence_graph/vendor/react-dom.production.min.js b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/vendor/react-dom.production.min.js new file mode 100644 index 0000000..fb4e099 --- /dev/null +++ b/src/fairscape_cli/datasheet_builder/templates/evidence_graph/vendor/react-dom.production.min.js @@ -0,0 +1,267 @@ +/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +(function(){/* + Modernizr 3.0.0pre (Custom Build) | MIT +*/ +'use strict';(function(Q,zb){"object"===typeof exports&&"undefined"!==typeof module?zb(exports,require("react")):"function"===typeof define&&define.amd?define(["exports","react"],zb):(Q=Q||self,zb(Q.ReactDOM={},Q.React))})(this,function(Q,zb){function m(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;cb}return!1}function Y(a,b,c,d,e,f,g){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f;this.removeEmptyString=g}function $d(a,b,c,d){var e=R.hasOwnProperty(b)?R[b]:null;if(null!==e?0!==e.type:d||!(2h||e[g]!==f[h]){var k="\n"+e[g].replace(" at new "," at ");a.displayName&&k.includes("")&&(k=k.replace("",a.displayName));return k}while(1<=g&&0<=h)}break}}}finally{ce=!1,Error.prepareStackTrace=c}return(a=a?a.displayName||a.name:"")?bc(a): +""}function fj(a){switch(a.tag){case 5:return bc(a.type);case 16:return bc("Lazy");case 13:return bc("Suspense");case 19:return bc("SuspenseList");case 0:case 2:case 15:return a=be(a.type,!1),a;case 11:return a=be(a.type.render,!1),a;case 1:return a=be(a.type,!0),a;default:return""}}function de(a){if(null==a)return null;if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case Bb:return"Fragment";case Cb:return"Portal";case ee:return"Profiler";case fe:return"StrictMode"; +case ge:return"Suspense";case he:return"SuspenseList"}if("object"===typeof a)switch(a.$$typeof){case gg:return(a.displayName||"Context")+".Consumer";case hg:return(a._context.displayName||"Context")+".Provider";case ie:var b=a.render;a=a.displayName;a||(a=b.displayName||b.name||"",a=""!==a?"ForwardRef("+a+")":"ForwardRef");return a;case je:return b=a.displayName||null,null!==b?b:de(a.type)||"Memo";case Ta:b=a._payload;a=a._init;try{return de(a(b))}catch(c){}}return null}function gj(a){var b=a.type; +switch(a.tag){case 24:return"Cache";case 9:return(b.displayName||"Context")+".Consumer";case 10:return(b._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return a=b.render,a=a.displayName||a.name||"",b.displayName||(""!==a?"ForwardRef("+a+")":"ForwardRef");case 7:return"Fragment";case 5:return b;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return de(b);case 8:return b===fe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler"; +case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if("function"===typeof b)return b.displayName||b.name||null;if("string"===typeof b)return b}return null}function Ua(a){switch(typeof a){case "boolean":case "number":case "string":case "undefined":return a;case "object":return a;default:return""}}function ig(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"=== +b)}function hj(a){var b=ig(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker= +null;delete a[b]}}}}function Pc(a){a._valueTracker||(a._valueTracker=hj(a))}function jg(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=ig(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Qc(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function ke(a,b){var c=b.checked;return E({},b,{defaultChecked:void 0,defaultValue:void 0, +value:void 0,checked:null!=c?c:a._wrapperState.initialChecked})}function kg(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=Ua(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function lg(a,b){b=b.checked;null!=b&&$d(a,"checked",b,!1)}function le(a,b){lg(a,b);var c=Ua(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!= +c)a.value=""+c}else a.value!==""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?me(a,b.type,c):b.hasOwnProperty("defaultValue")&&me(a,b.type,Ua(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function mg(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue; +c||b===a.value||(a.value=b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function me(a,b,c){if("number"!==b||Qc(a.ownerDocument)!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function Db(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e>>=0;return 0===a?32:31-(qj(a)/rj|0)|0}function hc(a){switch(a&-a){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a& +4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return a&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return a}}function Vc(a,b){var c=a.pendingLanes;if(0===c)return 0;var d=0,e=a.suspendedLanes,f=a.pingedLanes,g=c&268435455;if(0!==g){var h=g&~e;0!==h?d=hc(h):(f&=g,0!==f&&(d=hc(f)))}else g=c&~e,0!==g?d=hc(g):0!==f&&(d=hc(f));if(0===d)return 0;if(0!==b&&b!==d&&0===(b&e)&& +(e=d&-d,f=b&-b,e>=f||16===e&&0!==(f&4194240)))return b;0!==(d&4)&&(d|=c&16);b=a.entangledLanes;if(0!==b)for(a=a.entanglements,b&=d;0c;c++)b.push(a); +return b}function ic(a,b,c){a.pendingLanes|=b;536870912!==b&&(a.suspendedLanes=0,a.pingedLanes=0);a=a.eventTimes;b=31-ta(b);a[b]=c}function uj(a,b){var c=a.pendingLanes&~b;a.pendingLanes=b;a.suspendedLanes=0;a.pingedLanes=0;a.expiredLanes&=b;a.mutableReadLanes&=b;a.entangledLanes&=b;b=a.entanglements;var d=a.eventTimes;for(a=a.expirationTimes;0=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=$g(c)}}function bh(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?bh(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function ch(){for(var a=window,b=Qc();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break; +b=Qc(a.document)}return b}function Ie(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function Tj(a){var b=ch(),c=a.focusedElem,d=a.selectionRange;if(b!==c&&c&&c.ownerDocument&&bh(c.ownerDocument.documentElement,c)){if(null!==d&&Ie(c))if(b=d.start,a=d.end,void 0===a&&(a=b),"selectionStart"in c)c.selectionStart=b,c.selectionEnd=Math.min(a,c.value.length); +else if(a=(b=c.ownerDocument||document)&&b.defaultView||window,a.getSelection){a=a.getSelection();var e=c.textContent.length,f=Math.min(d.start,e);d=void 0===d.end?f:Math.min(d.end,e);!a.extend&&f>d&&(e=d,d=f,f=e);e=ah(c,f);var g=ah(c,d);e&&g&&(1!==a.rangeCount||a.anchorNode!==e.node||a.anchorOffset!==e.offset||a.focusNode!==g.node||a.focusOffset!==g.offset)&&(b=b.createRange(),b.setStart(e.node,e.offset),a.removeAllRanges(),f>d?(a.addRange(b),a.extend(g.node,g.offset)):(b.setEnd(g.node,g.offset), +a.addRange(b)))}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});"function"===typeof c.focus&&c.focus();for(c=0;cMb||(a.current=Se[Mb],Se[Mb]=null,Mb--)} +function y(a,b,c){Mb++;Se[Mb]=a.current;a.current=b}function Nb(a,b){var c=a.type.contextTypes;if(!c)return cb;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function ea(a){a=a.childContextTypes;return null!==a&&void 0!==a}function th(a,b,c){if(J.current!==cb)throw Error(m(168)); +y(J,b);y(S,c)}function uh(a,b,c){var d=a.stateNode;b=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in b))throw Error(m(108,gj(a)||"Unknown",e));return E({},c,d)}function ld(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||cb;pb=J.current;y(J,a);y(S,S.current);return!0}function vh(a,b,c){var d=a.stateNode;if(!d)throw Error(m(169));c?(a=uh(a,b,pb),d.__reactInternalMemoizedMergedChildContext=a,v(S),v(J),y(J,a)):v(S); +y(S,c)}function wh(a){null===La?La=[a]:La.push(a)}function jk(a){md=!0;wh(a)}function db(){if(!Te&&null!==La){Te=!0;var a=0,b=z;try{var c=La;for(z=1;a>=g;e-=g;Ma=1<<32-ta(b)+e|c<t?(q=l,l=null):q=l.sibling;var A=r(e,l,h[t],k);if(null===A){null===l&&(l=q);break}a&&l&&null===A.alternate&&b(e,l);g=f(A,g,t);null===m?n=A:m.sibling=A;m=A;l=q}if(t===h.length)return c(e,l),D&&qb(e,t),n;if(null===l){for(;t< +h.length;t++)l=u(e,h[t],k),null!==l&&(g=f(l,g,t),null===m?n=l:m.sibling=l,m=l);D&&qb(e,t);return n}for(l=d(e,l);tt?(A=q,q=null):A=q.sibling;var x=r(e,q,w.value,k);if(null===x){null===q&&(q=A);break}a&&q&&null===x.alternate&&b(e,q);g=f(x,g,t);null===l?n=x:l.sibling=x;l=x;q=A}if(w.done)return c(e,q),D&&qb(e,t),n;if(null===q){for(;!w.done;t++,w=h.next())w=u(e,w.value,k),null!==w&&(g=f(w,g,t),null===l?n=w:l.sibling=w,l=w);D&&qb(e,t);return n}for(q=d(e,q);!w.done;t++,w=h.next())w=p(q,e,t,w.value,k),null!==w&&(a&&null!==w.alternate&&q.delete(null===w.key?t:w.key),g=f(w,g,t),null===l?n=w:l.sibling= +w,l=w);a&&q.forEach(function(a){return b(e,a)});D&&qb(e,t);return n}function v(a,d,f,h){"object"===typeof f&&null!==f&&f.type===Bb&&null===f.key&&(f=f.props.children);if("object"===typeof f&&null!==f){switch(f.$$typeof){case sd:a:{for(var k=f.key,n=d;null!==n;){if(n.key===k){k=f.type;if(k===Bb){if(7===n.tag){c(a,n.sibling);d=e(n,f.props.children);d.return=a;a=d;break a}}else if(n.elementType===k||"object"===typeof k&&null!==k&&k.$$typeof===Ta&&Ch(k)===n.type){c(a,n.sibling);d=e(n,f.props);d.ref=vc(a, +n,f);d.return=a;a=d;break a}c(a,n);break}else b(a,n);n=n.sibling}f.type===Bb?(d=sb(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=rd(f.type,f.key,f.props,null,a.mode,h),h.ref=vc(a,d,f),h.return=a,a=h)}return g(a);case Cb:a:{for(n=f.key;null!==d;){if(d.key===n)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=$e(f,a.mode,h);d.return=a; +a=d}return g(a);case Ta:return n=f._init,v(a,d,n(f._payload),h)}if(cc(f))return x(a,d,f,h);if(ac(f))return I(a,d,f,h);qd(a,f)}return"string"===typeof f&&""!==f||"number"===typeof f?(f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=Ze(f,a.mode,h),d.return=a,a=d),g(a)):c(a,d)}return v}function af(){bf=Rb=td=null}function cf(a,b){b=ud.current;v(ud);a._currentValue=b}function df(a,b,c){for(;null!==a;){var d=a.alternate;(a.childLanes&b)!==b?(a.childLanes|=b,null!==d&&(d.childLanes|= +b)):null!==d&&(d.childLanes&b)!==b&&(d.childLanes|=b);if(a===c)break;a=a.return}}function Sb(a,b){td=a;bf=Rb=null;a=a.dependencies;null!==a&&null!==a.firstContext&&(0!==(a.lanes&b)&&(ha=!0),a.firstContext=null)}function qa(a){var b=a._currentValue;if(bf!==a)if(a={context:a,memoizedValue:b,next:null},null===Rb){if(null===td)throw Error(m(308));Rb=a;td.dependencies={lanes:0,firstContext:a}}else Rb=Rb.next=a;return b}function ef(a){null===tb?tb=[a]:tb.push(a)}function Eh(a,b,c,d){var e=b.interleaved; +null===e?(c.next=c,ef(b)):(c.next=e.next,e.next=c);b.interleaved=c;return Oa(a,d)}function Oa(a,b){a.lanes|=b;var c=a.alternate;null!==c&&(c.lanes|=b);c=a;for(a=a.return;null!==a;)a.childLanes|=b,c=a.alternate,null!==c&&(c.childLanes|=b),c=a,a=a.return;return 3===c.tag?c.stateNode:null}function ff(a){a.updateQueue={baseState:a.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Fh(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue= +{baseState:a.baseState,firstBaseUpdate:a.firstBaseUpdate,lastBaseUpdate:a.lastBaseUpdate,shared:a.shared,effects:a.effects})}function Pa(a,b){return{eventTime:a,lane:b,tag:0,payload:null,callback:null,next:null}}function fb(a,b,c){var d=a.updateQueue;if(null===d)return null;d=d.shared;if(0!==(p&2)){var e=d.pending;null===e?b.next=b:(b.next=e.next,e.next=b);d.pending=b;return kk(a,c)}e=d.interleaved;null===e?(b.next=b,ef(d)):(b.next=e.next,e.next=b);d.interleaved=b;return Oa(a,c)}function vd(a,b,c){b= +b.updateQueue;if(null!==b&&(b=b.shared,0!==(c&4194240))){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function Gh(a,b){var c=a.updateQueue,d=a.alternate;if(null!==d&&(d=d.updateQueue,c===d)){var e=null,f=null;c=c.firstBaseUpdate;if(null!==c){do{var g={eventTime:c.eventTime,lane:c.lane,tag:c.tag,payload:c.payload,callback:c.callback,next:null};null===f?e=f=g:f=f.next=g;c=c.next}while(null!==c);null===f?e=f=b:f=f.next=b}else e=f=b;c={baseState:d.baseState,firstBaseUpdate:e,lastBaseUpdate:f, +shared:d.shared,effects:d.effects};a.updateQueue=c;return}a=c.lastBaseUpdate;null===a?c.firstBaseUpdate=b:a.next=b;c.lastBaseUpdate=b}function wd(a,b,c,d){var e=a.updateQueue;gb=!1;var f=e.firstBaseUpdate,g=e.lastBaseUpdate,h=e.shared.pending;if(null!==h){e.shared.pending=null;var k=h,n=k.next;k.next=null;null===g?f=n:g.next=n;g=k;var l=a.alternate;null!==l&&(l=l.updateQueue,h=l.lastBaseUpdate,h!==g&&(null===h?l.firstBaseUpdate=n:h.next=n,l.lastBaseUpdate=k))}if(null!==f){var m=e.baseState;g=0;l= +n=k=null;h=f;do{var r=h.lane,p=h.eventTime;if((d&r)===r){null!==l&&(l=l.next={eventTime:p,lane:0,tag:h.tag,payload:h.payload,callback:h.callback,next:null});a:{var x=a,v=h;r=b;p=c;switch(v.tag){case 1:x=v.payload;if("function"===typeof x){m=x.call(p,m,r);break a}m=x;break a;case 3:x.flags=x.flags&-65537|128;case 0:x=v.payload;r="function"===typeof x?x.call(p,m,r):x;if(null===r||void 0===r)break a;m=E({},m,r);break a;case 2:gb=!0}}null!==h.callback&&0!==h.lane&&(a.flags|=64,r=e.effects,null===r?e.effects= +[h]:r.push(h))}else p={eventTime:p,lane:r,tag:h.tag,payload:h.payload,callback:h.callback,next:null},null===l?(n=l=p,k=m):l=l.next=p,g|=r;h=h.next;if(null===h)if(h=e.shared.pending,null===h)break;else r=h,h=r.next,r.next=null,e.lastBaseUpdate=r,e.shared.pending=null}while(1);null===l&&(k=m);e.baseState=k;e.firstBaseUpdate=n;e.lastBaseUpdate=l;b=e.shared.interleaved;if(null!==b){e=b;do g|=e.lane,e=e.next;while(e!==b)}else null===f&&(e.shared.lanes=0);ra|=g;a.lanes=g;a.memoizedState=m}}function Hh(a, +b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;bc?c:4;a(!0);var d=sf.transition;sf.transition= +{};try{a(!1),b()}finally{z=c,sf.transition=d}}function $h(){return sa().memoizedState}function qk(a,b,c){var d=hb(a);c={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ai(a))bi(b,c);else if(c=Eh(a,b,c,d),null!==c){var e=Z();xa(c,a,d,e);ci(c,b,d)}}function ok(a,b,c){var d=hb(a),e={lane:d,action:c,hasEagerState:!1,eagerState:null,next:null};if(ai(a))bi(b,e);else{var f=a.alternate;if(0===a.lanes&&(null===f||0===f.lanes)&&(f=b.lastRenderedReducer,null!==f))try{var g=b.lastRenderedState, +h=f(g,c);e.hasEagerState=!0;e.eagerState=h;if(ua(h,g)){var k=b.interleaved;null===k?(e.next=e,ef(b)):(e.next=k.next,k.next=e);b.interleaved=e;return}}catch(n){}finally{}c=Eh(a,b,e,d);null!==c&&(e=Z(),xa(c,a,d,e),ci(c,b,d))}}function ai(a){var b=a.alternate;return a===C||null!==b&&b===C}function bi(a,b){zc=Ad=!0;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b}function ci(a,b,c){if(0!==(c&4194240)){var d=b.lanes;d&=a.pendingLanes;c|=d;b.lanes=c;xe(a,c)}}function ya(a,b){if(a&& +a.defaultProps){b=E({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c]);return b}return b}function tf(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:E({},b,c);a.memoizedState=c;0===a.lanes&&(a.updateQueue.baseState=c)}function di(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,f,g):b.prototype&&b.prototype.isPureReactComponent?!qc(c,d)||!qc(e,f):!0}function ei(a,b,c){var d=!1,e=cb;var f=b.contextType;"object"===typeof f&& +null!==f?f=qa(f):(e=ea(b)?pb:J.current,d=b.contextTypes,f=(d=null!==d&&void 0!==d)?Nb(a,e):cb);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=Dd;a.stateNode=b;b._reactInternals=a;d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=e,a.__reactInternalMemoizedMaskedChildContext=f);return b}function fi(a,b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&& +b.UNSAFE_componentWillReceiveProps(c,d);b.state!==a&&Dd.enqueueReplaceState(b,b.state,null)}function uf(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs={};ff(a);var f=b.contextType;"object"===typeof f&&null!==f?e.context=qa(f):(f=ea(b)?pb:J.current,e.context=Nb(a,f));e.state=a.memoizedState;f=b.getDerivedStateFromProps;"function"===typeof f&&(tf(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!== +typeof e.UNSAFE_componentWillMount&&"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&Dd.enqueueReplaceState(e,e.state,null),wd(a,c,e,d),e.state=a.memoizedState);"function"===typeof e.componentDidMount&&(a.flags|=4194308)}function Ub(a,b){try{var c="",d=b;do c+=fj(d),d=d.return;while(d);var e=c}catch(f){e="\nError generating stack: "+f.message+ +"\n"+f.stack}return{value:a,source:b,stack:e,digest:null}}function vf(a,b,c){return{value:a,source:null,stack:null!=c?c:null,digest:null!=b?b:null}}function wf(a,b){try{console.error(b.value)}catch(c){setTimeout(function(){throw c;})}}function gi(a,b,c){c=Pa(-1,c);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){Ed||(Ed=!0,xf=d);wf(a,b)};return c}function hi(a,b,c){c=Pa(-1,c);c.tag=3;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){return d(e)}; +c.callback=function(){wf(a,b)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){wf(a,b);"function"!==typeof d&&(null===ib?ib=new Set([this]):ib.add(this));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:""})});return c}function ii(a,b,c){var d=a.pingCache;if(null===d){d=a.pingCache=new rk;var e=new Set;d.set(b,e)}else e=d.get(b),void 0===e&&(e=new Set,d.set(b,e));e.has(c)||(e.add(c),a=sk.bind(null,a,b,c),b.then(a,a))}function ji(a){do{var b; +if(b=13===a.tag)b=a.memoizedState,b=null!==b?null!==b.dehydrated?!0:!1:!0;if(b)return a;a=a.return}while(null!==a);return null}function ki(a,b,c,d,e){if(0===(a.mode&1))return a===b?a.flags|=65536:(a.flags|=128,c.flags|=131072,c.flags&=-52805,1===c.tag&&(null===c.alternate?c.tag=17:(b=Pa(-1,1),b.tag=2,fb(c,b,1))),c.lanes|=1),a;a.flags|=65536;a.lanes=e;return a}function aa(a,b,c,d){b.child=null===a?li(b,null,c,d):Vb(b,a.child,c,d)}function mi(a,b,c,d,e){c=c.render;var f=b.ref;Sb(b,e);d=mf(a,b,c,d,f, +e);c=nf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&c&&Ue(b);b.flags|=1;aa(a,b,d,e);return b.child}function ni(a,b,c,d,e){if(null===a){var f=c.type;if("function"===typeof f&&!yf(f)&&void 0===f.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=f,oi(a,b,f,d,e);a=rd(c.type,null,d,b,b.mode,e);a.ref=b.ref;a.return=b;return b.child=a}f=a.child;if(0===(a.lanes&e)){var g=f.memoizedProps;c=c.compare;c=null!==c?c:qc;if(c(g,d)&&a.ref=== +b.ref)return Qa(a,b,e)}b.flags|=1;a=eb(f,d);a.ref=b.ref;a.return=b;return b.child=a}function oi(a,b,c,d,e){if(null!==a){var f=a.memoizedProps;if(qc(f,d)&&a.ref===b.ref)if(ha=!1,b.pendingProps=d=f,0!==(a.lanes&e))0!==(a.flags&131072)&&(ha=!0);else return b.lanes=a.lanes,Qa(a,b,e)}return zf(a,b,c,d,e)}function pi(a,b,c){var d=b.pendingProps,e=d.children,f=null!==a?a.memoizedState:null;if("hidden"===d.mode)if(0===(b.mode&1))b.memoizedState={baseLanes:0,cachePool:null,transitions:null},y(Ga,ba),ba|=c; +else{if(0===(c&1073741824))return a=null!==f?f.baseLanes|c:c,b.lanes=b.childLanes=1073741824,b.memoizedState={baseLanes:a,cachePool:null,transitions:null},b.updateQueue=null,y(Ga,ba),ba|=a,null;b.memoizedState={baseLanes:0,cachePool:null,transitions:null};d=null!==f?f.baseLanes:c;y(Ga,ba);ba|=d}else null!==f?(d=f.baseLanes|c,b.memoizedState=null):d=c,y(Ga,ba),ba|=d;aa(a,b,e,c);return b.child}function qi(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.flags|=512,b.flags|=2097152}function zf(a, +b,c,d,e){var f=ea(c)?pb:J.current;f=Nb(b,f);Sb(b,e);c=mf(a,b,c,d,f,e);d=nf();if(null!==a&&!ha)return b.updateQueue=a.updateQueue,b.flags&=-2053,a.lanes&=~e,Qa(a,b,e);D&&d&&Ue(b);b.flags|=1;aa(a,b,c,e);return b.child}function ri(a,b,c,d,e){if(ea(c)){var f=!0;ld(b)}else f=!1;Sb(b,e);if(null===b.stateNode)Fd(a,b),ei(b,c,d),uf(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var k=g.context,n=c.contextType;"object"===typeof n&&null!==n?n=qa(n):(n=ea(c)?pb:J.current,n=Nb(b, +n));var l=c.getDerivedStateFromProps,m="function"===typeof l||"function"===typeof g.getSnapshotBeforeUpdate;m||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||k!==n)&&fi(b,g,d,n);gb=!1;var r=b.memoizedState;g.state=r;wd(b,d,g,e);k=b.memoizedState;h!==d||r!==k||S.current||gb?("function"===typeof l&&(tf(b,c,l,d),k=b.memoizedState),(h=gb||di(b,c,h,d,r,k,n))?(m||"function"!==typeof g.UNSAFE_componentWillMount&&"function"!==typeof g.componentWillMount|| +("function"===typeof g.componentWillMount&&g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.flags|=4194308)):("function"===typeof g.componentDidMount&&(b.flags|=4194308),b.memoizedProps=d,b.memoizedState=k),g.props=d,g.state=k,g.context=n,d=h):("function"===typeof g.componentDidMount&&(b.flags|=4194308),d=!1)}else{g=b.stateNode;Fh(a,b);h=b.memoizedProps;n=b.type===b.elementType?h:ya(b.type,h);g.props= +n;m=b.pendingProps;r=g.context;k=c.contextType;"object"===typeof k&&null!==k?k=qa(k):(k=ea(c)?pb:J.current,k=Nb(b,k));var p=c.getDerivedStateFromProps;(l="function"===typeof p||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==m||r!==k)&&fi(b,g,d,k);gb=!1;r=b.memoizedState;g.state=r;wd(b,d,g,e);var x=b.memoizedState;h!==m||r!==x||S.current||gb?("function"===typeof p&&(tf(b,c,p,d),x=b.memoizedState), +(n=gb||di(b,c,n,d,r,x,k)||!1)?(l||"function"!==typeof g.UNSAFE_componentWillUpdate&&"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,x,k),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,x,k)),"function"===typeof g.componentDidUpdate&&(b.flags|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.flags|=1024)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|= +4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),b.memoizedProps=d,b.memoizedState=x),g.props=d,g.state=x,g.context=k,d=n):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&r===a.memoizedState||(b.flags|=1024),d=!1)}return Af(a,b,c,d,f,e)}function Af(a,b,c,d,e,f){qi(a,b);var g=0!==(b.flags&128);if(!d&&!g)return e&&vh(b,c,!1), +Qa(a,b,f);d=b.stateNode;tk.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?null:d.render();b.flags|=1;null!==a&&g?(b.child=Vb(b,a.child,null,f),b.child=Vb(b,null,h,f)):aa(a,b,h,f);b.memoizedState=d.state;e&&vh(b,c,!0);return b.child}function si(a){var b=a.stateNode;b.pendingContext?th(a,b.pendingContext,b.pendingContext!==b.context):b.context&&th(a,b.context,!1);gf(a,b.containerInfo)}function ti(a,b,c,d,e){Qb();Ye(e);b.flags|=256;aa(a,b,c,d);return b.child}function Bf(a){return{baseLanes:a, +cachePool:null,transitions:null}}function ui(a,b,c){var d=b.pendingProps,e=F.current,f=!1,g=0!==(b.flags&128),h;(h=g)||(h=null!==a&&null===a.memoizedState?!1:0!==(e&2));if(h)f=!0,b.flags&=-129;else if(null===a||null!==a.memoizedState)e|=1;y(F,e&1);if(null===a){Xe(b);a=b.memoizedState;if(null!==a&&(a=a.dehydrated,null!==a))return 0===(b.mode&1)?b.lanes=1:"$!"===a.data?b.lanes=8:b.lanes=1073741824,null;g=d.children;a=d.fallback;return f?(d=b.mode,f=b.child,g={mode:"hidden",children:g},0===(d&1)&&null!== +f?(f.childLanes=0,f.pendingProps=g):f=Gd(g,d,0,null),a=sb(a,d,c,null),f.return=b,a.return=b,f.sibling=a,b.child=f,b.child.memoizedState=Bf(c),b.memoizedState=Cf,a):Df(b,g)}e=a.memoizedState;if(null!==e&&(h=e.dehydrated,null!==h))return uk(a,b,g,d,h,e,c);if(f){f=d.fallback;g=b.mode;e=a.child;h=e.sibling;var k={mode:"hidden",children:d.children};0===(g&1)&&b.child!==e?(d=b.child,d.childLanes=0,d.pendingProps=k,b.deletions=null):(d=eb(e,k),d.subtreeFlags=e.subtreeFlags&14680064);null!==h?f=eb(h,f):(f= +sb(f,g,c,null),f.flags|=2);f.return=b;d.return=b;d.sibling=f;b.child=d;d=f;f=b.child;g=a.child.memoizedState;g=null===g?Bf(c):{baseLanes:g.baseLanes|c,cachePool:null,transitions:g.transitions};f.memoizedState=g;f.childLanes=a.childLanes&~c;b.memoizedState=Cf;return d}f=a.child;a=f.sibling;d=eb(f,{mode:"visible",children:d.children});0===(b.mode&1)&&(d.lanes=c);d.return=b;d.sibling=null;null!==a&&(c=b.deletions,null===c?(b.deletions=[a],b.flags|=16):c.push(a));b.child=d;b.memoizedState=null;return d} +function Df(a,b,c){b=Gd({mode:"visible",children:b},a.mode,0,null);b.return=a;return a.child=b}function Hd(a,b,c,d){null!==d&&Ye(d);Vb(b,a.child,null,c);a=Df(b,b.pendingProps.children);a.flags|=2;b.memoizedState=null;return a}function uk(a,b,c,d,e,f,g){if(c){if(b.flags&256)return b.flags&=-257,d=vf(Error(m(422))),Hd(a,b,g,d);if(null!==b.memoizedState)return b.child=a.child,b.flags|=128,null;f=d.fallback;e=b.mode;d=Gd({mode:"visible",children:d.children},e,0,null);f=sb(f,e,g,null);f.flags|=2;d.return= +b;f.return=b;d.sibling=f;b.child=d;0!==(b.mode&1)&&Vb(b,a.child,null,g);b.child.memoizedState=Bf(g);b.memoizedState=Cf;return f}if(0===(b.mode&1))return Hd(a,b,g,null);if("$!"===e.data){d=e.nextSibling&&e.nextSibling.dataset;if(d)var h=d.dgst;d=h;f=Error(m(419));d=vf(f,d,void 0);return Hd(a,b,g,d)}h=0!==(g&a.childLanes);if(ha||h){d=O;if(null!==d){switch(g&-g){case 4:e=2;break;case 16:e=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:e= +32;break;case 536870912:e=268435456;break;default:e=0}e=0!==(e&(d.suspendedLanes|g))?0:e;0!==e&&e!==f.retryLane&&(f.retryLane=e,Oa(a,e),xa(d,a,e,-1))}Ef();d=vf(Error(m(421)));return Hd(a,b,g,d)}if("$?"===e.data)return b.flags|=128,b.child=a.child,b=vk.bind(null,a),e._reactRetry=b,null;a=f.treeContext;fa=Ka(e.nextSibling);la=b;D=!0;wa=null;null!==a&&(na[oa++]=Ma,na[oa++]=Na,na[oa++]=rb,Ma=a.id,Na=a.overflow,rb=b);b=Df(b,d.children);b.flags|=4096;return b}function vi(a,b,c){a.lanes|=b;var d=a.alternate; +null!==d&&(d.lanes|=b);df(a.return,b,c)}function Ff(a,b,c,d,e){var f=a.memoizedState;null===f?a.memoizedState={isBackwards:b,rendering:null,renderingStartTime:0,last:d,tail:c,tailMode:e}:(f.isBackwards=b,f.rendering=null,f.renderingStartTime=0,f.last=d,f.tail=c,f.tailMode=e)}function wi(a,b,c){var d=b.pendingProps,e=d.revealOrder,f=d.tail;aa(a,b,d.children,c);d=F.current;if(0!==(d&2))d=d&1|2,b.flags|=128;else{if(null!==a&&0!==(a.flags&128))a:for(a=b.child;null!==a;){if(13===a.tag)null!==a.memoizedState&& +vi(a,c,b);else if(19===a.tag)vi(a,c,b);else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===b)break a;for(;null===a.sibling;){if(null===a.return||a.return===b)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}d&=1}y(F,d);if(0===(b.mode&1))b.memoizedState=null;else switch(e){case "forwards":c=b.child;for(e=null;null!==c;)a=c.alternate,null!==a&&null===xd(a)&&(e=c),c=c.sibling;c=e;null===c?(e=b.child,b.child=null):(e=c.sibling,c.sibling=null);Ff(b,!1,e,c,f);break;case "backwards":c= +null;e=b.child;for(b.child=null;null!==e;){a=e.alternate;if(null!==a&&null===xd(a)){b.child=e;break}a=e.sibling;e.sibling=c;c=e;e=a}Ff(b,!0,c,null,f);break;case "together":Ff(b,!1,null,null,void 0);break;default:b.memoizedState=null}return b.child}function Fd(a,b){0===(b.mode&1)&&null!==a&&(a.alternate=null,b.alternate=null,b.flags|=2)}function Qa(a,b,c){null!==a&&(b.dependencies=a.dependencies);ra|=b.lanes;if(0===(c&b.childLanes))return null;if(null!==a&&b.child!==a.child)throw Error(m(153));if(null!== +b.child){a=b.child;c=eb(a,a.pendingProps);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=eb(a,a.pendingProps),c.return=b;c.sibling=null}return b.child}function wk(a,b,c){switch(b.tag){case 3:si(b);Qb();break;case 5:Ih(b);break;case 1:ea(b.type)&&ld(b);break;case 4:gf(b,b.stateNode.containerInfo);break;case 10:var d=b.type._context,e=b.memoizedProps.value;y(ud,d._currentValue);d._currentValue=e;break;case 13:d=b.memoizedState;if(null!==d){if(null!==d.dehydrated)return y(F,F.current& +1),b.flags|=128,null;if(0!==(c&b.child.childLanes))return ui(a,b,c);y(F,F.current&1);a=Qa(a,b,c);return null!==a?a.sibling:null}y(F,F.current&1);break;case 19:d=0!==(c&b.childLanes);if(0!==(a.flags&128)){if(d)return wi(a,b,c);b.flags|=128}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null,e.lastEffect=null);y(F,F.current);if(d)break;else return null;case 22:case 23:return b.lanes=0,pi(a,b,c)}return Qa(a,b,c)}function Dc(a,b){if(!D)switch(a.tailMode){case "hidden":b=a.tail;for(var c=null;null!== +b;)null!==b.alternate&&(c=b),b=b.sibling;null===c?a.tail=null:c.sibling=null;break;case "collapsed":c=a.tail;for(var d=null;null!==c;)null!==c.alternate&&(d=c),c=c.sibling;null===d?b||null===a.tail?a.tail=null:a.tail.sibling=null:d.sibling=null}}function W(a){var b=null!==a.alternate&&a.alternate.child===a.child,c=0,d=0;if(b)for(var e=a.child;null!==e;)c|=e.lanes|e.childLanes,d|=e.subtreeFlags&14680064,d|=e.flags&14680064,e.return=a,e=e.sibling;else for(e=a.child;null!==e;)c|=e.lanes|e.childLanes, +d|=e.subtreeFlags,d|=e.flags,e.return=a,e=e.sibling;a.subtreeFlags|=d;a.childLanes=c;return b}function xk(a,b,c){var d=b.pendingProps;Ve(b);switch(b.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return W(b),null;case 1:return ea(b.type)&&(v(S),v(J)),W(b),null;case 3:d=b.stateNode;Tb();v(S);v(J);jf();d.pendingContext&&(d.context=d.pendingContext,d.pendingContext=null);if(null===a||null===a.child)pd(b)?b.flags|=4:null===a||a.memoizedState.isDehydrated&&0===(b.flags& +256)||(b.flags|=1024,null!==wa&&(Gf(wa),wa=null));xi(a,b);W(b);return null;case 5:hf(b);var e=ub(xc.current);c=b.type;if(null!==a&&null!=b.stateNode)yk(a,b,c,d,e),a.ref!==b.ref&&(b.flags|=512,b.flags|=2097152);else{if(!d){if(null===b.stateNode)throw Error(m(166));W(b);return null}a=ub(Ea.current);if(pd(b)){d=b.stateNode;c=b.type;var f=b.memoizedProps;d[Da]=b;d[uc]=f;a=0!==(b.mode&1);switch(c){case "dialog":B("cancel",d);B("close",d);break;case "iframe":case "object":case "embed":B("load",d);break; +case "video":case "audio":for(e=0;e\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(c,{is:d.is}):(a=g.createElement(c),"select"===c&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,c);a[Da]=b;a[uc]=d;zk(a,b,!1,!1);b.stateNode=a;a:{g=qe(c,d);switch(c){case "dialog":B("cancel",a);B("close",a);e=d;break;case "iframe":case "object":case "embed":B("load",a);e=d;break; +case "video":case "audio":for(e=0;eHf&&(b.flags|=128,d=!0,Dc(f,!1),b.lanes=4194304)}else{if(!d)if(a=xd(g),null!==a){if(b.flags|=128,d=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.flags|=4),Dc(f,!0),null===f.tail&&"hidden"===f.tailMode&&!g.alternate&&!D)return W(b),null}else 2*P()-f.renderingStartTime>Hf&&1073741824!==c&&(b.flags|= +128,d=!0,Dc(f,!1),b.lanes=4194304);f.isBackwards?(g.sibling=b.child,b.child=g):(c=f.last,null!==c?c.sibling=g:b.child=g,f.last=g)}if(null!==f.tail)return b=f.tail,f.rendering=b,f.tail=b.sibling,f.renderingStartTime=P(),b.sibling=null,c=F.current,y(F,d?c&1|2:c&1),b;W(b);return null;case 22:case 23:return ba=Ga.current,v(Ga),d=null!==b.memoizedState,null!==a&&null!==a.memoizedState!==d&&(b.flags|=8192),d&&0!==(b.mode&1)?0!==(ba&1073741824)&&(W(b),b.subtreeFlags&6&&(b.flags|=8192)):W(b),null;case 24:return null; +case 25:return null}throw Error(m(156,b.tag));}function Bk(a,b,c){Ve(b);switch(b.tag){case 1:return ea(b.type)&&(v(S),v(J)),a=b.flags,a&65536?(b.flags=a&-65537|128,b):null;case 3:return Tb(),v(S),v(J),jf(),a=b.flags,0!==(a&65536)&&0===(a&128)?(b.flags=a&-65537|128,b):null;case 5:return hf(b),null;case 13:v(F);a=b.memoizedState;if(null!==a&&null!==a.dehydrated){if(null===b.alternate)throw Error(m(340));Qb()}a=b.flags;return a&65536?(b.flags=a&-65537|128,b):null;case 19:return v(F),null;case 4:return Tb(), +null;case 10:return cf(b.type._context),null;case 22:case 23:return ba=Ga.current,v(Ga),null;case 24:return null;default:return null}}function Wb(a,b){var c=a.ref;if(null!==c)if("function"===typeof c)try{c(null)}catch(d){G(a,b,d)}else c.current=null}function If(a,b,c){try{c()}catch(d){G(a,b,d)}}function Ck(a,b){Jf=Zc;a=ch();if(Ie(a)){if("selectionStart"in a)var c={start:a.selectionStart,end:a.selectionEnd};else a:{c=(c=a.ownerDocument)&&c.defaultView||window;var d=c.getSelection&&c.getSelection(); +if(d&&0!==d.rangeCount){c=d.anchorNode;var e=d.anchorOffset,f=d.focusNode;d=d.focusOffset;try{c.nodeType,f.nodeType}catch(M){c=null;break a}var g=0,h=-1,k=-1,n=0,q=0,u=a,r=null;b:for(;;){for(var p;;){u!==c||0!==e&&3!==u.nodeType||(h=g+e);u!==f||0!==d&&3!==u.nodeType||(k=g+d);3===u.nodeType&&(g+=u.nodeValue.length);if(null===(p=u.firstChild))break;r=u;u=p}for(;;){if(u===a)break b;r===c&&++n===e&&(h=g);r===f&&++q===d&&(k=g);if(null!==(p=u.nextSibling))break;u=r;r=u.parentNode}u=p}c=-1===h||-1===k?null: +{start:h,end:k}}else c=null}c=c||{start:0,end:0}}else c=null;Kf={focusedElem:a,selectionRange:c};Zc=!1;for(l=b;null!==l;)if(b=l,a=b.child,0!==(b.subtreeFlags&1028)&&null!==a)a.return=b,l=a;else for(;null!==l;){b=l;try{var x=b.alternate;if(0!==(b.flags&1024))switch(b.tag){case 0:case 11:case 15:break;case 1:if(null!==x){var v=x.memoizedProps,z=x.memoizedState,w=b.stateNode,A=w.getSnapshotBeforeUpdate(b.elementType===b.type?v:ya(b.type,v),z);w.__reactInternalSnapshotBeforeUpdate=A}break;case 3:var t= +b.stateNode.containerInfo;1===t.nodeType?t.textContent="":9===t.nodeType&&t.documentElement&&t.removeChild(t.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(m(163));}}catch(M){G(b,b.return,M)}a=b.sibling;if(null!==a){a.return=b.return;l=a;break}l=b.return}x=zi;zi=!1;return x}function Gc(a,b,c){var d=b.updateQueue;d=null!==d?d.lastEffect:null;if(null!==d){var e=d=d.next;do{if((e.tag&a)===a){var f=e.destroy;e.destroy=void 0;void 0!==f&&If(b,c,f)}e=e.next}while(e!==d)}} +function Id(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.create;c.destroy=d()}c=c.next}while(c!==b)}}function Lf(a){var b=a.ref;if(null!==b){var c=a.stateNode;switch(a.tag){case 5:a=c;break;default:a=c}"function"===typeof b?b(a):b.current=a}}function Ai(a){var b=a.alternate;null!==b&&(a.alternate=null,Ai(b));a.child=null;a.deletions=null;a.sibling=null;5===a.tag&&(b=a.stateNode,null!==b&&(delete b[Da],delete b[uc],delete b[Me],delete b[Dk], +delete b[Ek]));a.stateNode=null;a.return=null;a.dependencies=null;a.memoizedProps=null;a.memoizedState=null;a.pendingProps=null;a.stateNode=null;a.updateQueue=null}function Bi(a){return 5===a.tag||3===a.tag||4===a.tag}function Ci(a){a:for(;;){for(;null===a.sibling;){if(null===a.return||Bi(a.return))return null;a=a.return}a.sibling.return=a.return;for(a=a.sibling;5!==a.tag&&6!==a.tag&&18!==a.tag;){if(a.flags&2)continue a;if(null===a.child||4===a.tag)continue a;else a.child.return=a,a=a.child}if(!(a.flags& +2))return a.stateNode}}function Mf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=kd));else if(4!==d&&(a=a.child,null!==a))for(Mf(a,b,c),a=a.sibling;null!==a;)Mf(a,b,c),a=a.sibling}function Nf(a,b,c){var d=a.tag;if(5===d||6===d)a=a.stateNode,b?c.insertBefore(a,b):c.appendChild(a); +else if(4!==d&&(a=a.child,null!==a))for(Nf(a,b,c),a=a.sibling;null!==a;)Nf(a,b,c),a=a.sibling}function jb(a,b,c){for(c=c.child;null!==c;)Di(a,b,c),c=c.sibling}function Di(a,b,c){if(Ca&&"function"===typeof Ca.onCommitFiberUnmount)try{Ca.onCommitFiberUnmount(Uc,c)}catch(h){}switch(c.tag){case 5:X||Wb(c,b);case 6:var d=T,e=za;T=null;jb(a,b,c);T=d;za=e;null!==T&&(za?(a=T,c=c.stateNode,8===a.nodeType?a.parentNode.removeChild(c):a.removeChild(c)):T.removeChild(c.stateNode));break;case 18:null!==T&&(za? +(a=T,c=c.stateNode,8===a.nodeType?Re(a.parentNode,c):1===a.nodeType&&Re(a,c),nc(a)):Re(T,c.stateNode));break;case 4:d=T;e=za;T=c.stateNode.containerInfo;za=!0;jb(a,b,c);T=d;za=e;break;case 0:case 11:case 14:case 15:if(!X&&(d=c.updateQueue,null!==d&&(d=d.lastEffect,null!==d))){e=d=d.next;do{var f=e,g=f.destroy;f=f.tag;void 0!==g&&(0!==(f&2)?If(c,b,g):0!==(f&4)&&If(c,b,g));e=e.next}while(e!==d)}jb(a,b,c);break;case 1:if(!X&&(Wb(c,b),d=c.stateNode,"function"===typeof d.componentWillUnmount))try{d.props= +c.memoizedProps,d.state=c.memoizedState,d.componentWillUnmount()}catch(h){G(c,b,h)}jb(a,b,c);break;case 21:jb(a,b,c);break;case 22:c.mode&1?(X=(d=X)||null!==c.memoizedState,jb(a,b,c),X=d):jb(a,b,c);break;default:jb(a,b,c)}}function Ei(a){var b=a.updateQueue;if(null!==b){a.updateQueue=null;var c=a.stateNode;null===c&&(c=a.stateNode=new Fk);b.forEach(function(b){var d=Gk.bind(null,a,b);c.has(b)||(c.add(b),b.then(d,d))})}}function Aa(a,b,c){c=b.deletions;if(null!==c)for(var d=0;de&&(e=g);d&=~f}d=e;d=P()-d;d=(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*Mk(d/1960))-d;if(10a?16:a;if(null===lb)var d=!1;else{a=lb;lb=null;Qd=0;if(0!==(p&6))throw Error(m(331));var e=p;p|=4;for(l=a.current;null!==l;){var f=l,g=f.child;if(0!==(l.flags&16)){var h=f.deletions;if(null!==h){for(var k=0;kP()-Of?wb(a,0):Sf|=c);ia(a,b)}function Ti(a,b){0===b&&(0===(a.mode&1)?b=1:(b=Rd,Rd<<=1,0===(Rd&130023424)&&(Rd=4194304)));var c=Z();a=Oa(a,b);null!==a&&(ic(a,b,c),ia(a,c))}function vk(a){var b=a.memoizedState,c=0;null!==b&&(c=b.retryLane);Ti(a,c)}function Gk(a,b){var c=0;switch(a.tag){case 13:var d=a.stateNode;var e=a.memoizedState;null!==e&&(c=e.retryLane); +break;case 19:d=a.stateNode;break;default:throw Error(m(314));}null!==d&&d.delete(b);Ti(a,c)}function Mi(a,b){return xh(a,b)}function Tk(a,b,c,d){this.tag=a;this.key=c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.subtreeFlags=this.flags=0;this.deletions=null;this.childLanes=this.lanes=0;this.alternate=null}function yf(a){a= +a.prototype;return!(!a||!a.isReactComponent)}function Uk(a){if("function"===typeof a)return yf(a)?1:0;if(void 0!==a&&null!==a){a=a.$$typeof;if(a===ie)return 11;if(a===je)return 14}return 2}function eb(a,b){var c=a.alternate;null===c?(c=pa(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.type=a.type,c.flags=0,c.subtreeFlags=0,c.deletions=null);c.flags=a.flags&14680064;c.childLanes=a.childLanes;c.lanes=a.lanes;c.child= +a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;b=a.dependencies;c.dependencies=null===b?null:{lanes:b.lanes,firstContext:b.firstContext};c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function rd(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)yf(a)&&(g=1);else if("string"===typeof a)g=5;else a:switch(a){case Bb:return sb(c.children,e,f,b);case fe:g=8;e|=8;break;case ee:return a=pa(12,c,b,e|2),a.elementType=ee,a.lanes=f,a;case ge:return a= +pa(13,c,b,e),a.elementType=ge,a.lanes=f,a;case he:return a=pa(19,c,b,e),a.elementType=he,a.lanes=f,a;case Ui:return Gd(c,e,f,b);default:if("object"===typeof a&&null!==a)switch(a.$$typeof){case hg:g=10;break a;case gg:g=9;break a;case ie:g=11;break a;case je:g=14;break a;case Ta:g=16;d=null;break a}throw Error(m(130,null==a?a:typeof a,""));}b=pa(g,c,b,e);b.elementType=a;b.type=d;b.lanes=f;return b}function sb(a,b,c,d){a=pa(7,a,d,b);a.lanes=c;return a}function Gd(a,b,c,d){a=pa(22,a,d,b);a.elementType= +Ui;a.lanes=c;a.stateNode={isHidden:!1};return a}function Ze(a,b,c){a=pa(6,a,null,b);a.lanes=c;return a}function $e(a,b,c){b=pa(4,null!==a.children?a.children:[],a.key,b);b.lanes=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Vk(a,b,c,d,e){this.tag=b;this.containerInfo=a;this.finishedWork=this.pingCache=this.current=this.pendingChildren=null;this.timeoutHandle=-1;this.callbackNode=this.pendingContext=this.context=null;this.callbackPriority= +0;this.eventTimes=we(0);this.expirationTimes=we(-1);this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0;this.entanglements=we(0);this.identifierPrefix=d;this.onRecoverableError=e;this.mutableSourceEagerHydrationData=null}function Vf(a,b,c,d,e,f,g,h,k,l){a=new Vk(a,b,c,h,k);1===b?(b=1,!0===f&&(b|=8)):b=0;f=pa(3,null,null,b);a.current=f;f.stateNode=a;f.memoizedState={element:d,isDehydrated:c,cache:null,transitions:null, +pendingSuspenseBoundaries:null};ff(f);return a}function Wk(a,b,c){var d=3