mostlyright-data 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mostlyright/data_harness/__init__.py +158 -0
- mostlyright/data_harness/acquisition/__init__.py +55 -0
- mostlyright/data_harness/acquisition/http.py +2773 -0
- mostlyright/data_harness/acquisition/parsing.py +809 -0
- mostlyright/data_harness/acquisition/ranges.py +495 -0
- mostlyright/data_harness/acquisition/result_download.py +360 -0
- mostlyright/data_harness/acquisition/retention_admission.py +248 -0
- mostlyright/data_harness/acquisition/sandbox.py +4888 -0
- mostlyright/data_harness/acquisition/url_policy.py +530 -0
- mostlyright/data_harness/agent_runtime.py +2743 -0
- mostlyright/data_harness/assets/logo-ink.svg +31 -0
- mostlyright/data_harness/backends/__init__.py +28 -0
- mostlyright/data_harness/backends/pandas_backend.py +350 -0
- mostlyright/data_harness/backends/polars_backend.py +366 -0
- mostlyright/data_harness/backends/protocol.py +124 -0
- mostlyright/data_harness/backends/reference.py +83 -0
- mostlyright/data_harness/backends/registry.py +55 -0
- mostlyright/data_harness/backends/restrictions.py +126 -0
- mostlyright/data_harness/canonical.py +333 -0
- mostlyright/data_harness/catalog_job.py +625 -0
- mostlyright/data_harness/cli.py +5398 -0
- mostlyright/data_harness/contracts.py +53 -0
- mostlyright/data_harness/coordinator.py +1307 -0
- mostlyright/data_harness/deploy.py +924 -0
- mostlyright/data_harness/deploy_target.py +312 -0
- mostlyright/data_harness/deployment_evidence.py +1067 -0
- mostlyright/data_harness/event_presentation.py +576 -0
- mostlyright/data_harness/events.py +2152 -0
- mostlyright/data_harness/fast_delimited.py +239 -0
- mostlyright/data_harness/fleet.py +237 -0
- mostlyright/data_harness/formats.py +236 -0
- mostlyright/data_harness/governors.py +1163 -0
- mostlyright/data_harness/hosted_bootstrap.py +972 -0
- mostlyright/data_harness/hosted_crawler.py +1115 -0
- mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
- mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
- mostlyright/data_harness/hosted_crawler_job.py +1277 -0
- mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
- mostlyright/data_harness/hosted_dataset.py +1500 -0
- mostlyright/data_harness/hosted_deploy.py +3037 -0
- mostlyright/data_harness/hosted_handoff.py +62 -0
- mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
- mostlyright/data_harness/hosted_ingestion_job.py +356 -0
- mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
- mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
- mostlyright/data_harness/hosted_session_worker.py +3554 -0
- mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
- mostlyright/data_harness/hosted_worker.py +6784 -0
- mostlyright/data_harness/ingestion/__init__.py +56 -0
- mostlyright/data_harness/ingestion/contracts.py +461 -0
- mostlyright/data_harness/ingestion/faults.py +42 -0
- mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
- mostlyright/data_harness/ingestion/spool.py +130 -0
- mostlyright/data_harness/ingestion/store.py +885 -0
- mostlyright/data_harness/key_seam.py +434 -0
- mostlyright/data_harness/linux_process_boundary.py +262 -0
- mostlyright/data_harness/local_contracts.py +2880 -0
- mostlyright/data_harness/local_search/__init__.py +5 -0
- mostlyright/data_harness/local_search/build_index.py +1087 -0
- mostlyright/data_harness/local_search/contracts.py +920 -0
- mostlyright/data_harness/local_search/query_trace.py +266 -0
- mostlyright/data_harness/local_search/retrieval.py +700 -0
- mostlyright/data_harness/local_search/sealed.py +474 -0
- mostlyright/data_harness/local_search/service.py +784 -0
- mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
- mostlyright/data_harness/nbrender/__init__.py +12 -0
- mostlyright/data_harness/nbrender/chrome.py +359 -0
- mostlyright/data_harness/nbrender/code_body.py +266 -0
- mostlyright/data_harness/nbrender/document.py +407 -0
- mostlyright/data_harness/nbrender/frame.py +275 -0
- mostlyright/data_harness/nbrender/interactive.py +337 -0
- mostlyright/data_harness/nbrender/markdown_body.py +477 -0
- mostlyright/data_harness/nbrender/mr_components.py +134 -0
- mostlyright/data_harness/nbrender/outputs_data.py +595 -0
- mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
- mostlyright/data_harness/nbrender/outputs_source.py +260 -0
- mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
- mostlyright/data_harness/nbrender/outputs_text.py +400 -0
- mostlyright/data_harness/nbrender/parse.py +394 -0
- mostlyright/data_harness/nbrender/status.py +40 -0
- mostlyright/data_harness/nbrender/tokens.py +1295 -0
- mostlyright/data_harness/notebook.py +1710 -0
- mostlyright/data_harness/offline.py +2049 -0
- mostlyright/data_harness/operation_registry.py +1007 -0
- mostlyright/data_harness/operator_setup.py +239 -0
- mostlyright/data_harness/pipeline.py +6428 -0
- mostlyright/data_harness/plan_graph.py +2026 -0
- mostlyright/data_harness/preparation/__init__.py +104 -0
- mostlyright/data_harness/preparation/contracts.py +1017 -0
- mostlyright/data_harness/preparation/engine.py +221 -0
- mostlyright/data_harness/preparation/errors.py +14 -0
- mostlyright/data_harness/preparation/gates.py +751 -0
- mostlyright/data_harness/preparation/joins.py +574 -0
- mostlyright/data_harness/preparation/profile.py +384 -0
- mostlyright/data_harness/preparation/table.py +217 -0
- mostlyright/data_harness/preparation/transforms.py +568 -0
- mostlyright/data_harness/progress_events.py +534 -0
- mostlyright/data_harness/readers/__init__.py +46 -0
- mostlyright/data_harness/readers/containers.py +963 -0
- mostlyright/data_harness/readers/contracts.py +542 -0
- mostlyright/data_harness/readers/delimited.py +257 -0
- mostlyright/data_harness/readers/grib2/__init__.py +33 -0
- mostlyright/data_harness/readers/grib2/admission.py +722 -0
- mostlyright/data_harness/readers/grib2/decode.py +1009 -0
- mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
- mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
- mostlyright/data_harness/readers/json_tabular.py +485 -0
- mostlyright/data_harness/readers/registry.py +514 -0
- mostlyright/data_harness/readers/samples/README.md +110 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples.py +582 -0
- mostlyright/data_harness/readers/spreadsheet.py +803 -0
- mostlyright/data_harness/readers/tabular.py +510 -0
- mostlyright/data_harness/recipe.py +5321 -0
- mostlyright/data_harness/repair/__init__.py +78 -0
- mostlyright/data_harness/repair/adapters.py +274 -0
- mostlyright/data_harness/repair/contracts.py +872 -0
- mostlyright/data_harness/repair/coordinator.py +1099 -0
- mostlyright/data_harness/repair/errors.py +16 -0
- mostlyright/data_harness/review.py +2533 -0
- mostlyright/data_harness/rowset.py +283 -0
- mostlyright/data_harness/serving.py +1975 -0
- mostlyright/data_harness/serving_edge.py +590 -0
- mostlyright/data_harness/serving_http.py +1031 -0
- mostlyright/data_harness/session_probes.py +759 -0
- mostlyright/data_harness/signing.py +101 -0
- mostlyright/data_harness/source_discovery.py +898 -0
- mostlyright/data_harness/sources/__init__.py +209 -0
- mostlyright/data_harness/sources/_adapter_steps.py +213 -0
- mostlyright/data_harness/sources/adapters.py +1214 -0
- mostlyright/data_harness/sources/cadence.py +1428 -0
- mostlyright/data_harness/sources/cadence_emission.py +453 -0
- mostlyright/data_harness/sources/cadence_history.py +546 -0
- mostlyright/data_harness/sources/catalog/__init__.py +17 -0
- mostlyright/data_harness/sources/catalog/admission.py +477 -0
- mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
- mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
- mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
- mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
- mostlyright/data_harness/sources/catalog/channel.py +523 -0
- mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
- mostlyright/data_harness/sources/catalog/contracts.py +825 -0
- mostlyright/data_harness/sources/catalog/coverage.py +137 -0
- mostlyright/data_harness/sources/catalog/delta.py +1340 -0
- mostlyright/data_harness/sources/catalog/embedding.py +532 -0
- mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
- mostlyright/data_harness/sources/catalog/fill.py +3889 -0
- mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
- mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
- mostlyright/data_harness/sources/catalog/gating.py +374 -0
- mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
- mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
- mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
- mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
- mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
- mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
- mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
- mostlyright/data_harness/sources/catalog/health.py +447 -0
- mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
- mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
- mostlyright/data_harness/sources/catalog/neural.py +1618 -0
- mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
- mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
- mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
- mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
- mostlyright/data_harness/sources/catalog/recommend.py +171 -0
- mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
- mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
- mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
- mostlyright/data_harness/sources/catalog/sealed.py +560 -0
- mostlyright/data_harness/sources/catalog/search.py +230 -0
- mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
- mostlyright/data_harness/sources/catalog/update.py +891 -0
- mostlyright/data_harness/sources/collections.py +815 -0
- mostlyright/data_harness/sources/contracts.py +2223 -0
- mostlyright/data_harness/sources/deletion.py +761 -0
- mostlyright/data_harness/sources/fitness.py +162 -0
- mostlyright/data_harness/sources/governance.py +163 -0
- mostlyright/data_harness/sources/hosted.py +173 -0
- mostlyright/data_harness/sources/integration.py +218 -0
- mostlyright/data_harness/sources/range_reader.py +418 -0
- mostlyright/data_harness/sources/registry.py +514 -0
- mostlyright/data_harness/sources/rights_rule.py +59 -0
- mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
- mostlyright/data_harness/sources/sports.py +521 -0
- mostlyright/data_harness/sources/stream.py +524 -0
- mostlyright/data_harness/sources/stream_connector.py +418 -0
- mostlyright/data_harness/sources/stream_recorder.py +1404 -0
- mostlyright/data_harness/studio_boundary.py +2019 -0
- mostlyright/data_harness/thin/__init__.py +37 -0
- mostlyright/data_harness/thin/acquire.py +1137 -0
- mostlyright/data_harness/thin/acquire_cancel.py +579 -0
- mostlyright/data_harness/thin/approvals.py +617 -0
- mostlyright/data_harness/thin/commands.py +406 -0
- mostlyright/data_harness/thin/download.py +194 -0
- mostlyright/data_harness/thin/narrative.py +589 -0
- mostlyright/data_harness/thin/parity.py +1070 -0
- mostlyright/data_harness/thin/propose.py +2759 -0
- mostlyright/data_harness/thin/research.py +1663 -0
- mostlyright/data_harness/thin/router.py +924 -0
- mostlyright/data_harness/thin/runs.py +519 -0
- mostlyright/data_harness/thin/session.py +281 -0
- mostlyright/data_harness/thin/stream.py +501 -0
- mostlyright/data_harness/thin/transport.py +187 -0
- mostlyright/data_harness/thin/vocabulary.py +368 -0
- mostlyright/data_harness/thin/workers.py +164 -0
- mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
- mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
- mostlyright/data_harness/unit_flow.py +927 -0
- mostlyright/data_harness/units.py +572 -0
- mostlyright/data_harness/ux/__init__.py +9 -0
- mostlyright/data_harness/ux/approve.py +485 -0
- mostlyright/data_harness/ux/author_yaml.py +597 -0
- mostlyright/data_harness/ux/cloud_auth.py +447 -0
- mostlyright/data_harness/ux/commands/__init__.py +260 -0
- mostlyright/data_harness/ux/commands/approve.py +136 -0
- mostlyright/data_harness/ux/commands/auth.py +744 -0
- mostlyright/data_harness/ux/commands/author.py +79 -0
- mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
- mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
- mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
- mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
- mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
- mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
- mostlyright/data_harness/ux/commands/deploy.py +134 -0
- mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
- mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
- mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
- mostlyright/data_harness/ux/commands/diff.py +74 -0
- mostlyright/data_harness/ux/commands/index.py +84 -0
- mostlyright/data_harness/ux/commands/inventory.py +47 -0
- mostlyright/data_harness/ux/commands/list_builds.py +143 -0
- mostlyright/data_harness/ux/commands/login.py +63 -0
- mostlyright/data_harness/ux/commands/peek.py +236 -0
- mostlyright/data_harness/ux/commands/plan_check.py +90 -0
- mostlyright/data_harness/ux/commands/preflight.py +97 -0
- mostlyright/data_harness/ux/commands/record.py +107 -0
- mostlyright/data_harness/ux/commands/review_setup.py +47 -0
- mostlyright/data_harness/ux/commands/search.py +440 -0
- mostlyright/data_harness/ux/commands/show.py +61 -0
- mostlyright/data_harness/ux/commands/whoami.py +37 -0
- mostlyright/data_harness/ux/credential_native.py +551 -0
- mostlyright/data_harness/ux/credential_store.py +1055 -0
- mostlyright/data_harness/ux/credentials.py +631 -0
- mostlyright/data_harness/ux/diffing.py +444 -0
- mostlyright/data_harness/ux/headline.py +671 -0
- mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
- mostlyright/data_harness/ux/hosted_run_status.py +619 -0
- mostlyright/data_harness/ux/inventory.py +427 -0
- mostlyright/data_harness/ux/local_review.py +375 -0
- mostlyright/data_harness/ux/login.py +691 -0
- mostlyright/data_harness/ux/path_kind.py +147 -0
- mostlyright/data_harness/ux/peek.py +1000 -0
- mostlyright/data_harness/ux/plain_file.py +178 -0
- mostlyright/data_harness/ux/plan_check.py +311 -0
- mostlyright/data_harness/ux/preflight.py +918 -0
- mostlyright/data_harness/ux/readers.py +1124 -0
- mostlyright/data_harness/ux/remediation.py +2195 -0
- mostlyright/data_harness/ux/render.py +657 -0
- mostlyright/data_harness/ux/workload.py +1077 -0
- mostlyright/data_harness/viewer.py +3713 -0
- mostlyright/data_harness/visual_run/__init__.py +83 -0
- mostlyright/data_harness/visual_run/authoring.py +235 -0
- mostlyright/data_harness/visual_run/contracts.py +673 -0
- mostlyright/data_harness/visual_run/materialize.py +486 -0
- mostlyright/data_harness/visual_run/observations.py +874 -0
- mostlyright/data_harness/visual_run/query.py +259 -0
- mostlyright/data_harness/visual_run/reducer.py +280 -0
- mostlyright/data_harness/visual_run/sdk.py +892 -0
- mostlyright/data_harness/visual_run/store.py +584 -0
- mostlyright/data_harness/visual_run/transport.py +239 -0
- mostlyright/data_harness/watch.py +2999 -0
- mostlyright_data-0.9.0.dist-info/METADATA +607 -0
- mostlyright_data-0.9.0.dist-info/RECORD +314 -0
- mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
- mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""Group C · markdown (C13) and the inline entity-chip hook (F29).
|
|
2
|
+
|
|
3
|
+
Dispatched from ``document``. A hand-written, escaping-safe mini-markdown engine renders
|
|
4
|
+
prose — no markdown pip dependency — and every text run is escaped before any inline markup is
|
|
5
|
+
re-introduced, so a ``<script>`` in the source can never survive. Inline ``$…$`` hands off to
|
|
6
|
+
``code_body.render_math``; ``[[type:id]]`` refs are split by :func:`parse.split_entity_refs` and
|
|
7
|
+
each rendered through :func:`render_entity_chip`.
|
|
8
|
+
|
|
9
|
+
Engine shape. ``render_markdown`` splits the source into blocks (heading / hr / fenced code /
|
|
10
|
+
blockquote / table / list / paragraph) on a line-oriented pass, then renders each block's text
|
|
11
|
+
through :func:`_render_inline`. The inline pass is escape-first: it isolates the constructs whose
|
|
12
|
+
interior must NOT be re-interpreted (code spans, math, links, images), and every remaining plain
|
|
13
|
+
run is escaped via :func:`parse.esc` before emphasis markers and entity chips are re-introduced.
|
|
14
|
+
Raw HTML in the source is therefore never passed through — it is escaped and shown verbatim.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import html
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
from mostlyright.data_harness.nbrender import code_body
|
|
23
|
+
from mostlyright.data_harness.nbrender.parse import (
|
|
24
|
+
Cell,
|
|
25
|
+
EntityRef,
|
|
26
|
+
RenderContext,
|
|
27
|
+
esc,
|
|
28
|
+
heading_text,
|
|
29
|
+
slugify,
|
|
30
|
+
split_entity_refs,
|
|
31
|
+
)
|
|
32
|
+
from mostlyright.data_harness.nbrender.status import is_status_column, render_status
|
|
33
|
+
|
|
34
|
+
# F29 · the four entity types that earn a colored-dot chip; any other type renders as plain text.
|
|
35
|
+
_KNOWN_ENTITY_TYPES = ("station", "dataset", "run", "source")
|
|
36
|
+
|
|
37
|
+
# --- block classifiers ------------------------------------------------------------------------
|
|
38
|
+
# atx heading; deeper levels are capped at h4 because the type scale only defines h1-h4.
|
|
39
|
+
_HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
|
|
40
|
+
# thematic break: three or more of the same -, * or _, spaces allowed between.
|
|
41
|
+
_HR_RE = re.compile(r"^ {0,3}([-*_])(?: *\1){2,} *$")
|
|
42
|
+
# fenced code opener: three or more backticks or tildes, optional info string.
|
|
43
|
+
_FENCE_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})\s*([^`]*)$")
|
|
44
|
+
# list item: optional indent, a bullet or ordered marker, then content.
|
|
45
|
+
_LIST_RE = re.compile(r"^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$")
|
|
46
|
+
# blockquote line: one > and an optional single space.
|
|
47
|
+
_QUOTE_RE = re.compile(r"^ {0,3}>\s?(.*)$")
|
|
48
|
+
# blockquote attribution -> <footer>: an em dash (U+2014) or a double hyphen, then text.
|
|
49
|
+
_FOOTER_RE = re.compile("^(?:\\u2014|--)\\s+(.*)$")
|
|
50
|
+
# a table body cell counts as numeric (right-aligned) when it matches this.
|
|
51
|
+
_NUMERIC_RE = re.compile(r"^[-+]?\$?\d[\d,]*(?:\.\d+)?%?$")
|
|
52
|
+
|
|
53
|
+
# --- inline scanner ---------------------------------------------------------------------------
|
|
54
|
+
# One alternative per construct, each a single named group with no capturing subgroups, so
|
|
55
|
+
# ``match.lastgroup`` names the construct. The URL alternative permits one level of balanced
|
|
56
|
+
# parens so ``(javascript:alert(1))`` is consumed whole and never leaks a trailing ``)``.
|
|
57
|
+
# Interiors are re-parsed by ``_PARTS`` below.
|
|
58
|
+
_URL = r"\((?:[^()]|\([^()]*\))*\)"
|
|
59
|
+
_INLINE_RE = re.compile(
|
|
60
|
+
r"(?P<disp>\$\$.+?\$\$)"
|
|
61
|
+
r"|(?P<imath>\$(?!\s)[^$\n]+?(?<!\s)\$)"
|
|
62
|
+
r"|(?P<code>`[^`\n]+`)"
|
|
63
|
+
r"|(?P<image>!\[[^\]]*\]" + _URL + r")"
|
|
64
|
+
r"|(?P<link>\[[^\]]*\]" + _URL + r")",
|
|
65
|
+
re.DOTALL,
|
|
66
|
+
)
|
|
67
|
+
# alt/text in group 1, the raw parenthesized target in group 2 (first token is the URL).
|
|
68
|
+
_PARTS = re.compile(r"^!?\[([^\]]*)\]\((.*)\)$", re.DOTALL)
|
|
69
|
+
|
|
70
|
+
# standalone-block forms of image and display math (a paragraph that is nothing else).
|
|
71
|
+
_STANDALONE_IMG_RE = re.compile(r"^!\[([^\]]*)\]" + _URL + r"$")
|
|
72
|
+
_STANDALONE_MATH_RE = re.compile(r"^\$\$(.+?)\$\$$", re.DOTALL)
|
|
73
|
+
|
|
74
|
+
# schemes an image src may carry; anything else falls back to the alt caption (no external fetch).
|
|
75
|
+
_SCHEME_RE = re.compile(r"^[a-z][a-z0-9+.\-]*:")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def render_markdown(cell: Cell, ctx: RenderContext) -> str:
|
|
79
|
+
"""C13 · markdown — headings, lists, tables, blockquotes, inline code, links, images, math."""
|
|
80
|
+
return f'<div class="nb-md">{_render_blocks(cell.source, ctx)}</div>'
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def render_entity_chip(ref: EntityRef, ctx: RenderContext) -> str:
|
|
84
|
+
"""F29 · entity chip — inline mono chip with type-colored dot. Static: no popover.
|
|
85
|
+
|
|
86
|
+
The dot conveys the entity type; the chip text is the id. A type outside the four known
|
|
87
|
+
kinds carries no styling class, so it renders as plain escaped text rather than an unstyled
|
|
88
|
+
chip.
|
|
89
|
+
"""
|
|
90
|
+
label = esc(ref.id)
|
|
91
|
+
if ref.type not in _KNOWN_ENTITY_TYPES:
|
|
92
|
+
return label
|
|
93
|
+
return (
|
|
94
|
+
f'<span class="nb-chip nb-chip--{ref.type}"><span class="nb-chip-dot"></span>{label}</span>'
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --- block layer ------------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _render_blocks(source: str, ctx: RenderContext) -> str:
|
|
102
|
+
lines = source.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
103
|
+
parts: list[str] = []
|
|
104
|
+
i = 0
|
|
105
|
+
n = len(lines)
|
|
106
|
+
while i < n:
|
|
107
|
+
line = lines[i]
|
|
108
|
+
if line.strip() == "":
|
|
109
|
+
i += 1
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
fence = _FENCE_RE.match(line)
|
|
113
|
+
if fence:
|
|
114
|
+
i, block = _consume_fence(lines, i, fence.group(1)[0])
|
|
115
|
+
parts.append(block)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
heading = _HEADING_RE.match(line)
|
|
119
|
+
if heading:
|
|
120
|
+
level = min(len(heading.group(1)), 4)
|
|
121
|
+
# Emit a stable id so the A04 outline's ``#slug`` anchors resolve — the slug is derived
|
|
122
|
+
# from the SAME shared helpers chrome.py uses, so the two always agree.
|
|
123
|
+
slug = esc(slugify(heading_text(heading.group(2))))
|
|
124
|
+
parts.append(
|
|
125
|
+
f'<h{level} id="{slug}">{_render_inline(heading.group(2), ctx)}</h{level}>'
|
|
126
|
+
)
|
|
127
|
+
i += 1
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if _HR_RE.match(line):
|
|
131
|
+
parts.append("<hr>")
|
|
132
|
+
i += 1
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if _QUOTE_RE.match(line):
|
|
136
|
+
buf = []
|
|
137
|
+
while i < n and _QUOTE_RE.match(lines[i]):
|
|
138
|
+
buf.append(_QUOTE_RE.match(lines[i]).group(1))
|
|
139
|
+
i += 1
|
|
140
|
+
parts.append(_render_blockquote(buf, ctx))
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if _is_table_header(lines, i):
|
|
144
|
+
header, sep = lines[i], lines[i + 1]
|
|
145
|
+
i += 2
|
|
146
|
+
body = []
|
|
147
|
+
while (
|
|
148
|
+
i < n and lines[i].strip() != "" and "|" in lines[i] and not _starts_block(lines, i)
|
|
149
|
+
):
|
|
150
|
+
body.append(lines[i])
|
|
151
|
+
i += 1
|
|
152
|
+
parts.append(_render_table(header, sep, body, ctx))
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if _LIST_RE.match(line):
|
|
156
|
+
buf = []
|
|
157
|
+
while i < n and lines[i].strip() != "":
|
|
158
|
+
if _LIST_RE.match(lines[i]) or lines[i].startswith((" ", "\t")):
|
|
159
|
+
buf.append(lines[i])
|
|
160
|
+
i += 1
|
|
161
|
+
else:
|
|
162
|
+
break
|
|
163
|
+
parts.append(_render_list(buf, ctx))
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
buf = []
|
|
167
|
+
while i < n and lines[i].strip() != "" and not _starts_block(lines, i):
|
|
168
|
+
buf.append(lines[i])
|
|
169
|
+
i += 1
|
|
170
|
+
parts.append(_render_paragraph(buf, ctx))
|
|
171
|
+
return "".join(parts)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _starts_block(lines: list[str], i: int) -> bool:
|
|
175
|
+
"""True when line ``i`` opens a non-paragraph block, so paragraph/table collection stops."""
|
|
176
|
+
if i >= len(lines):
|
|
177
|
+
return False
|
|
178
|
+
line = lines[i]
|
|
179
|
+
if line.strip() == "":
|
|
180
|
+
return True
|
|
181
|
+
if (
|
|
182
|
+
_HEADING_RE.match(line)
|
|
183
|
+
or _HR_RE.match(line)
|
|
184
|
+
or _FENCE_RE.match(line)
|
|
185
|
+
or _QUOTE_RE.match(line)
|
|
186
|
+
or _LIST_RE.match(line)
|
|
187
|
+
):
|
|
188
|
+
return True
|
|
189
|
+
return _is_table_header(lines, i)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _consume_fence(lines: list[str], i: int, fence_char: str) -> tuple[int, str]:
|
|
193
|
+
close = re.compile(r"^ {0,3}" + re.escape(fence_char) + r"{3,}\s*$")
|
|
194
|
+
i += 1
|
|
195
|
+
buf: list[str] = []
|
|
196
|
+
while i < len(lines):
|
|
197
|
+
if close.match(lines[i]):
|
|
198
|
+
i += 1
|
|
199
|
+
break
|
|
200
|
+
buf.append(lines[i])
|
|
201
|
+
i += 1
|
|
202
|
+
# Fenced code is a paper-ground panel; its interior is escaped verbatim, never re-parsed.
|
|
203
|
+
return i, f'<pre class="nb-md-fence">{esc(chr(10).join(buf))}</pre>'
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _render_paragraph(lines: list[str], ctx: RenderContext) -> str:
|
|
207
|
+
raw = " ".join(part.strip() for part in lines).strip()
|
|
208
|
+
if not raw:
|
|
209
|
+
return ""
|
|
210
|
+
math = _STANDALONE_MATH_RE.match(raw)
|
|
211
|
+
if math:
|
|
212
|
+
return _call_math(math.group(1).strip(), raw, ctx, display=True)
|
|
213
|
+
image = _STANDALONE_IMG_RE.match(raw)
|
|
214
|
+
if image:
|
|
215
|
+
parts = _PARTS.match(raw)
|
|
216
|
+
return _render_image(parts.group(1), _first_url(parts.group(2)), block=True)
|
|
217
|
+
# A paragraph may contain a block-level inline result (a math well, since C14 renders even
|
|
218
|
+
# inline math as a display well). Emit those blocks between <p> runs so no block nests in a
|
|
219
|
+
# <p>; consecutive inline fragments group into one paragraph.
|
|
220
|
+
out: list[str] = []
|
|
221
|
+
buffer: list[str] = []
|
|
222
|
+
|
|
223
|
+
def flush() -> None:
|
|
224
|
+
text = "".join(buffer).strip()
|
|
225
|
+
buffer.clear()
|
|
226
|
+
if text:
|
|
227
|
+
out.append(f"<p>{text}</p>")
|
|
228
|
+
|
|
229
|
+
for is_block, fragment in _inline_segments(raw, ctx):
|
|
230
|
+
if is_block:
|
|
231
|
+
flush()
|
|
232
|
+
out.append(fragment)
|
|
233
|
+
else:
|
|
234
|
+
buffer.append(fragment)
|
|
235
|
+
flush()
|
|
236
|
+
return "".join(out)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _render_blockquote(lines: list[str], ctx: RenderContext) -> str:
|
|
240
|
+
body: list[str] = []
|
|
241
|
+
footer: str | None = None
|
|
242
|
+
for line in lines:
|
|
243
|
+
match = _FOOTER_RE.match(line.strip())
|
|
244
|
+
if match is not None:
|
|
245
|
+
footer = match.group(1)
|
|
246
|
+
else:
|
|
247
|
+
body.append(line)
|
|
248
|
+
text = " ".join(part.strip() for part in body).strip()
|
|
249
|
+
html_out = f"<blockquote>{_render_inline(text, ctx)}"
|
|
250
|
+
if footer is not None:
|
|
251
|
+
html_out += f"<footer>{_render_inline(footer, ctx)}</footer>"
|
|
252
|
+
return html_out + "</blockquote>"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _render_list(lines: list[str], ctx: RenderContext) -> str:
|
|
256
|
+
items: list[dict] = []
|
|
257
|
+
for line in lines:
|
|
258
|
+
match = _LIST_RE.match(line)
|
|
259
|
+
if match is None:
|
|
260
|
+
if items:
|
|
261
|
+
items[-1]["content"] += " " + line.strip()
|
|
262
|
+
continue
|
|
263
|
+
indent = len(match.group(1).expandtabs(4))
|
|
264
|
+
ordered = match.group(2)[0].isdigit()
|
|
265
|
+
content = match.group(3)
|
|
266
|
+
if indent >= 2 and items:
|
|
267
|
+
items[-1]["subs"].append({"ordered": ordered, "content": content})
|
|
268
|
+
else:
|
|
269
|
+
items.append({"ordered": ordered, "content": content, "subs": []})
|
|
270
|
+
if not items:
|
|
271
|
+
return ""
|
|
272
|
+
tag = "ol" if items[0]["ordered"] else "ul"
|
|
273
|
+
out = [f"<{tag}>"]
|
|
274
|
+
for item in items:
|
|
275
|
+
out.append(f"<li>{_render_inline(item['content'], ctx)}")
|
|
276
|
+
subs = item["subs"]
|
|
277
|
+
if subs:
|
|
278
|
+
subtag = "ol" if subs[0]["ordered"] else "ul"
|
|
279
|
+
out.append(f"<{subtag}>")
|
|
280
|
+
for sub in subs:
|
|
281
|
+
out.append(f"<li>{_render_inline(sub['content'], ctx)}</li>")
|
|
282
|
+
out.append(f"</{subtag}>")
|
|
283
|
+
out.append("</li>")
|
|
284
|
+
out.append(f"</{tag}>")
|
|
285
|
+
return "".join(out)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _render_table(header: str, sep: str, body: list[str], ctx: RenderContext) -> str:
|
|
289
|
+
headers = _split_row(header)
|
|
290
|
+
ncol = len(headers)
|
|
291
|
+
rows = [_split_row(line) for line in body]
|
|
292
|
+
numeric = []
|
|
293
|
+
for col in range(ncol):
|
|
294
|
+
vals = [row[col] for row in rows if col < len(row) and row[col] != ""]
|
|
295
|
+
numeric.append(bool(vals) and all(_NUMERIC_RE.match(value) for value in vals))
|
|
296
|
+
head_cells = "".join(f"<th>{_render_inline(cell, ctx)}</th>" for cell in headers)
|
|
297
|
+
body_rows = []
|
|
298
|
+
for row in rows:
|
|
299
|
+
cells = []
|
|
300
|
+
for col in range(ncol):
|
|
301
|
+
value = row[col] if col < len(row) else ""
|
|
302
|
+
klass = ' class="nb-num"' if numeric[col] else ""
|
|
303
|
+
status = render_status(value) if is_status_column(headers[col]) else None
|
|
304
|
+
cells.append(f"<td{klass}>{status or _render_inline(value, ctx)}</td>")
|
|
305
|
+
body_rows.append(f"<tr>{''.join(cells)}</tr>")
|
|
306
|
+
return (
|
|
307
|
+
'<table class="nb-md-table">'
|
|
308
|
+
f"<thead><tr>{head_cells}</tr></thead>"
|
|
309
|
+
f"<tbody>{''.join(body_rows)}</tbody></table>"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _is_table_header(lines: list[str], i: int) -> bool:
|
|
314
|
+
"""A table is a ``|`` header row followed by a ``|`` delimiter row of dashes."""
|
|
315
|
+
if i + 1 >= len(lines):
|
|
316
|
+
return False
|
|
317
|
+
if "|" not in lines[i] or "|" not in lines[i + 1]:
|
|
318
|
+
return False
|
|
319
|
+
return _is_table_sep(lines[i + 1])
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _is_table_sep(line: str) -> bool:
|
|
323
|
+
cells = _split_row(line)
|
|
324
|
+
return bool(cells) and all(re.fullmatch(r":?-{1,}:?", cell) for cell in cells)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _split_row(line: str) -> list[str]:
|
|
328
|
+
text = line.strip()
|
|
329
|
+
if text.startswith("|"):
|
|
330
|
+
text = text[1:]
|
|
331
|
+
if text.endswith("|"):
|
|
332
|
+
text = text[:-1]
|
|
333
|
+
cells: list[str] = []
|
|
334
|
+
current: list[str] = []
|
|
335
|
+
index = 0
|
|
336
|
+
while index < len(text):
|
|
337
|
+
character = text[index]
|
|
338
|
+
if character == "\\" and index + 1 < len(text) and text[index + 1] in {"\\", "|"}:
|
|
339
|
+
current.append(text[index + 1])
|
|
340
|
+
index += 2
|
|
341
|
+
continue
|
|
342
|
+
if character == "|":
|
|
343
|
+
cells.append("".join(current).strip())
|
|
344
|
+
current = []
|
|
345
|
+
else:
|
|
346
|
+
current.append(character)
|
|
347
|
+
index += 1
|
|
348
|
+
cells.append("".join(current).strip())
|
|
349
|
+
return cells
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# --- inline layer -----------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _render_inline(text: str, ctx: RenderContext) -> str:
|
|
356
|
+
"""Inline HTML for a flow context (heading/list/table/blockquote) — segments joined flat."""
|
|
357
|
+
return "".join(fragment for _, fragment in _inline_segments(text, ctx))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _inline_segments(text: str, ctx: RenderContext) -> list[tuple[bool, str]]:
|
|
361
|
+
"""Scan ``text`` into (is_block, html) fragments.
|
|
362
|
+
|
|
363
|
+
Only math can be a block: C14 renders it as a display well. ``is_block`` is decided from the
|
|
364
|
+
returned markup, so if C14 ever yields a truly inline span the fragment stays inline.
|
|
365
|
+
"""
|
|
366
|
+
out: list[tuple[bool, str]] = []
|
|
367
|
+
pos = 0
|
|
368
|
+
for match in _INLINE_RE.finditer(text):
|
|
369
|
+
if match.start() > pos:
|
|
370
|
+
out.append((False, _render_text_run(text[pos : match.start()], ctx)))
|
|
371
|
+
kind = match.lastgroup
|
|
372
|
+
token = match.group()
|
|
373
|
+
if kind == "disp":
|
|
374
|
+
fragment = _call_math(token[2:-2].strip(), token, ctx, display=True)
|
|
375
|
+
out.append((_is_block_markup(fragment), fragment))
|
|
376
|
+
elif kind == "imath":
|
|
377
|
+
fragment = _call_math(token[1:-1].strip(), token, ctx, display=False)
|
|
378
|
+
out.append((_is_block_markup(fragment), fragment))
|
|
379
|
+
elif kind == "code":
|
|
380
|
+
out.append((False, f"<code>{esc(token[1:-1])}</code>"))
|
|
381
|
+
elif kind == "image":
|
|
382
|
+
parts = _PARTS.match(token)
|
|
383
|
+
image = _render_image(parts.group(1), _first_url(parts.group(2)), block=False)
|
|
384
|
+
out.append((False, image))
|
|
385
|
+
elif kind == "link":
|
|
386
|
+
parts = _PARTS.match(token)
|
|
387
|
+
out.append((False, _render_link(parts.group(1), _first_url(parts.group(2)), ctx)))
|
|
388
|
+
pos = match.end()
|
|
389
|
+
if pos < len(text):
|
|
390
|
+
out.append((False, _render_text_run(text[pos:], ctx)))
|
|
391
|
+
return out
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
_BLOCK_TAGS = ("<div", "<p", "<pre", "<table", "<blockquote", "<ul", "<ol")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _is_block_markup(fragment: str) -> bool:
|
|
398
|
+
return fragment.lstrip().startswith(_BLOCK_TAGS)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _first_url(target: str) -> str:
|
|
402
|
+
"""The URL from a link/image target, dropping any optional ``"title"`` after whitespace."""
|
|
403
|
+
target = target.strip()
|
|
404
|
+
return target.split(None, 1)[0] if target else ""
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _render_text_run(text: str, ctx: RenderContext) -> str:
|
|
408
|
+
"""A plain run: split out entity chips, escape the rest, then re-introduce emphasis."""
|
|
409
|
+
out: list[str] = []
|
|
410
|
+
for segment in split_entity_refs(text):
|
|
411
|
+
if isinstance(segment, EntityRef):
|
|
412
|
+
out.append(render_entity_chip(segment, ctx))
|
|
413
|
+
else:
|
|
414
|
+
out.append(_emphasize(esc(segment)))
|
|
415
|
+
return "".join(out)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _emphasize(escaped: str) -> str:
|
|
419
|
+
"""Apply bold/italic to already-escaped text (markers survive escaping; content stays safe)."""
|
|
420
|
+
escaped = re.sub(r"\*\*(\S.*?\S|\S)\*\*", r"<strong>\1</strong>", escaped)
|
|
421
|
+
escaped = re.sub(r"(?<![\w*])__(\S.*?\S|\S)__(?![\w*])", r"<strong>\1</strong>", escaped)
|
|
422
|
+
escaped = re.sub(r"(?<!\*)\*(?!\s)([^*]+?)(?<!\s)\*(?!\*)", r"<em>\1</em>", escaped)
|
|
423
|
+
escaped = re.sub(r"(?<![\w_])_(?!\s)([^_]+?)(?<!\s)_(?![\w_])", r"<em>\1</em>", escaped)
|
|
424
|
+
return escaped
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _render_link(text: str, url: str, ctx: RenderContext) -> str:
|
|
428
|
+
inner = _render_text_run(text, ctx)
|
|
429
|
+
if _href_dangerous(url):
|
|
430
|
+
# Neutralized: drop the anchor, keep the visible (escaped) text.
|
|
431
|
+
return inner
|
|
432
|
+
return f'<a href="{esc(url.strip())}">{inner}</a>'
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _href_dangerous(url: str) -> bool:
|
|
436
|
+
"""True for script-bearing hrefs, after unwrapping entity obfuscation and whitespace.
|
|
437
|
+
|
|
438
|
+
Order matters: decode HTML entities first so ``java	script:`` becomes ``java<tab>script:``,
|
|
439
|
+
then remove the whitespace/control the browser itself strips, leaving a bare ``javascript:``.
|
|
440
|
+
"""
|
|
441
|
+
decoded = html.unescape(url)
|
|
442
|
+
collapsed = re.sub(r"[\x00-\x20]+", "", decoded).lower()
|
|
443
|
+
return (
|
|
444
|
+
collapsed.startswith("javascript:")
|
|
445
|
+
or collapsed.startswith("vbscript:")
|
|
446
|
+
or collapsed.startswith("data:text/html")
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _render_image(alt: str, src: str, *, block: bool) -> str:
|
|
451
|
+
if _img_src_allowed(src):
|
|
452
|
+
return f'<img src="{esc(src.strip())}" alt="{esc(alt)}">'
|
|
453
|
+
# Blocked src (external fetch) or missing src: the alt text becomes the caption.
|
|
454
|
+
caption = esc(alt) if alt.strip() else "image unavailable"
|
|
455
|
+
tag = "div" if block else "span"
|
|
456
|
+
return f'<{tag} class="nb-md-caption">{caption}</{tag}>'
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _img_src_allowed(src: str) -> bool:
|
|
460
|
+
"""Only inline ``data:image/`` blobs and relative paths render; external URLs do not."""
|
|
461
|
+
text = src.strip()
|
|
462
|
+
if not text:
|
|
463
|
+
return False
|
|
464
|
+
lowered = text.lower()
|
|
465
|
+
if lowered.startswith("data:image/"):
|
|
466
|
+
return True
|
|
467
|
+
if _SCHEME_RE.match(lowered) or text.startswith("//"):
|
|
468
|
+
return False
|
|
469
|
+
return True
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _call_math(tex: str, raw: str, ctx: RenderContext, *, display: bool) -> str:
|
|
473
|
+
"""Hand off to C14, falling back to the escaped TeX source if it is unimplemented."""
|
|
474
|
+
try:
|
|
475
|
+
return code_body.render_math(tex, ctx, display=display)
|
|
476
|
+
except NotImplementedError:
|
|
477
|
+
return f"<p>{esc(raw)}</p>" if display else esc(raw)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Group F · Mostly Right components (F27, F28): provenance callout and leakage warning.
|
|
2
|
+
|
|
3
|
+
Dispatched from ``document.render_notebook`` (the F29 entity chip is an inline markdown hook in
|
|
4
|
+
``markdown_body``). Both render only when their metadata block is present: F27 from
|
|
5
|
+
``notebook.metadata.mostlyright.provenance``, F28 from ``notebook.metadata.mostlyright.leakage[]``.
|
|
6
|
+
A leakage entry is placed by ``document`` after the cell it references. Escape every key and value
|
|
7
|
+
from the metadata JSON; the leakage body permits only inline ``code`` spans, nothing else.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from mostlyright.data_harness.nbrender.parse import Notebook, RenderContext, esc
|
|
16
|
+
|
|
17
|
+
# Sanctioned middot separator (U+00B7), used to detect a secondary tail in a provenance value.
|
|
18
|
+
_MIDDOT = "·"
|
|
19
|
+
|
|
20
|
+
# Provenance fields: metadata key -> displayed label. Rendered in this order; a field
|
|
21
|
+
# whose value is absent or empty is skipped so the grid never shows a labelled blank.
|
|
22
|
+
_PROV_FIELDS: tuple[tuple[str, str], ...] = (
|
|
23
|
+
("source", "Source identity"),
|
|
24
|
+
("join", "Join"),
|
|
25
|
+
("revision", "Revision"),
|
|
26
|
+
("cache", "Cache"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# A trailing parenthetical (the short sha) is the secondary part of a value; ``re.DOTALL`` off so a
|
|
30
|
+
# newline never leaks into the match.
|
|
31
|
+
_PAREN_TAIL_RE = re.compile(r"^(.*\S)\s+(\([^()]*\))\s*$")
|
|
32
|
+
|
|
33
|
+
# Inline code spans in a leakage message. Everything outside a span is escaped plain text; the span
|
|
34
|
+
# contents are escaped too, so the only markup this body ever introduces is ``<code>``.
|
|
35
|
+
_INLINE_CODE_RE = re.compile(r"`([^`]*)`")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _prov_mark(size: int = 15) -> str:
|
|
39
|
+
"""The 15px brand mark with every facet the yellow token (F27 header)."""
|
|
40
|
+
return (
|
|
41
|
+
f'<svg width="{size}" height="{size}" viewBox="0 0 26 26" '
|
|
42
|
+
'aria-hidden="true" focusable="false">'
|
|
43
|
+
'<rect x="0" y="0" width="11" height="11" rx="2" fill="var(--nb-yellow)"></rect>'
|
|
44
|
+
'<rect x="15" y="0" width="11" height="11" rx="2" fill="var(--nb-yellow)"></rect>'
|
|
45
|
+
'<rect x="0" y="15" width="11" height="11" rx="2" fill="var(--nb-yellow)"></rect>'
|
|
46
|
+
'<rect x="15" y="15" width="11" height="11" rx="2" fill="var(--nb-yellow)"></rect>'
|
|
47
|
+
"</svg>"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _split_secondary(value: str) -> tuple[str, str]:
|
|
52
|
+
"""Split a value into its primary text and a dimmed secondary tail (short sha or middot suffix).
|
|
53
|
+
|
|
54
|
+
``r7 (a1b2c3d)`` -> ``("r7", "(a1b2c3d)")``; ``warm · 4m ago`` -> ``("warm", "· 4m ago")``; a
|
|
55
|
+
value with neither shape stays whole.
|
|
56
|
+
"""
|
|
57
|
+
match = _PAREN_TAIL_RE.match(value)
|
|
58
|
+
if match:
|
|
59
|
+
return match.group(1), match.group(2)
|
|
60
|
+
sep = f" {_MIDDOT} "
|
|
61
|
+
if sep in value:
|
|
62
|
+
head, _, tail = value.partition(sep)
|
|
63
|
+
return head, f"{_MIDDOT} {tail}"
|
|
64
|
+
return value, ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _prov_field(label: str, value: Any) -> str:
|
|
68
|
+
"""One grid cell: mono key over mono value, with any secondary tail dimmed. Classless wrapper
|
|
69
|
+
so key and value stack inside a single ``nb-prov-grid`` cell."""
|
|
70
|
+
primary, secondary = _split_secondary(str(value))
|
|
71
|
+
val = '<div class="nb-prov-val">' + esc(primary)
|
|
72
|
+
if secondary:
|
|
73
|
+
val += ' <span class="nb-prov-sub">' + esc(secondary) + "</span>"
|
|
74
|
+
val += "</div>"
|
|
75
|
+
return "<div>" + '<div class="nb-prov-key">' + esc(label) + "</div>" + val + "</div>"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def render_provenance(nb: Notebook, ctx: RenderContext) -> str:
|
|
79
|
+
"""F27 · provenance callout — dark card, brand mark, source/join/revision/cache field grid.
|
|
80
|
+
|
|
81
|
+
Renders only when ``notebook.metadata.mostlyright.provenance`` is present; absent -> empty
|
|
82
|
+
string (no card). Fields render in ``_PROV_FIELDS`` order; each value's short-sha / timestamp
|
|
83
|
+
tail is dimmed via ``nb-prov-sub``. Every value is escaped at interpolation.
|
|
84
|
+
"""
|
|
85
|
+
provenance = nb.provenance
|
|
86
|
+
if not provenance:
|
|
87
|
+
return ""
|
|
88
|
+
fields = [
|
|
89
|
+
_prov_field(label, provenance[key])
|
|
90
|
+
for key, label in _PROV_FIELDS
|
|
91
|
+
if provenance.get(key) not in (None, "")
|
|
92
|
+
]
|
|
93
|
+
head = (
|
|
94
|
+
'<div class="nb-prov-head">'
|
|
95
|
+
+ _prov_mark(15)
|
|
96
|
+
+ '<span class="nb-prov-title">Provenance</span>'
|
|
97
|
+
"</div>"
|
|
98
|
+
)
|
|
99
|
+
grid = '<div class="nb-prov-grid">' + "".join(fields) + "</div>"
|
|
100
|
+
return '<div class="nb-prov">' + head + grid + "</div>"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _leak_body(message: str) -> str:
|
|
104
|
+
"""Escape a leakage message, promoting only backtick spans to ``<code>`` (no other markup)."""
|
|
105
|
+
parts: list[str] = []
|
|
106
|
+
cursor = 0
|
|
107
|
+
for match in _INLINE_CODE_RE.finditer(message):
|
|
108
|
+
if match.start() > cursor:
|
|
109
|
+
parts.append(esc(message[cursor : match.start()]))
|
|
110
|
+
parts.append("<code>" + esc(match.group(1)) + "</code>")
|
|
111
|
+
cursor = match.end()
|
|
112
|
+
if cursor < len(message):
|
|
113
|
+
parts.append(esc(message[cursor:]))
|
|
114
|
+
return "".join(parts)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def render_leakage(entry: dict[str, Any], ctx: RenderContext) -> str:
|
|
118
|
+
"""F28 · leakage warning — warn-tinted callout, badge, body with inline code spans only.
|
|
119
|
+
|
|
120
|
+
``document`` places this after the cell the entry's integer ``cell`` index names. The body is
|
|
121
|
+
the entry's ``message``, escaped, with backtick spans rendered as inline ``code`` and nothing
|
|
122
|
+
else — no headings, links, or raw HTML survive.
|
|
123
|
+
"""
|
|
124
|
+
message = entry.get("message")
|
|
125
|
+
body = _leak_body(str(message)) if message else ""
|
|
126
|
+
head = (
|
|
127
|
+
'<div class="nb-leak-head">'
|
|
128
|
+
'<span class="nb-leak-badge">!</span>'
|
|
129
|
+
'<span class="nb-leak-title">Leakage guard</span>'
|
|
130
|
+
"</div>"
|
|
131
|
+
)
|
|
132
|
+
return (
|
|
133
|
+
'<div class="nb-leak">' + head + '<div class="nb-leak-body">' + body + "</div>" + "</div>"
|
|
134
|
+
)
|