hecks 3.10.0 → 3.11.0
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.
- checksums.yaml +4 -4
- data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
- data/exe/hecks +4 -4
- data/lib/hecks/adapters/driven/d1.rb +2 -10
- data/lib/hecks/adapters/driven/heki.adapter +1 -1
- data/lib/hecks/adapters/driven/heki.rb +2 -12
- data/lib/hecks/adapters/driven/lambda.adapter +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +3 -18
- data/lib/hecks/adapters/driven/postgres.rb +4 -11
- data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
- data/lib/hecks/adapters/driven/sqlite.rb +2 -10
- data/lib/hecks/adapters/driving/admission.rb +46 -0
- data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
- data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
- data/lib/hecks/adapters/driving/cli.rb +127 -0
- data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
- data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
- data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
- data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
- data/lib/hecks/adapters/driving/command_request.rb +121 -0
- data/lib/hecks/adapters/driving/handle.rb +214 -0
- data/lib/hecks/adapters/driving/json.rb +136 -0
- data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
- data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
- data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
- data/lib/hecks/adapters/driving/mcp.rb +159 -0
- data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
- data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
- data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
- data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
- data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
- data/lib/hecks/adapters/driving/ruby.rb +83 -0
- data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
- data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
- data/lib/hecks/behaviors/expectations/boot.rb +1 -1
- data/lib/hecks/bench/ruby_runner.rb +1 -1
- data/lib/hecks/bluebook/ask_resolution.rb +130 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
- data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
- data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
- data/lib/hecks/bluebook/assembly/marks.rb +1 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/command.rb +13 -8
- data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
- data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
- data/lib/hecks/bluebook/capabilities.rb +91 -9
- data/lib/hecks/bluebook/domain_port.rb +16 -4
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
- data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
- data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
- data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
- data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
- data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
- data/lib/hecks/bluebook/expression/projection.json +36 -0
- data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
- data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
- data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
- data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
- data/lib/hecks/bluebook/expression/resolver.rb +20 -1
- data/lib/hecks/bluebook/hecksagon.rb +23 -3
- data/lib/hecks/bluebook/lifecycle.rb +22 -3
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
- data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +12 -12
- data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
- data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
- data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
- data/lib/hecks/bluebook/model_check.rb +8 -2
- data/lib/hecks/bluebook/policy.rb +4 -2
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/bluebook/synthesizer.rb +2 -0
- data/lib/hecks/bluebook.rb +1 -0
- data/lib/hecks/cli/command_table.rb +1 -1
- data/lib/hecks/cli/console.rb +2 -1
- data/lib/hecks/cli/document.rb +1 -1
- data/lib/hecks/cli/launcher_words.rb +4 -4
- data/lib/hecks/cli/mcp.rb +4 -4
- data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
- data/lib/hecks/cli/project_cli/templates.rb +5 -5
- data/lib/hecks/cli/project_cli.rb +9 -6
- data/lib/hecks/cli/run.rb +2 -2
- data/lib/hecks/cli/smoke_http/checks.rb +4 -1
- data/lib/hecks/cli/smoke_http/transport.rb +3 -3
- data/lib/hecks/cli/smoke_http.rb +3 -0
- data/lib/hecks/codemod.rb +5 -2
- data/lib/hecks/corpus/routes.rb +7 -0
- data/lib/hecks/corpus/rust.rb +3 -2
- data/lib/hecks/deploy/bluebook/deploy.bluebook +18 -2
- data/lib/hecks/doc/reference/rendering.rb +6 -3
- data/lib/hecks/forms/app/commands.rb +8 -7
- data/lib/hecks/forms/app/queries.rb +2 -1
- data/lib/hecks/forms/app.rb +1 -1
- data/lib/hecks/forms/record_renderer.rb +1 -1
- data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
- data/lib/hecks/fuzzing/failure_triage.rb +2 -1
- data/lib/hecks/fuzzing/mutation/run.rb +11 -4
- data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
- data/lib/hecks/fuzzing/properties/guards.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency.rb +6 -2
- data/lib/hecks/fuzzing/shrinker.rb +3 -1
- data/lib/hecks/fuzzing/value_generator.rb +19 -5
- data/lib/hecks/gate/stages.yml +4 -10
- data/lib/hecks/grammar/evolve.rb +1 -1
- data/lib/hecks/grammar/expression_operators.json +372 -0
- data/lib/hecks/grammar/operators.rb +3 -1
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
- data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
- data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
- data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
- data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
- data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
- data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
- data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
- data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
- data/lib/hecks/hecks/adapters/local_files.rb +2 -2
- data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
- data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
- data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
- data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
- data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
- data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
- data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
- data/lib/hecks/hecks/adapters/terminal.rb +13 -11
- data/lib/hecks/hecks/codebase.behaviors +13 -1
- data/lib/hecks/hecks/codebase.bluebook +239 -5
- data/lib/hecks/hecks/custodian.behaviors +24 -6
- data/lib/hecks/hecks/custodian.bluebook +6 -1227
- data/lib/hecks/hecks/deploy.behaviors +1 -1
- data/lib/hecks/hecks/deploy.world +1 -3
- data/lib/hecks/hecks/hecks.behaviors +1 -1
- data/lib/hecks/hecks/hecks.hecksagon +53 -18
- data/lib/hecks/hecks/hecks.world +10 -10
- data/lib/hecks/hecks/rehearsal.bluebook +477 -0
- data/lib/hecks/hecks/site.behaviors +1 -1
- data/lib/hecks/hecks/site.world +0 -1
- data/lib/hecks/hecks/tooling.bluebook +1238 -0
- data/lib/hecks/indifferent_key.rb +20 -0
- data/lib/hecks/language/adapter.bluebook +13 -3
- data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
- data/lib/hecks/language/bluebook/entity.bluebook +18 -0
- data/lib/hecks/language/bluebook/policy.bluebook +9 -1
- data/lib/hecks/language/bluebook/query.bluebook +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
- data/lib/hecks/language/oidc.json +10 -0
- data/lib/hecks/language/port.bluebook +4 -4
- data/lib/hecks/literal.rb +12 -3
- data/lib/hecks/ports/key_vault.rb +7 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
- data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/shared.rb +5 -2
- data/lib/hecks/projections/model/deviations.rb +2 -2
- data/lib/hecks/projections/model.rb +2 -2
- data/lib/hecks/projections/oidc.rb +13 -1
- data/lib/hecks/projections/shape.rb +5 -2
- data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
- data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
- data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
- data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
- data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
- data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
- data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
- data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
- data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
- data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
- data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
- data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
- data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
- data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
- data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
- data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
- data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
- data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
- data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
- data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
- data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
- data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
- data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
- data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
- data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
- data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
- data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
- data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
- data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
- data/lib/hecks/projections/site/cms_editor.rb +49 -23
- data/lib/hecks/projections/site/role_probe.rb +2 -0
- data/lib/hecks/projector/cli_audience.rb +2 -1
- data/lib/hecks/projector/cli_projector/listing.rb +1 -1
- data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
- data/lib/hecks/projector/docs_projector/closing.rb +1 -1
- data/lib/hecks/projector/docs_projector.rb +1 -1
- data/lib/hecks/projector/exporter/bindings.rb +40 -10
- data/lib/hecks/projector/exporter/capability_values.rb +55 -0
- data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
- data/lib/hecks/projector/target.rb +11 -0
- data/lib/hecks/projector.rb +32 -5
- data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
- data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +6 -2
- data/lib/hecks/release/approval.rb +64 -0
- data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
- data/lib/hecks/router/namespace_installer.rb +16 -16
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/best_effort.rb +22 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
- data/lib/hecks/runtime/command_interpreter.rb +3 -2
- data/lib/hecks/runtime/command_rules/emission.rb +1 -1
- data/lib/hecks/runtime/dispatcher.rb +4 -4
- data/lib/hecks/runtime/entity_element/location.rb +1 -1
- data/lib/hecks/runtime/event.rb +5 -0
- data/lib/hecks/runtime/identity.rb +7 -6
- data/lib/hecks/runtime/interpreting.rb +2 -1
- data/lib/hecks/runtime/invocation_builder.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
- data/lib/hecks/runtime/loader.rb +10 -10
- data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
- data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
- data/lib/hecks/runtime/policy_interpreter.rb +6 -6
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +1 -1
- data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
- data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
- data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
- data/lib/hecks/runtime/registry/merging.rb +1 -1
- data/lib/hecks/runtime/registry/providers.rb +16 -0
- data/lib/hecks/runtime/registry/read_repository.rb +6 -4
- data/lib/hecks/runtime/registry/verification.rb +4 -1
- data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
- data/lib/hecks/runtime/registry.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
- data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter.rb +3 -3
- data/lib/hecks/runtime/tenant_check.rb +5 -4
- data/lib/hecks/runtime/value/coercion.rb +2 -2
- data/lib/hecks/runtime/value/validation.rb +16 -4
- data/lib/hecks/runtime.rb +9 -9
- data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
- data/lib/hecks/rust_build/wasm.rb +1 -1
- data/lib/hecks/rust_build/wasm_browser.rb +1 -1
- data/lib/hecks/rust_build.rb +11 -0
- data/lib/hecks/served_options.rb +53 -0
- data/lib/hecks/site/bluebook/site.bluebook +1 -0
- data/lib/hecks/storehouse/audit.rb +1 -1
- data/lib/hecks/storehouse/dispatching.rb +8 -6
- data/lib/hecks/storehouse/introspection.rb +2 -2
- data/lib/hecks/storehouse/querying.rb +6 -4
- data/lib/hecks/storehouse.rb +4 -4
- data/lib/hecks/tools/comment_style/source_file.rb +4 -1
- data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
- data/lib/hecks/tools/lanes/rows.rb +6 -0
- data/lib/hecks/tools/oidc_manifests.rb +4 -5
- data/lib/hecks/tools/regeneration_run.rb +11 -4
- data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
- data/lib/hecks/tools/tenant_provisioning.rb +2 -2
- data/lib/hecks/tools/tools_doc.rb +4 -4
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +4 -4
- data/lib/hecks.rb +15 -15
- data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
- data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
- data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
- data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
- data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
- data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
- data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
- data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
- data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
- data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
- data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
- data/rust/.gitignore +6 -0
- data/rust/Cargo.toml +6 -0
- data/rust/build/src/binding_pass.rs +103 -0
- data/rust/build/src/cargo_sync.rs +6 -0
- data/rust/build/src/lineage_pass.rs +3 -3
- data/rust/build/src/main.rs +17 -5
- data/rust/build/src/pipeline.rs +48 -5
- data/rust/build/src/seams_pass.rs +123 -0
- data/rust/build/src/translation_pass.rs +167 -0
- data/rust/codegen/src/asks.rs +103 -0
- data/rust/codegen/src/commands.rs +13 -8
- data/rust/codegen/src/constraints.rs +25 -0
- data/rust/codegen/src/domain_generator.rs +2 -0
- data/rust/codegen/src/expr_emitter.rs +13 -0
- data/rust/codegen/src/json_codec.rs +54 -15
- data/rust/codegen/src/main.rs +1 -0
- data/rust/codegen/src/mutations.rs +128 -3
- data/rust/codegen/src/queries.rs +11 -2
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/types.rs +166 -8
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +4 -4
- data/rust/host/src/bin/validate_harness.rs +43 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
- data/rust/host/src/boundary_fuzz/ir.rs +1 -0
- data/rust/host/src/boundary_fuzz/server.rs +1 -1
- data/rust/host/src/checkout.rs +35 -19
- data/rust/host/src/commerce_ir.rs +408 -2
- data/rust/host/src/expr_json.rs +118 -0
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +27 -1
- data/rust/host/src/payments.rs +12 -4
- data/rust/host/src/query_step.rs +22 -4
- data/rust/host/src/reference_validate.rs +653 -108
- data/rust/host/src/resend.rs +2 -0
- data/rust/host/src/server.rs +19 -6
- data/rust/host/src/ui_schema.rs +132 -5
- data/rust/host/src/web/newsletter.rs +7 -8
- data/rust/host/src/web/newsletter_send.rs +38 -13
- data/rust/host/src/web/registrations.rs +229 -26
- data/rust/host/src/web.rs +3 -3
- data/rust/parser/src/emit.rs +38 -6
- data/rust/parser/src/expr/ast_json.rs +7 -1
- data/rust/parser/src/expr/resolver.rs +171 -6
- data/rust/parser/src/ir.rs +7 -0
- data/rust/parser/src/keywords.rs +24 -0
- data/rust/parser/src/main.rs +3 -0
- data/rust/parser/src/parse/aggregate.rs +2 -1
- data/rust/parser/src/parse/chapter.rs +2 -2
- data/rust/parser/src/parse/command.rs +2 -0
- data/rust/parser/src/parse/hecksagon.rs +5 -2
- data/rust/parser/src/parse/lifecycle.rs +81 -0
- data/rust/parser/src/parse/mod.rs +15 -1
- data/rust/parser/src/parse/policy.rs +13 -2
- data/rust/parser/src/parse/value_object.rs +13 -8
- data/rust/parser/src/ruby_value.rs +1 -1
- data/rust/src/exemplar/commands.rs +4 -4
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/json.rs +43 -1
- data/rust/src/kernel/cli.rs +7 -5
- data/rust/src/kernel/dispatch.rs +2 -2
- data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
- data/rust/src/kernel/expr.rs +66 -3
- data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
- data/rust/src/kernel/expression_operators/text.rs +26 -2
- data/rust/src/kernel/json.rs +15 -0
- data/rust/src/kernel/mod.rs +1 -1
- data/rust/src/kernel/orchestrate.rs +54 -5
- data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
- data/rust/src/kernel/pattern.rs +60 -0
- data/rust/src/kernel/reference_lookup.rs +90 -7
- metadata +140 -38
- data/lib/hecks/doors/cli_door/nesting.rb +0 -34
- data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
- data/lib/hecks/doors/cli_door.rb +0 -124
- data/lib/hecks/doors/cli_runner/answers.rb +0 -138
- data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
- data/lib/hecks/doors/cli_runner/settling.rb +0 -115
- data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
- data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
- data/lib/hecks/doors/cli_runner.rb +0 -66
- data/lib/hecks/doors/command_request.rb +0 -119
- data/lib/hecks/doors/handle.rb +0 -208
- data/lib/hecks/doors/json_door.rb +0 -132
- data/lib/hecks/doors/launcher_options.rb +0 -247
- data/lib/hecks/doors/mcp_door/properties.rb +0 -47
- data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
- data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
- data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
- data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
- data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
- data/lib/hecks/doors/mcp_door.rb +0 -156
- data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
- data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
- data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
- data/lib/hecks/doors/mcp_door_scope.rb +0 -201
- data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
- data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
- data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
- data/lib/hecks/doors/ruby_door.rb +0 -80
- data/lib/hecks/doors/usage_cache.rb +0 -139
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
- data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
- data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
- data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The domain's document body and the rich-text editor's document, converted one into the other. This
|
|
4
|
+
// is the only place the two meet: the editor (body_editor.js) holds a Tiptap document while a person
|
|
5
|
+
// writes, and every change is turned back into the body the domain takes and posted as the same
|
|
6
|
+
// dotted-path form fields as any other value object. The editor's own format is never sent or stored.
|
|
7
|
+
//
|
|
8
|
+
// Plain JavaScript with no dependencies and no DOM, so the conversion runs the same in node (where
|
|
9
|
+
// the specs hold it) and in the browser. The two models agree on lists because the editor's lists are
|
|
10
|
+
// flat: each item is a block of its own with an `indent`, as a body's list item has a `depth`, so an
|
|
11
|
+
// item converts to an item. A body list block becomes a run of items, and a run of adjacent items
|
|
12
|
+
// with the same alignment and shift becomes one list block again (its kind is that of its first
|
|
13
|
+
// item, and an item of the other kind carries `list_kind`). Where the editor's document holds
|
|
14
|
+
// something the body cannot, it is reduced to the nearest thing the body holds.
|
|
15
|
+
import { ALIGNS, MARKS, MAX_DEPTH, MAX_INDENT, safeHref } from "../ui/body_model.js";
|
|
16
|
+
|
|
17
|
+
/** The editor's list item node for each kind of body list, and back. */
|
|
18
|
+
const ITEMS = { bullet_list: "flatListItemUnordered", numbered_list: "flatListItemOrdered" };
|
|
19
|
+
const KINDS = { flatListItemUnordered: "bullet_list", flatListItemOrdered: "numbered_list" };
|
|
20
|
+
|
|
21
|
+
const whole = (value, low, high) => (Number.isInteger(Number(value)) && value !== "" && value !== null && value !== undefined ? Math.min(high, Math.max(low, Number(value))) : null);
|
|
22
|
+
|
|
23
|
+
const list = (value) => (Array.isArray(value) ? value : []);
|
|
24
|
+
|
|
25
|
+
const markName = (mark) => (typeof mark === "string" ? mark : mark?.name);
|
|
26
|
+
|
|
27
|
+
/** The layout attributes of a block, as the editor's nodes carry them (`shift` is the block's indent). */
|
|
28
|
+
function layoutAttrs(block) {
|
|
29
|
+
return { align: ALIGNS.includes(block.align) ? block.align : null, shift: whole(block.indent, 0, MAX_INDENT) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The inline nodes of `spans`: text carrying its marks and address, and a hard break for a line break. */
|
|
33
|
+
export function spansToInline(spans) {
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const span of list(spans)) {
|
|
36
|
+
const names = MARKS.filter((name) => list(span?.marks).map(markName).includes(name));
|
|
37
|
+
const href = safeHref(span?.href);
|
|
38
|
+
const marks = [...names.map((type) => ({ type })), ...(href ? [{ type: "link", attrs: { href } }] : [])];
|
|
39
|
+
String(span?.text ?? "").split("\n").forEach((part, at) => {
|
|
40
|
+
if (at > 0) out.push({ type: "hardBreak" });
|
|
41
|
+
if (part !== "") out.push({ type: "text", text: part, ...(marks.length ? { marks } : {}) });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const paragraph = (spans, attrs = {}) => ({ type: "paragraph", attrs, content: spansToInline(spans) });
|
|
48
|
+
|
|
49
|
+
/** The items of a body list block as flat items, numbering ordered ones as the editor does. */
|
|
50
|
+
function itemsToNodes(block) {
|
|
51
|
+
const kind = block.kind;
|
|
52
|
+
const layout = layoutAttrs(block);
|
|
53
|
+
const counts = [];
|
|
54
|
+
let above = -1;
|
|
55
|
+
return list(block.items).map((item) => {
|
|
56
|
+
const depth = Math.min(whole(item?.depth, 0, MAX_DEPTH) ?? 0, above + 1);
|
|
57
|
+
above = depth;
|
|
58
|
+
counts.length = depth + 1;
|
|
59
|
+
const own = item?.list_kind in ITEMS ? item.list_kind : kind;
|
|
60
|
+
counts[depth] = own === "numbered_list" ? (counts[depth] ?? 0) + 1 : 0;
|
|
61
|
+
const attrs = { ...layout, ...(depth > 0 ? { indent: depth } : {}), ...(own === "numbered_list" ? { counter: counts[depth] } : {}) };
|
|
62
|
+
return { type: ITEMS[own], attrs, content: spansToInline(item?.spans) };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function blockToNodes(block) {
|
|
67
|
+
const kind = block?.kind;
|
|
68
|
+
if (kind === "divider") return [{ type: "horizontalRule" }];
|
|
69
|
+
if (kind === "image") return [{ type: "image", attrs: { mediaRef: block.media_ref ?? "", alt: block.alt ?? "", caption: block.caption ?? "" } }];
|
|
70
|
+
if (kind in ITEMS) return itemsToNodes(block);
|
|
71
|
+
const layout = layoutAttrs(block ?? {});
|
|
72
|
+
if (kind === "quote") return [{ type: "blockquote", attrs: layout, content: [paragraph(block.spans)] }];
|
|
73
|
+
if (kind === "heading") return [{ type: "heading", attrs: { level: whole(block.level, 1, 4) ?? 2, ...layout }, content: spansToInline(block.spans) }];
|
|
74
|
+
return [paragraph(block?.spans, layout)];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The editor's document for a body: a body with no blocks is one empty paragraph, as the editor needs. */
|
|
78
|
+
export function bodyToDoc(body) {
|
|
79
|
+
const content = list(body?.blocks).flatMap(blockToNodes);
|
|
80
|
+
return { type: "doc", content: content.length ? content : [{ type: "paragraph", attrs: {} }] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The mark names of an inline node, in the body's own order, and its address. */
|
|
84
|
+
function marksOf(node) {
|
|
85
|
+
const held = list(node.marks);
|
|
86
|
+
const names = MARKS.filter((name) => held.some((mark) => mark.type === name));
|
|
87
|
+
const link = held.find((mark) => mark.type === "link");
|
|
88
|
+
return { marks: names.map((name) => ({ name })), href: safeHref(link?.attrs?.href) ?? undefined };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The spans of inline nodes: adjacent spans of the same marks and address become one, empty ones go. */
|
|
92
|
+
export function inlineToSpans(nodes) {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const node of list(nodes)) {
|
|
95
|
+
const span = node.type === "hardBreak" ? { text: "\n", marks: [] } : node.type === "text" ? { text: node.text ?? "", ...marksOf(node) } : null;
|
|
96
|
+
if (!span || span.text === "") continue;
|
|
97
|
+
const last = out[out.length - 1];
|
|
98
|
+
const same = last && last.text !== "\n" && span.text !== "\n" && last.href === span.href && JSON.stringify(last.marks) === JSON.stringify(span.marks);
|
|
99
|
+
if (same) last.text += span.text;
|
|
100
|
+
else out.push({ text: span.text, marks: span.marks, ...(span.href ? { href: span.href } : {}) });
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const withLayout = (node) => {
|
|
106
|
+
const attrs = node.attrs ?? {};
|
|
107
|
+
const shift = whole(attrs.shift, 0, MAX_INDENT);
|
|
108
|
+
return { ...(ALIGNS.includes(attrs.align) ? { align: attrs.align } : {}), ...(shift !== null && shift > 0 ? { indent: shift } : {}) };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const layoutKey = (node) => JSON.stringify(withLayout(node));
|
|
112
|
+
|
|
113
|
+
/** Groups the run of flat items that starts `nodes[from]` into one list block; returns it and where it ends. */
|
|
114
|
+
function listBlock(nodes, from) {
|
|
115
|
+
const first = nodes[from];
|
|
116
|
+
const kind = KINDS[first.type];
|
|
117
|
+
const items = [];
|
|
118
|
+
let at = from;
|
|
119
|
+
while (at < nodes.length && nodes[at].type in KINDS && layoutKey(nodes[at]) === layoutKey(first)) {
|
|
120
|
+
const node = nodes[at];
|
|
121
|
+
const above = items.length === 0 ? -1 : items[items.length - 1].depth;
|
|
122
|
+
const depth = Math.min(whole(node.attrs?.indent, 0, MAX_DEPTH) ?? 0, above + 1);
|
|
123
|
+
items.push({ spans: inlineToSpans(node.content), depth, ...(KINDS[node.type] !== kind ? { list_kind: KINDS[node.type] } : {}) });
|
|
124
|
+
at += 1;
|
|
125
|
+
}
|
|
126
|
+
return [{ kind, ...withLayout(first), items }, at];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function blockFromNode(node) {
|
|
130
|
+
switch (node.type) {
|
|
131
|
+
case "horizontalRule":
|
|
132
|
+
return [{ kind: "divider" }];
|
|
133
|
+
case "image":
|
|
134
|
+
return [{ kind: "image", ...(node.attrs?.mediaRef ? { media_ref: node.attrs.mediaRef } : {}), ...(node.attrs?.alt ? { alt: node.attrs.alt } : {}), ...(node.attrs?.caption ? { caption: node.attrs.caption } : {}) }];
|
|
135
|
+
case "heading":
|
|
136
|
+
return [{ kind: "heading", level: whole(node.attrs?.level, 1, 4) ?? 2, ...withLayout(node), spans: inlineToSpans(node.content) }];
|
|
137
|
+
case "blockquote":
|
|
138
|
+
return list(node.content).filter((child) => child.type === "paragraph").map((child) => ({ kind: "quote", ...withLayout(node), spans: inlineToSpans(child.content) }));
|
|
139
|
+
default:
|
|
140
|
+
return [{ kind: "paragraph", ...withLayout(node), spans: inlineToSpans(node.content) }];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const emptyParagraph = (block) => block.kind === "paragraph" && block.spans.length === 0 && !("align" in block) && !("indent" in block);
|
|
145
|
+
|
|
146
|
+
/** The body for an editor document. Trailing empty paragraphs, which the editor keeps for typing, are not blocks. */
|
|
147
|
+
export function docToBody(doc) {
|
|
148
|
+
const nodes = list(doc?.content);
|
|
149
|
+
const blocks = [];
|
|
150
|
+
for (let at = 0; at < nodes.length;) {
|
|
151
|
+
if (nodes[at].type in KINDS) {
|
|
152
|
+
const [made, next] = listBlock(nodes, at);
|
|
153
|
+
blocks.push(made);
|
|
154
|
+
at = next;
|
|
155
|
+
} else {
|
|
156
|
+
blocks.push(...blockFromNode(nodes[at]));
|
|
157
|
+
at += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
while (blocks.length > 0 && emptyParagraph(blocks[blocks.length - 1])) blocks.pop();
|
|
161
|
+
return { blocks };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* What loading a body into the editor changes, so the editor can say so: a list item more than one
|
|
166
|
+
* level below the one above it is moved up, because the editor's lists cannot skip a level.
|
|
167
|
+
*/
|
|
168
|
+
export function reductions(body) {
|
|
169
|
+
const notes = [];
|
|
170
|
+
for (const block of list(body?.blocks)) {
|
|
171
|
+
const depths = list(block.items).map((item) => whole(item?.depth, 0, MAX_DEPTH) ?? 0);
|
|
172
|
+
if (depths.some((depth, at) => depth > (at === 0 ? 0 : depths[at - 1] + 1))) notes.push("a list item more than one level below the one above it was moved up to one level below");
|
|
173
|
+
}
|
|
174
|
+
return notes;
|
|
175
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The writing surface for a structured document body. A Tiptap editor (ProseMirror underneath) holds
|
|
4
|
+
// the document while a person writes; every change is converted to the domain's own body by
|
|
5
|
+
// body_doc.js and written into the form's hidden inputs, named by dotted path like every other value
|
|
6
|
+
// object, so the editor's format is never posted and a browser with no script posts the body as it
|
|
7
|
+
// was. Pasted and dropped HTML first goes through the same reducer the server uses
|
|
8
|
+
// (`htmlToBodyWithNotes`), which says what it reduced.
|
|
9
|
+
//
|
|
10
|
+
// Lists are flat: each item is a block with an `indent`, as a body's item has a `depth`
|
|
11
|
+
// (tiptap-extension-flat-list). Two things differ from the stock extensions because the page is
|
|
12
|
+
// served under a strict content security policy: no style element is injected (the stylesheet has
|
|
13
|
+
// ProseMirror's rules), and no style attribute is written (alignment and indent are data
|
|
14
|
+
// attributes the stylesheet reads).
|
|
15
|
+
import { Editor, Extension } from "@tiptap/core";
|
|
16
|
+
import Blockquote from "@tiptap/extension-blockquote";
|
|
17
|
+
import Image from "@tiptap/extension-image";
|
|
18
|
+
import { Placeholder } from "@tiptap/extensions";
|
|
19
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
20
|
+
import { FlatListCore, FlatListOrdered, FlatListUnordered } from "tiptap-extension-flat-list";
|
|
21
|
+
|
|
22
|
+
import { ALIGNS, MAX_DEPTH, MAX_INDENT, bodyToFields, bodyToHtml, fieldsToInputs, safeHref } from "../ui/body_model.js";
|
|
23
|
+
import { htmlToBodyWithNotes } from "../ui/body_parse.js";
|
|
24
|
+
import { startDraft } from "./autosave.js";
|
|
25
|
+
import { bodyToDoc, docToBody } from "./body_doc.js";
|
|
26
|
+
|
|
27
|
+
const loadPicker = __PICKER_LOADER__;
|
|
28
|
+
|
|
29
|
+
const icon = (name) => `<svg class="icon" aria-hidden="true" focusable="false"><use href="#i-${name}"/></svg>`;
|
|
30
|
+
|
|
31
|
+
const make = (tag, props = {}, ...children) => {
|
|
32
|
+
const element = Object.assign(document.createElement(tag), props);
|
|
33
|
+
element.append(...children);
|
|
34
|
+
return element;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const BLOCKS = [
|
|
38
|
+
["paragraph", "Paragraph"], ["h1", "Heading 1"], ["h2", "Heading 2"], ["h3", "Heading 3"], ["h4", "Heading 4"],
|
|
39
|
+
["quote", "Quote"], ["bullet", "Bulleted list"], ["numbered", "Numbered list"],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** What the faint label in the left margin of the focused block says: short, because the margin is narrow. */
|
|
43
|
+
const SHORT = { paragraph: "Text", h1: "H1", h2: "H2", h3: "H3", h4: "H4", quote: "Quote", bullet: "List", numbered: "List" };
|
|
44
|
+
|
|
45
|
+
const ITEMS = ["flatListItemUnordered", "flatListItemOrdered"];
|
|
46
|
+
|
|
47
|
+
// Each group of the toolbar: [action, icon, tooltip].
|
|
48
|
+
const GROUPS = [
|
|
49
|
+
[["bold", "bold", "Bold (Ctrl+B)"], ["italic", "italic", "Italic (Ctrl+I)"], ["underline", "underline", "Underline (Ctrl+U)"],
|
|
50
|
+
["strike", "strike", "Strike-through (Ctrl+Shift+S)"], ["code", "code", "Code (Ctrl+E)"]],
|
|
51
|
+
[["link", "link", "Link (Ctrl+K)"], ["unlink", "unlink", "Remove link"]],
|
|
52
|
+
[["bullet", "list-bullets", "Bulleted list"], ["numbered", "list-numbers", "Numbered list"], ["outdent", "outdent", "Move out (Shift+Tab in a list)"],
|
|
53
|
+
["indent", "indent", "Move in (Tab in a list)"]],
|
|
54
|
+
[["left", "align-left", "Align left"], ["center", "align-center", "Align centre"], ["right", "align-right", "Align right"], ["justify", "align-justify", "Justify"]],
|
|
55
|
+
[["image", "image", "Insert a picture"], ["divider", "divider", "Insert a divider"], ["break", "line-break", "Line break (Shift+Enter)"]],
|
|
56
|
+
[["undo", "undo", "Undo (Ctrl+Z)"], ["redo", "redo", "Redo (Ctrl+Shift+Z)"]],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** Alignment and the block's own indent, as data attributes every block-level node carries. */
|
|
60
|
+
const Layout = Extension.create({
|
|
61
|
+
name: "layout",
|
|
62
|
+
addGlobalAttributes() {
|
|
63
|
+
const pick = (attribute, name) => ({
|
|
64
|
+
default: null,
|
|
65
|
+
parseHTML: (element) => element.getAttribute(`data-${attribute}`),
|
|
66
|
+
renderHTML: (attributes) => (attributes[name] ? { [`data-${attribute}`]: String(attributes[name]) } : {}),
|
|
67
|
+
});
|
|
68
|
+
return [{
|
|
69
|
+
types: ["paragraph", "heading", "blockquote", ...ITEMS],
|
|
70
|
+
attributes: { align: pick("align", "align"), shift: pick("indent", "shift") },
|
|
71
|
+
}];
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/** A list item sits no deeper than the body allows. */
|
|
76
|
+
const DepthCap = Extension.create({
|
|
77
|
+
name: "depthCap",
|
|
78
|
+
priority: 300,
|
|
79
|
+
addKeyboardShortcuts() {
|
|
80
|
+
return { Tab: ({ editor }) => ITEMS.includes(editor.state.selection.$from.parent.type.name) && (editor.state.selection.$from.parent.attrs.indent ?? 0) >= MAX_DEPTH };
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** A flat item drawn as a one-item list with its indent as an attribute, not a style. */
|
|
85
|
+
const bullet = FlatListUnordered.extend({
|
|
86
|
+
renderHTML({ node }) {
|
|
87
|
+
const indent = node.attrs.indent || null;
|
|
88
|
+
return ["ul", { "data-list-indent": indent }, ["li", { "data-list-indent": indent }, 0]];
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const numbered = FlatListOrdered.extend({
|
|
93
|
+
renderHTML({ node }) {
|
|
94
|
+
const indent = node.attrs.indent || null;
|
|
95
|
+
return ["ol", { start: node.attrs.counter, "data-list-indent": indent }, ["li", { "data-list-indent": indent }, 0]];
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/** A picture is its media key, alt text and caption; it is drawn from the picture store when there is one. */
|
|
100
|
+
function picture(base) {
|
|
101
|
+
const attributes = (element) => ({
|
|
102
|
+
mediaRef: element.getAttribute("data-media-ref") ?? "",
|
|
103
|
+
alt: element.getAttribute("data-alt") ?? "",
|
|
104
|
+
caption: element.getAttribute("data-caption") ?? "",
|
|
105
|
+
});
|
|
106
|
+
return Image.extend({
|
|
107
|
+
addAttributes: () => ({ mediaRef: { default: "" }, alt: { default: "" }, caption: { default: "" } }),
|
|
108
|
+
parseHTML: () => [{ tag: "figure[data-media-ref]", getAttrs: attributes }, { tag: "img[data-media-ref]", getAttrs: attributes }],
|
|
109
|
+
renderHTML({ node }) {
|
|
110
|
+
const held = { "data-media-ref": node.attrs.mediaRef, "data-alt": node.attrs.alt, "data-caption": node.attrs.caption };
|
|
111
|
+
if (base) return ["img", { ...held, src: `${base}/${encodeURIComponent(node.attrs.mediaRef)}`, alt: node.attrs.alt }];
|
|
112
|
+
return ["span", { ...held, class: "body-media" }, `image ${node.attrs.mediaRef}${node.attrs.alt ? ` ${node.attrs.alt}` : ""}`];
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const kindOf = (editor) => {
|
|
118
|
+
for (const level of [1, 2, 3, 4]) if (editor.isActive("heading", { level })) return `h${level}`;
|
|
119
|
+
if (editor.isActive("blockquote")) return "quote";
|
|
120
|
+
if (editor.isActive("flatListItemUnordered")) return "bullet";
|
|
121
|
+
if (editor.isActive("flatListItemOrdered")) return "numbered";
|
|
122
|
+
return "paragraph";
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** The top-level block holding the caret: its position and node. */
|
|
126
|
+
function topBlock(editor) {
|
|
127
|
+
const { $from } = editor.state.selection;
|
|
128
|
+
if ($from.depth < 1) return null;
|
|
129
|
+
const pos = $from.before(1);
|
|
130
|
+
return { pos, node: editor.state.doc.nodeAt(pos) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const wordsIn = (editor) => (editor.state.doc.textBetween(0, editor.state.doc.content.size, " ").match(/\S+/g) ?? []).length;
|
|
134
|
+
|
|
135
|
+
function mount(root, base) {
|
|
136
|
+
if (root.dataset.mounted) return;
|
|
137
|
+
root.dataset.mounted = "true";
|
|
138
|
+
const fields = root.querySelector("[data-fields]");
|
|
139
|
+
const initial = JSON.parse(root.dataset.body || '{"blocks":[]}');
|
|
140
|
+
const frame = make("div", { className: "relative" });
|
|
141
|
+
const bar = make("div", { className: "sticky top-14 z-10 flex flex-nowrap items-center gap-1 overflow-x-auto border border-base-300 bg-base-100 p-1 sm:flex-wrap sm:overflow-visible", role: "toolbar" });
|
|
142
|
+
bar.setAttribute("aria-label", `Formatting for ${root.dataset.label}`);
|
|
143
|
+
// Inside a card of a list of blocks the writing is a part of the card, so its page is shorter.
|
|
144
|
+
const nested = root.closest("[data-block-list]") !== null;
|
|
145
|
+
const page = make("div", { className: `relative ${nested ? "min-h-32 px-4 py-4" : "min-h-96 px-5 py-8 sm:px-12 sm:py-12"} border border-t-0 border-base-300 bg-base-100 focus-within:border-primary` });
|
|
146
|
+
const surface = make("div");
|
|
147
|
+
const label = make("div", { className: "pointer-events-none absolute left-1 hidden w-10 overflow-hidden text-[0.6875rem] font-medium leading-tight text-muted sm:block" });
|
|
148
|
+
label.setAttribute("aria-hidden", "true");
|
|
149
|
+
const notes = make("p", { className: "mt-2 text-xs text-muted" });
|
|
150
|
+
notes.setAttribute("role", "status");
|
|
151
|
+
const status = make("p", { className: "mt-2 flex justify-between gap-4 text-xs text-muted" }, make("span", { textContent: "0 words" }), make("span"));
|
|
152
|
+
page.append(surface, label);
|
|
153
|
+
frame.append(bar, page);
|
|
154
|
+
root.querySelector("[data-fields]").before(frame, notes, status);
|
|
155
|
+
root.querySelector("[data-body-fallback]").hidden = true;
|
|
156
|
+
root.querySelector("[data-needs-script]").hidden = true;
|
|
157
|
+
|
|
158
|
+
const draft = root.hasAttribute("data-draft") ? startDraft(root) : null;
|
|
159
|
+
let editor;
|
|
160
|
+
let changed = false;
|
|
161
|
+
const buttons = new Map();
|
|
162
|
+
const say = (messages) => {
|
|
163
|
+
notes.textContent = messages.length ? `Pasted content was adjusted to fit: ${messages.join("; ")}.` : "";
|
|
164
|
+
};
|
|
165
|
+
const sync = () => {
|
|
166
|
+
const body = docToBody(editor.getJSON());
|
|
167
|
+
fields.innerHTML = fieldsToInputs(bodyToFields(body, root.dataset.name));
|
|
168
|
+
const words = wordsIn(editor);
|
|
169
|
+
status.firstChild.textContent = `${words} ${words === 1 ? "word" : "words"}`;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const toolbar = () => {
|
|
173
|
+
const select = make("select", { className: "select select-ghost select-sm w-36" });
|
|
174
|
+
select.setAttribute("aria-label", "Block type");
|
|
175
|
+
for (const [value, text] of BLOCKS) select.append(make("option", { value, textContent: text }));
|
|
176
|
+
select.addEventListener("change", () => setBlock(select.value));
|
|
177
|
+
bar.append(make("div", { className: "flex shrink-0 items-center border-r border-base-300 pr-2" }, select));
|
|
178
|
+
buttons.set("select", select);
|
|
179
|
+
for (const group of GROUPS) {
|
|
180
|
+
const holder = make("div", { className: "flex shrink-0 items-center gap-0.5 border-r border-base-300 pr-2 last:border-r-0" });
|
|
181
|
+
for (const [action, glyph, tip] of group) {
|
|
182
|
+
const button = make("button", { type: "button", className: "btn btn-ghost btn-square btn-sm" });
|
|
183
|
+
button.dataset.action = action;
|
|
184
|
+
button.setAttribute("aria-label", tip.replace(/ \(.*\)$/, ""));
|
|
185
|
+
button.setAttribute("aria-pressed", "false");
|
|
186
|
+
button.innerHTML = icon(glyph);
|
|
187
|
+
const tooltip = make("div", { className: "tooltip tooltip-bottom" }, button);
|
|
188
|
+
tooltip.dataset.tip = tip;
|
|
189
|
+
holder.append(tooltip);
|
|
190
|
+
buttons.set(action, button);
|
|
191
|
+
}
|
|
192
|
+
bar.append(holder);
|
|
193
|
+
}
|
|
194
|
+
const all = [...bar.querySelectorAll("button, select")];
|
|
195
|
+
all.forEach((control, at) => control.setAttribute("tabindex", at === 0 ? "0" : "-1"));
|
|
196
|
+
bar.addEventListener("keydown", (event) => {
|
|
197
|
+
if (!["ArrowRight", "ArrowLeft", "Home", "End"].includes(event.key) || event.target.tagName === "INPUT") return;
|
|
198
|
+
const at = all.indexOf(event.target);
|
|
199
|
+
if (at < 0 || (event.target.tagName === "SELECT" && event.key !== "Home" && event.key !== "End")) return;
|
|
200
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? all.length - 1 : (at + (event.key === "ArrowRight" ? 1 : -1) + all.length) % all.length;
|
|
201
|
+
all.forEach((control, index) => control.setAttribute("tabindex", index === next ? "0" : "-1"));
|
|
202
|
+
all[next].focus();
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
});
|
|
205
|
+
bar.addEventListener("mousedown", (event) => { if (event.target.closest("button")) event.preventDefault(); });
|
|
206
|
+
bar.addEventListener("click", (event) => {
|
|
207
|
+
const button = event.target.closest("button[data-action]");
|
|
208
|
+
if (button) act(button.dataset.action);
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const setBlock = (kind) => {
|
|
213
|
+
const chain = editor.chain().focus();
|
|
214
|
+
if (kind === "bullet") chain.toggleFlatListItem("flatListItemUnordered").run();
|
|
215
|
+
else if (kind === "numbered") chain.toggleFlatListItem("flatListItemOrdered").run();
|
|
216
|
+
else if (kind === "quote") (editor.isActive("blockquote") ? chain.lift("blockquote") : chain.clearNodes().setBlockquote()).run();
|
|
217
|
+
else if (kind === "paragraph") chain.clearNodes().setParagraph().run();
|
|
218
|
+
else chain.clearNodes().setHeading({ level: Number(kind.slice(1)) }).run();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const setLayout = (change) => {
|
|
222
|
+
const found = topBlock(editor);
|
|
223
|
+
if (!found) return;
|
|
224
|
+
editor.view.dispatch(editor.state.tr.setNodeMarkup(found.pos, null, { ...found.node.attrs, ...change(found.node.attrs) }));
|
|
225
|
+
editor.view.focus();
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const bumpIndent = (step) => {
|
|
229
|
+
if (ITEMS.includes(editor.state.selection.$from.parent.type.name)) {
|
|
230
|
+
const depth = editor.state.selection.$from.parent.attrs.indent ?? 0;
|
|
231
|
+
if (step > 0) return depth < MAX_DEPTH && editor.chain().focus().indentFlatListItem().run();
|
|
232
|
+
return editor.chain().focus().dedentFlatListItem(true).run();
|
|
233
|
+
}
|
|
234
|
+
return setLayout((attrs) => ({ shift: Math.min(MAX_INDENT, Math.max(0, Number(attrs.shift ?? 0) + step)) || null }));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const align = (value) => setLayout((attrs) => ({ align: attrs.align === value ? null : value }));
|
|
238
|
+
|
|
239
|
+
const popover = (title, inputs, onApply, extras = []) => {
|
|
240
|
+
frame.querySelector("[data-popover]")?.remove();
|
|
241
|
+
const panel = make("div", { className: "absolute left-2 right-2 top-full z-20 mt-1 grid max-w-lg gap-3 rounded-box border border-base-300 bg-base-100 p-4 shadow-xl" });
|
|
242
|
+
panel.dataset.popover = "";
|
|
243
|
+
panel.setAttribute("role", "group");
|
|
244
|
+
panel.setAttribute("aria-label", title);
|
|
245
|
+
const controls = inputs.map(([key, text, value]) => {
|
|
246
|
+
const input = make("input", { type: "text", name: key, value, className: "input w-full" });
|
|
247
|
+
panel.append(make("label", { className: "grid gap-1 font-semibold" }, text, input));
|
|
248
|
+
return [key, input];
|
|
249
|
+
});
|
|
250
|
+
const problem = make("div", { className: "alert alert-error alert-soft", hidden: true });
|
|
251
|
+
problem.setAttribute("role", "alert");
|
|
252
|
+
const button = (text, run, className) => make("button", { type: "button", className, textContent: text, onclick: run });
|
|
253
|
+
const close = () => { panel.remove(); editor.chain().focus().run(); };
|
|
254
|
+
const apply = () => {
|
|
255
|
+
const message = onApply(Object.fromEntries(controls.map(([key, input]) => [key, input.value])));
|
|
256
|
+
if (message) { problem.textContent = message; problem.hidden = false; } else close();
|
|
257
|
+
};
|
|
258
|
+
panel.append(problem, make("div", { className: "flex flex-wrap gap-2" }, button("Apply", apply, "btn btn-neutral"), ...extras.map(([text, run]) => button(text, () => { run(); close(); }, "btn")), button("Cancel", close, "btn btn-ghost")));
|
|
259
|
+
panel.addEventListener("keydown", (event) => {
|
|
260
|
+
if (event.key === "Escape") { event.preventDefault(); close(); }
|
|
261
|
+
if (event.key === "Enter" && event.target.tagName === "INPUT") { event.preventDefault(); apply(); }
|
|
262
|
+
});
|
|
263
|
+
frame.append(panel);
|
|
264
|
+
panel.style.top = `${bar.offsetHeight}px`;
|
|
265
|
+
controls[0][1].focus();
|
|
266
|
+
controls[0][1].select();
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const link = () => {
|
|
270
|
+
const held = editor.getAttributes("link").href;
|
|
271
|
+
const unlink = () => editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
272
|
+
const apply = ({ href }) => {
|
|
273
|
+
const typed = href.trim();
|
|
274
|
+
if (typed === "") return unlink();
|
|
275
|
+
const safe = safeHref(typed);
|
|
276
|
+
if (!safe) return "That address is not allowed: use a path, http://, https://, mailto: or tel:.";
|
|
277
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: safe }).run();
|
|
278
|
+
};
|
|
279
|
+
popover("Link", [["href", "Link address: a path, http://, https://, mailto: or tel:", held ?? "https://"]], apply, held ? [["Remove link", unlink]] : []);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const putImage = (attrs, at) => {
|
|
283
|
+
if (at === undefined) editor.chain().focus().insertContent({ type: "image", attrs }).run();
|
|
284
|
+
else editor.view.dispatch(editor.state.tr.setNodeMarkup(at, null, attrs));
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const askImage = (current, at) => {
|
|
288
|
+
if (loadPicker && root.dataset.media) {
|
|
289
|
+
loadPicker().then(({ openPicker }) => openPicker(frame, bar, {
|
|
290
|
+
url: root.dataset.media,
|
|
291
|
+
current,
|
|
292
|
+
onPick: (key, alt, caption) => { putImage({ mediaRef: key, alt, caption }, at); editor.view.focus(); },
|
|
293
|
+
onClose: () => editor.view.focus(),
|
|
294
|
+
}));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const inputs = [["media", "Media key of the picture", current?.mediaRef ?? ""], ["alt", "Alt text", current?.alt ?? ""], ["caption", "Caption (optional)", current?.caption ?? ""]];
|
|
298
|
+
popover("Picture", inputs, ({ media, alt, caption }) => {
|
|
299
|
+
if (!media.trim()) return "Name the picture by its media key.";
|
|
300
|
+
putImage({ mediaRef: media.trim(), alt, caption }, at);
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const act = (action) => {
|
|
305
|
+
const chain = editor.chain().focus();
|
|
306
|
+
const marks = { bold: "toggleBold", italic: "toggleItalic", underline: "toggleUnderline", strike: "toggleStrike", code: "toggleCode" };
|
|
307
|
+
if (marks[action]) return chain[marks[action]]().run();
|
|
308
|
+
if (ALIGNS.includes(action)) return align(action);
|
|
309
|
+
const handlers = {
|
|
310
|
+
link, unlink: () => chain.extendMarkRange("link").unsetLink().run(), bullet: () => setBlock("bullet"), numbered: () => setBlock("numbered"),
|
|
311
|
+
indent: () => bumpIndent(1), outdent: () => bumpIndent(-1), image: () => askImage(null), divider: () => chain.setHorizontalRule().run(),
|
|
312
|
+
break: () => chain.setHardBreak().run(), undo: () => chain.undo().run(), redo: () => chain.redo().run(),
|
|
313
|
+
};
|
|
314
|
+
return handlers[action]?.();
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const refresh = () => {
|
|
318
|
+
const found = topBlock(editor);
|
|
319
|
+
const state = { bold: "bold", italic: "italic", underline: "underline", strike: "strike", code: "code", link: "link", bullet: "flatListItemUnordered", numbered: "flatListItemOrdered" };
|
|
320
|
+
for (const [action, mark] of Object.entries(state)) buttons.get(action).setAttribute("aria-pressed", String(editor.isActive(mark)));
|
|
321
|
+
for (const value of ALIGNS) buttons.get(value).setAttribute("aria-pressed", String(found?.node.attrs.align === value));
|
|
322
|
+
buttons.get("unlink").disabled = !editor.isActive("link");
|
|
323
|
+
buttons.get("undo").disabled = !editor.can().undo();
|
|
324
|
+
buttons.get("redo").disabled = !editor.can().redo();
|
|
325
|
+
buttons.get("select").value = kindOf(editor);
|
|
326
|
+
placeLabel(found);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const placeLabel = (found) => {
|
|
330
|
+
const focused = editor.isFocused && found;
|
|
331
|
+
if (!focused) {
|
|
332
|
+
label.textContent = "";
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const parent = editor.state.selection.$from.parent;
|
|
336
|
+
const depth = ITEMS.includes(parent.type.name) ? (parent.attrs.indent ?? 0) + 1 : 0;
|
|
337
|
+
const text = SHORT[kindOf(editor)] ?? "Text";
|
|
338
|
+
label.textContent = depth > 0 ? `${text} ${depth}` : text;
|
|
339
|
+
label.style.top = `${Math.max(0, editor.view.coordsAtPos(found.pos + 1).top - page.getBoundingClientRect().top)}px`;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const shortcuts = Extension.create({ name: "shortcuts", addKeyboardShortcuts: () => ({ "Mod-k": () => { link(); return true; } }) });
|
|
343
|
+
|
|
344
|
+
toolbar();
|
|
345
|
+
editor = new Editor({
|
|
346
|
+
element: surface,
|
|
347
|
+
injectCSS: false,
|
|
348
|
+
content: bodyToDoc(initial),
|
|
349
|
+
extensions: [
|
|
350
|
+
StarterKit.configure({
|
|
351
|
+
heading: { levels: [1, 2, 3, 4] },
|
|
352
|
+
codeBlock: false,
|
|
353
|
+
blockquote: false,
|
|
354
|
+
bulletList: false,
|
|
355
|
+
orderedList: false,
|
|
356
|
+
listItem: false,
|
|
357
|
+
listKeymap: false,
|
|
358
|
+
trailingNode: false,
|
|
359
|
+
link: { openOnClick: false, autolink: false, linkOnPaste: false, HTMLAttributes: { rel: "noopener noreferrer", target: null } },
|
|
360
|
+
}),
|
|
361
|
+
Blockquote.extend({ content: "paragraph+" }),
|
|
362
|
+
FlatListCore,
|
|
363
|
+
bullet,
|
|
364
|
+
numbered,
|
|
365
|
+
picture(base),
|
|
366
|
+
Placeholder.configure({ placeholder: "Start writing" }),
|
|
367
|
+
Layout,
|
|
368
|
+
DepthCap,
|
|
369
|
+
shortcuts,
|
|
370
|
+
],
|
|
371
|
+
editorProps: {
|
|
372
|
+
attributes: { role: "textbox", "aria-multiline": "true", "aria-label": root.dataset.label ?? "Text", class: `prose mx-auto ${nested ? "min-h-16" : "min-h-72"} max-w-none focus:outline-none` },
|
|
373
|
+
transformPastedHTML(html) {
|
|
374
|
+
if (html.includes("data-pm-slice")) return html;
|
|
375
|
+
const { body, notes: found } = htmlToBodyWithNotes(html);
|
|
376
|
+
say(found);
|
|
377
|
+
return bodyToHtml(body);
|
|
378
|
+
},
|
|
379
|
+
handleClickOn(view, pos, node, nodePos) {
|
|
380
|
+
if (node.type.name !== "image") return false;
|
|
381
|
+
askImage({ mediaRef: node.attrs.mediaRef, alt: node.attrs.alt, caption: node.attrs.caption }, nodePos);
|
|
382
|
+
return true;
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
onUpdate() {
|
|
386
|
+
changed = true;
|
|
387
|
+
status.lastChild.textContent = draft ? "" : "Not saved yet";
|
|
388
|
+
sync();
|
|
389
|
+
draft?.touch();
|
|
390
|
+
root.dispatchEvent(new CustomEvent("editor:changed", { bubbles: true }));
|
|
391
|
+
},
|
|
392
|
+
onTransaction: () => refresh(),
|
|
393
|
+
onFocus: () => refresh(),
|
|
394
|
+
onBlur: () => {
|
|
395
|
+
label.textContent = "";
|
|
396
|
+
draft?.blur();
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
sync();
|
|
400
|
+
status.lastChild.textContent = "";
|
|
401
|
+
root.closest("form")?.addEventListener("submit", () => { if (changed) sync(); });
|
|
402
|
+
root.editor = editor;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Replaces each rendered body field with a writing surface, except those inside a list of mixed
|
|
407
|
+
* blocks: there each is mounted when its card is first opened (see `mountWithin`).
|
|
408
|
+
*/
|
|
409
|
+
export function mountAll() {
|
|
410
|
+
for (const root of document.querySelectorAll("[data-body-editor]")) if (!root.closest("[data-block-list]")) mount(root, root.dataset.media);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Mounts the writing surfaces inside `scope` that are shown and not yet mounted. */
|
|
414
|
+
export function mountWithin(scope) {
|
|
415
|
+
for (const root of scope.querySelectorAll("[data-body-editor]:not([data-mounted])")) {
|
|
416
|
+
if (!root.closest("[hidden]")) mount(root, root.dataset.media);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// Moments between the domain and the person. The domain holds whole seconds since 1970 UTC; a person
|
|
4
|
+
// reads and writes a date and a time in their own time zone. These functions turn one into the other
|
|
5
|
+
// with the browser's own zone (the `TZ` environment variable under node), touch no page, and are held
|
|
6
|
+
// under node in several zones. A date (no time) is the start of that day in the person's zone.
|
|
7
|
+
|
|
8
|
+
const pad = (number, width = 2) => String(number).padStart(width, "0");
|
|
9
|
+
|
|
10
|
+
/** The moment as a native input holds it: `YYYY-MM-DDTHH:MM` (`datetime-local`) or `YYYY-MM-DD` (`date`), in the local zone. */
|
|
11
|
+
export function toLocal(seconds, mode) {
|
|
12
|
+
const moment = new Date(seconds * 1000);
|
|
13
|
+
const day = `${pad(moment.getFullYear(), 4)}-${pad(moment.getMonth() + 1)}-${pad(moment.getDate())}`;
|
|
14
|
+
return mode === "date" ? day : `${day}T${pad(moment.getHours())}:${pad(moment.getMinutes())}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The whole seconds a native input's text stands for in the local zone, or null when it is not a date. */
|
|
18
|
+
export function fromLocal(text, mode) {
|
|
19
|
+
const found = /^(\d{4,6})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?$/.exec(text ?? "");
|
|
20
|
+
if (!found) return null;
|
|
21
|
+
const [year, month, day] = found.slice(1, 4).map(Number);
|
|
22
|
+
const moment = new Date(2000, 0, 1, 0, 0, 0, 0);
|
|
23
|
+
moment.setFullYear(year, month - 1, day);
|
|
24
|
+
if (mode !== "date") moment.setHours(Number(found[4] ?? 0), Number(found[5] ?? 0), 0, 0);
|
|
25
|
+
const seconds = Math.floor(moment.getTime() / 1000);
|
|
26
|
+
return Number.isFinite(seconds) && moment.getDate() === day ? seconds : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The moment in the person's own words and zone: `Oct 7, 2026` or `Oct 7, 2026, 2:30 PM`. */
|
|
30
|
+
export function formatMoment(seconds, mode) {
|
|
31
|
+
const style = mode === "date" ? { dateStyle: "medium" } : { dateStyle: "medium", timeStyle: "short" };
|
|
32
|
+
return new Intl.DateTimeFormat(undefined, style).format(new Date(seconds * 1000));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const UNITS = [
|
|
36
|
+
["year", 31_536_000],
|
|
37
|
+
["month", 2_592_000],
|
|
38
|
+
["day", 86_400],
|
|
39
|
+
["hour", 3_600],
|
|
40
|
+
["minute", 60],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const startOfDay = (moment) => new Date(moment.getFullYear(), moment.getMonth(), moment.getDate()).getTime();
|
|
44
|
+
|
|
45
|
+
/** How far from `now` (whole seconds) the moment is, in words: `in 3 days`, `2 hours ago`, `yesterday`. */
|
|
46
|
+
export function relative(seconds, now, mode) {
|
|
47
|
+
const words = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
|
|
48
|
+
if (mode === "date") return words.format(Math.round((startOfDay(new Date(seconds * 1000)) - startOfDay(new Date(now * 1000))) / 86_400_000), "day");
|
|
49
|
+
const apart = seconds - now;
|
|
50
|
+
const [unit, size] = UNITS.find(([, span]) => Math.abs(apart) >= span) ?? ["second", 1];
|
|
51
|
+
return words.format(Math.trunc(apart / size), unit);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The person's zone as words: `Europe/Paris (UTC+02:00)`, the offset being the one in force at `seconds`. */
|
|
55
|
+
export function zoneLabel(seconds) {
|
|
56
|
+
const minutes = -new Date(seconds * 1000).getTimezoneOffset();
|
|
57
|
+
const sign = minutes < 0 ? "-" : "+";
|
|
58
|
+
const offset = `UTC${sign}${pad(Math.floor(Math.abs(minutes) / 60))}:${pad(Math.abs(minutes) % 60)}`;
|
|
59
|
+
const name = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
60
|
+
return name ? `${name} (${offset})` : offset;
|
|
61
|
+
}
|