stream_weaver 0.3.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 +7 -0
- data/.beads/.gitignore +77 -0
- data/.beads/README.md +81 -0
- data/.beads/config.yaml +54 -0
- data/.beads/metadata.json +7 -0
- data/.rspec +3 -0
- data/AGENTS.md +91 -0
- data/CHANGELOG.md +294 -0
- data/CLAUDE.md +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +565 -0
- data/Rakefile +13 -0
- data/assets/streamweaver-hero.jpg +0 -0
- data/bench/baselines/ledger.rb +58 -0
- data/bench/baselines/warroom.rb +51 -0
- data/bench/fixtures/ledger.rb +76 -0
- data/bench/fixtures/warroom.rb +60 -0
- data/bench/results/6daf8b8.md +102 -0
- data/bench/results/c8d749b.md +102 -0
- data/bench/results/eb06ab9.md +102 -0
- data/bench/run.rb +151 -0
- data/bench/support.rb +84 -0
- data/copy_this/dto_run_sample_run_7631.html +220 -0
- data/docs/SERVICE_MODE.md +302 -0
- data/docs/architecture/how_streamweaver_works.md +772 -0
- data/docs/blog/2026-01-16-streamweaver-introduction.md +515 -0
- data/docs/bug-2026-08-31-canvas-bridge-socket-collision.md +83 -0
- data/docs/canvas-ipc-session-summary.md +127 -0
- data/docs/canvas-panel-workflow.md +172 -0
- data/docs/canvas-read.md +147 -0
- data/docs/canvas-roadmap.md +275 -0
- data/docs/case-studies/2026-09-03-didx-canvas-read-shelf.md +147 -0
- data/docs/claude-code-companion-skill-spec.md +567 -0
- data/docs/components_reference.md +1223 -0
- data/docs/crud-patterns.md +265 -0
- data/docs/endpoints.md +122 -0
- data/docs/for_llms.md +1 -0
- data/docs/form-patterns.md +165 -0
- data/docs/frontend-only.md +169 -0
- data/docs/html-artifact-audit.md +238 -0
- data/docs/ideas/2025-01-01-as-a-service.md +203 -0
- data/docs/ideas/2026-01-16-charm-tui-exploration.md +542 -0
- data/docs/ideas/2026-08-27-static-doc-shelf-export.md +153 -0
- data/docs/ideas/dhh-review-future-refactors.md +142 -0
- data/docs/opal-jamstack.md +47 -0
- data/docs/opal-npm-direction.md +184 -0
- data/docs/opal-spike-findings.md +194 -0
- data/docs/plans/2025-12-16-form-blocks-design.md +124 -0
- data/docs/plans/2025-12-30-examples-browser-design.md +152 -0
- data/docs/plans/2026-01-02-charts-design.md +288 -0
- data/docs/plans/2026-01-10-canvas-ipc-design.md +247 -0
- data/docs/plans/2026-01-19-cabinet-control-components.md +176 -0
- data/docs/plans/2026-01-26-gem-release-and-panel.md +140 -0
- data/docs/plans/canvas-claude-project-design.md +238 -0
- data/docs/plans/canvas-doc-location-and-discovery.md +32 -0
- data/docs/plans/org-doc-preview-surfaces.md +293 -0
- data/docs/plans/shadcn-polish-plan.md +417 -0
- data/docs/porting-artifacts.md +252 -0
- data/docs/reference/agent-skills-comparison.md +369 -0
- data/docs/reference/travel-state-prd.artifact.html +780 -0
- data/docs/research/2026-08-17-hotwire-alike-landscape.md +268 -0
- data/docs/research/2026-08-17-hotwire-concept-map.md +404 -0
- data/docs/research/2026-08-22-lazy-fragments-trigger-decision.md +246 -0
- data/docs/research/2026-08-22-learnhotwire-syllabus-coverage.md +239 -0
- data/docs/research/frontend-only-matrix.md +278 -0
- data/docs/research/streamweaver-way-spike-findings.md +529 -0
- data/docs/resource-dsl.md +342 -0
- data/docs/routing.md +302 -0
- data/docs/ruby-ui-comparison.md +319 -0
- data/docs/shared-dsl-fragments.md +356 -0
- data/docs/streamweaver-for-ai-agents.md +401 -0
- data/docs/streamweaver-frontend-vision.md +123 -0
- data/docs/streamweaver_canvas/glimmer_initial_final_layer.rb +77 -0
- data/docs/streamweaver_canvas/save_example.rb +9 -0
- data/docs/templates.md +213 -0
- data/docs/testing.md +155 -0
- data/docs/theming-hooks.md +281 -0
- data/docs/tutorials/the-streamweaver-way.md +882 -0
- data/docs/university/capability-inventory.md +137 -0
- data/docs/university/dependency-survey.md +114 -0
- data/docs/university/design-spec.md +217 -0
- data/docs/university/mockups/code-block-doc-theme-dark.png +0 -0
- data/docs/university/mockups/code-block-doc-theme-light.png +0 -0
- data/docs/university/mockups/course-list-dark.png +0 -0
- data/docs/university/mockups/course-list-light.png +0 -0
- data/docs/university/mockups/course_canvas_mockup.rb +741 -0
- data/docs/university/mockups/step-screen-dark.png +0 -0
- data/docs/university/mockups/step-screen-light.png +0 -0
- data/docs/university/roadmap.md +72 -0
- data/docs/university/send-to-coworker.md +77 -0
- data/docs/university/worker-session-mining.md +109 -0
- data/docs/visual/browser-rendering-spike.md +173 -0
- data/docs/visual/builder-visual-plan-analysis.md +378 -0
- data/docs/visual/sw-plan-format-exploration.md +511 -0
- data/docs/visual/sw-plan-rendering-deep-dive.md +453 -0
- data/docs/visual-skills/PROGRESS.md +60 -0
- data/docs/visual-skills/SESSION-CONTEXT.md +207 -0
- data/docs/visual-skills/analysis/components.md +317 -0
- data/docs/visual-skills/analysis/overlap.md +360 -0
- data/docs/visual-skills/analysis/pi-design-deck.md +1102 -0
- data/docs/visual-skills/analysis/streamweaver-inventory.md +93 -0
- data/docs/visual-skills/analysis/unified-specs.feature +1003 -0
- data/docs/visual-skills/analysis/visual-explainer.md +964 -0
- data/docs/visual-skills/blog/blog-series-outline.md +39 -0
- data/docs/visual-skills/blog/token-efficiency.md +73 -0
- data/docs/visual-skills/design/architecture.md +1725 -0
- data/docs/visual-skills/design/codex-review.md +340 -0
- data/docs/visual-skills/design/dhh-review.md +209 -0
- data/docs/visual-skills/design/evolution.md +100 -0
- data/docs/visual-skills/design/gemini-review.md +55 -0
- data/docs/visual-skills/design/review-synthesis.md +149 -0
- data/docs/visual-skills/implementation/STATE.md +22 -0
- data/docs/visual-skills/implementation/plan.md +347 -0
- data/docs/visual-skills/implementation/spike-findings.md +237 -0
- data/docs/visual-skills/implementation/tasks.md +702 -0
- data/docs/visual-skills/lessons-learned/process.md +145 -0
- data/examples/README.md +96 -0
- data/examples/advanced/all_components.rb +177 -0
- data/examples/advanced/examples_browser.rb +394 -0
- data/examples/advanced/teachables_browser.rb +261 -0
- data/examples/advanced/theme_tweaker.rb +366 -0
- data/examples/advanced/tutorial.rb +1528 -0
- data/examples/agentic/agentic_form.rb +30 -0
- data/examples/agentic/agentic_form_autoclose.rb +30 -0
- data/examples/agentic/cultivation_tracker.rb +69 -0
- data/examples/basic/hello_world.rb +22 -0
- data/examples/basic/opal_tabs_table.rb +44 -0
- data/examples/basic/todo_list.rb +39 -0
- data/examples/button_loading_test.rb +36 -0
- data/examples/canvas/mermaid_canvas_demo.sh +151 -0
- data/examples/charts/bar_chart_demo.rb +57 -0
- data/examples/charts/line_chart_demo.rb +73 -0
- data/examples/charts/pie_area_demo.rb +87 -0
- data/examples/charts/stacked_bar_chart_demo.rb +69 -0
- data/examples/claude_code/README.md +85 -0
- data/examples/claude_code/codebreaker/.claude/commands/infiltrate.md +632 -0
- data/examples/claude_code/codebreaker/.claude/settings.json +11 -0
- data/examples/claude_code/codebreaker/.claude/settings.local.json +17 -0
- data/examples/claude_code/codebreaker/.claude/skills/infiltrate.md +0 -0
- data/examples/claude_code/codebreaker/README.md +213 -0
- data/examples/claude_code/tutorial/.claude/commands/learn.md +453 -0
- data/examples/claude_code/tutorial/README.md +102 -0
- data/examples/claude_code/verification_flow/.claude/commands/verify.md +171 -0
- data/examples/claude_code/verification_flow/README.md +126 -0
- data/examples/components/annotated_code_demo.rb +54 -0
- data/examples/components/callout_demo.rb +33 -0
- data/examples/components/checkbox_group_demo.rb +46 -0
- data/examples/components/design_review.css +468 -0
- data/examples/components/design_review_demo.rb +44 -0
- data/examples/components/design_review_dsl.rb +300 -0
- data/examples/components/diff_block_demo.rb +112 -0
- data/examples/components/events_demo.rb +184 -0
- data/examples/components/form_demo.rb +62 -0
- data/examples/components/lesson_demo.rb +53 -0
- data/examples/components/markdown_demo.rb +110 -0
- data/examples/components/mermaid_demo.rb +166 -0
- data/examples/components/pareto_set.rb +64 -0
- data/examples/components/prd_demo.rb +21 -0
- data/examples/components/prd_dsl.rb +385 -0
- data/examples/components/quiz_demo.rb +47 -0
- data/examples/components/run_viewer_demo.rb +100 -0
- data/examples/components/score_and_collapsible_demo.rb +82 -0
- data/examples/components/select_stale_value_smoke_test.rb +39 -0
- data/examples/components/table_demo.rb +247 -0
- data/examples/components/timer_with_state_demo.rb +58 -0
- data/examples/components/todo_due_dates.rb +63 -0
- data/examples/components/uat_gaps_demo.rb +112 -0
- data/examples/dashboard/feed_simulator.rb +60 -0
- data/examples/dashboard/live_dashboard.rb +80 -0
- data/examples/dashboard_components.rb +108 -0
- data/examples/deferred_fragments_demo.rb +62 -0
- data/examples/generate_more_spike/README.md +63 -0
- data/examples/generate_more_spike/app.rb +441 -0
- data/examples/git_health.sh +401 -0
- data/examples/layout/layout_components_demo.rb +174 -0
- data/examples/layout/modal_demo.rb +215 -0
- data/examples/layout/navigation_demo.rb +227 -0
- data/examples/layout/route_tabs_demo.rb +52 -0
- data/examples/layout/routing_demo.rb +52 -0
- data/examples/layout/scroll_box_demo.rb +154 -0
- data/examples/lazy_fragments_demo.rb +151 -0
- data/examples/my_todos/README.md +29 -0
- data/examples/my_todos/my_todos.rb +312 -0
- data/examples/my_todos/store.rb +86 -0
- data/examples/opal/reactive_demo.rb +62 -0
- data/examples/opal/scenarios/s1_counter.rb +15 -0
- data/examples/opal/scenarios/s2_search_filter.rb +17 -0
- data/examples/opal/scenarios/s3_sibling_tabs.rb +25 -0
- data/examples/opal/scenarios/s4_shopping_cart.rb +34 -0
- data/examples/opal/scenarios/s5_watch.rb +39 -0
- data/examples/opal/scenarios/s6_on_start.rb +20 -0
- data/examples/opal/scenarios/s7_wizard.rb +33 -0
- data/examples/opal/scenarios/s8_loan_calculator.rb +21 -0
- data/examples/opal/scenarios/s9_dashboard.rb +27 -0
- data/examples/operations_dashboard_demo.rb +167 -0
- data/examples/panel_demo.sh +447 -0
- data/examples/parity/assets/tyrion_slice.css +86 -0
- data/examples/parity/rivet_people_slice.rb +182 -0
- data/examples/parity/tyrion_components.css +777 -0
- data/examples/parity/tyrion_warroom_components.rb +414 -0
- data/examples/parity/tyrion_warroom_slice.rb +549 -0
- data/examples/puma_dev/README.md +124 -0
- data/examples/puma_dev/config.ru +34 -0
- data/examples/puma_dev/standalone_app.rb +34 -0
- data/examples/scaffolding/blog.rb +49 -0
- data/examples/scaffolding/utf_lite.rb +105 -0
- data/examples/styling/feedback_demo.rb +229 -0
- data/examples/styling/style_showcase.rb +172 -0
- data/examples/styling/theme_demo.rb +508 -0
- data/examples/timer_health_checker.rb +111 -0
- data/examples/timer_showcase.rb +131 -0
- data/examples/tutorials/render_markdown.rb +112 -0
- data/examples/tutorials/streamweaver_way_tutorial.rb +44 -0
- data/examples/tutorials/tutorial_content.rb +1082 -0
- data/examples/visual_skills/design_deck_demo.rb +314 -0
- data/examples/visual_skills/explainer_demo.rb +499 -0
- data/exe/streamweaver +9 -0
- data/gsd/ROADMAP-1.0.md +167 -0
- data/gsd/STATE.md +13 -0
- data/gsd/research/market-positioning-research.md +139 -0
- data/gsd/research/production-patterns-research.md +270 -0
- data/gsd/research/repo-audit-1.0.md +274 -0
- data/lib/stream_weaver/action_token.rb +58 -0
- data/lib/stream_weaver/adapter/alpinejs.rb +8249 -0
- data/lib/stream_weaver/adapter/base.rb +591 -0
- data/lib/stream_weaver/adapter/opal.rb +334 -0
- data/lib/stream_weaver/adapter/static.rb +1118 -0
- data/lib/stream_weaver/admin.rb +176 -0
- data/lib/stream_weaver/app.rb +1821 -0
- data/lib/stream_weaver/assets/js/sw-copy.js +50 -0
- data/lib/stream_weaver/assets/js/sw-heredoc-rewrite.js +68 -0
- data/lib/stream_weaver/assets/js/sw-keyboard.js +165 -0
- data/lib/stream_weaver/assets/js/sw-mermaid-zoom.js +621 -0
- data/lib/stream_weaver/assets/js/sw-route-tabs.js +53 -0
- data/lib/stream_weaver/assets/js/sw-sidebar-toc.js +116 -0
- data/lib/stream_weaver/assets/js/sw-slide-nav.js +103 -0
- data/lib/stream_weaver/canvas/bridge.rb +239 -0
- data/lib/stream_weaver/canvas/bridge_server.rb +646 -0
- data/lib/stream_weaver/canvas/client.rb +298 -0
- data/lib/stream_weaver/canvas/doc_roots.rb +233 -0
- data/lib/stream_weaver/canvas/doc_store.rb +252 -0
- data/lib/stream_weaver/canvas/gist_publisher.rb +264 -0
- data/lib/stream_weaver/canvas/gist_save_handler.rb +89 -0
- data/lib/stream_weaver/canvas/gist_store.rb +135 -0
- data/lib/stream_weaver/canvas/helpers.rb +109 -0
- data/lib/stream_weaver/canvas/history.rb +90 -0
- data/lib/stream_weaver/canvas/protocol.rb +86 -0
- data/lib/stream_weaver/canvas/reader.rb +853 -0
- data/lib/stream_weaver/canvas/save_doc_widget.rb +457 -0
- data/lib/stream_weaver/canvas/scroll_top_hint.rb +21 -0
- data/lib/stream_weaver/canvas/session.rb +132 -0
- data/lib/stream_weaver/cli.rb +3235 -0
- data/lib/stream_weaver/component_assets.rb +70 -0
- data/lib/stream_weaver/component_registry.rb +67 -0
- data/lib/stream_weaver/component_renderer.rb +48 -0
- data/lib/stream_weaver/components/annotated_code.rb +53 -0
- data/lib/stream_weaver/components/api_endpoint.rb +42 -0
- data/lib/stream_weaver/components/callout.rb +59 -0
- data/lib/stream_weaver/components/chart.rb +84 -0
- data/lib/stream_weaver/components/code_block.rb +77 -0
- data/lib/stream_weaver/components/comparison.rb +39 -0
- data/lib/stream_weaver/components/decision.rb +38 -0
- data/lib/stream_weaver/components/deck/close_overlay.rb +84 -0
- data/lib/stream_weaver/components/deck/confirmation_bar.rb +60 -0
- data/lib/stream_weaver/components/deck/deck_option.rb +61 -0
- data/lib/stream_weaver/components/deck/deck_slide.rb +77 -0
- data/lib/stream_weaver/components/deck/deck_state.rb +469 -0
- data/lib/stream_weaver/components/deck/deck_summary.rb +73 -0
- data/lib/stream_weaver/components/deck/design_deck.rb +60 -0
- data/lib/stream_weaver/components/deck/generate_more_controls.rb +84 -0
- data/lib/stream_weaver/components/deck/model_selector.rb +84 -0
- data/lib/stream_weaver/components/deck/skeleton_placeholder.rb +36 -0
- data/lib/stream_weaver/components/diff_block.rb +125 -0
- data/lib/stream_weaver/components/doc_header.rb +57 -0
- data/lib/stream_weaver/components/image_block.rb +70 -0
- data/lib/stream_weaver/components/implementation_map.rb +33 -0
- data/lib/stream_weaver/components/keyboard_shortcuts.rb +94 -0
- data/lib/stream_weaver/components/kpi_dashboard.rb +78 -0
- data/lib/stream_weaver/components/mermaid.rb +79 -0
- data/lib/stream_weaver/components/pipeline.rb +63 -0
- data/lib/stream_weaver/components/sidebar_toc.rb +46 -0
- data/lib/stream_weaver/components/slide_container.rb +141 -0
- data/lib/stream_weaver/components/timeline_event.rb +58 -0
- data/lib/stream_weaver/components/wireframe.rb +29 -0
- data/lib/stream_weaver/components/wireframe_block.rb +34 -0
- data/lib/stream_weaver/components.rb +2711 -0
- data/lib/stream_weaver/css.rb +230 -0
- data/lib/stream_weaver/dev_fallback_overlay.rb +61 -0
- data/lib/stream_weaver/display_dsl.rb +1004 -0
- data/lib/stream_weaver/export/html_exporter.rb +478 -0
- data/lib/stream_weaver/feed.rb +34 -0
- data/lib/stream_weaver/feed_builder.rb +31 -0
- data/lib/stream_weaver/fonts.rb +33 -0
- data/lib/stream_weaver/interaction_runner.rb +487 -0
- data/lib/stream_weaver/iterm.rb +460 -0
- data/lib/stream_weaver/layout_registry.rb +92 -0
- data/lib/stream_weaver/opal/bridge.rb +52 -0
- data/lib/stream_weaver/opal/builder.rb +193 -0
- data/lib/stream_weaver/opal/env.rb +29 -0
- data/lib/stream_weaver/opal/reactive_state.rb +75 -0
- data/lib/stream_weaver/opal/regexp_anchor_patch.rb +62 -0
- data/lib/stream_weaver/opal/renderer.rb +77 -0
- data/lib/stream_weaver/opal/runtime.rb +250 -0
- data/lib/stream_weaver/opal/shell.rb +145 -0
- data/lib/stream_weaver/opal/string_bridge.rb +50 -0
- data/lib/stream_weaver/opal/stubs/diff.min.js +1 -0
- data/lib/stream_weaver/opal/stubs/digest.rb +15 -0
- data/lib/stream_weaver/opal/stubs/marked.umd.js +79 -0
- data/lib/stream_weaver/opal/stubs/md5.rb +3 -0
- data/lib/stream_weaver/opal/stubs/morphdom.min.js +775 -0
- data/lib/stream_weaver/opal/stubs/prism-tomorrow.min.css +1 -0
- data/lib/stream_weaver/opal/stubs/prism.min.js +1967 -0
- data/lib/stream_weaver/opal_entry.rb +136 -0
- data/lib/stream_weaver/org/inline.rb +95 -0
- data/lib/stream_weaver/org/reader.rb +561 -0
- data/lib/stream_weaver/org/recording_context.rb +86 -0
- data/lib/stream_weaver/org/source_splitter.rb +63 -0
- data/lib/stream_weaver/org/writer.rb +300 -0
- data/lib/stream_weaver/page_shell.rb +530 -0
- data/lib/stream_weaver/portfile.rb +79 -0
- data/lib/stream_weaver/pushable.rb +42 -0
- data/lib/stream_weaver/resource/default_views.rb +111 -0
- data/lib/stream_weaver/resource/field_input.rb +25 -0
- data/lib/stream_weaver/resource/state_keys.rb +16 -0
- data/lib/stream_weaver/resource/store.rb +18 -0
- data/lib/stream_weaver/resource.rb +104 -0
- data/lib/stream_weaver/server.rb +1471 -0
- data/lib/stream_weaver/service.rb +1240 -0
- data/lib/stream_weaver/service_client.rb +104 -0
- data/lib/stream_weaver/session_store.rb +186 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/SKILL.md +66 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/examples/canvas-safe-showcase.rb +98 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/actions-and-buttons.md +47 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/charts-and-diagrams.md +38 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/deck.md +32 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/inputs-and-forms.md +65 -0
- data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/tabs-and-navigation.md +53 -0
- data/lib/stream_weaver/skills/streamweaver-doc-builder/SKILL.md +298 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/SKILL.md +130 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.css +468 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.rb +39 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example_dsl.rb +300 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example.rb +24 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example_dsl.rb +385 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/checkpoints-and-forms.md +28 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/cleanup-and-panel.md +44 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/example-gallery.md +8 -0
- data/lib/stream_weaver/skills/streamweaver-visual-companion/references/persistence.md +58 -0
- data/lib/stream_weaver/skills/streamweaver-way/SKILL.md +396 -0
- data/lib/stream_weaver/skills/visual-plan/SKILL.md +201 -0
- data/lib/stream_weaver/skills/visual-recap/SKILL.md +244 -0
- data/lib/stream_weaver/streamer.rb +63 -0
- data/lib/stream_weaver/templates/choices.rb +149 -0
- data/lib/stream_weaver/templates/code.rb +157 -0
- data/lib/stream_weaver/templates/confirm.rb +110 -0
- data/lib/stream_weaver/templates/diff.rb +195 -0
- data/lib/stream_weaver/templates/info.rb +142 -0
- data/lib/stream_weaver/templates/table.rb +169 -0
- data/lib/stream_weaver/templates/wizard.rb +271 -0
- data/lib/stream_weaver/theme/auto_mode.rb +116 -0
- data/lib/stream_weaver/theme/presets.rb +555 -0
- data/lib/stream_weaver/theme.rb +639 -0
- data/lib/stream_weaver/university/canvas.rb +924 -0
- data/lib/stream_weaver/university/course.rb +699 -0
- data/lib/stream_weaver/university/demos/counter.rb +33 -0
- data/lib/stream_weaver/university/demos/dashboard.rb +147 -0
- data/lib/stream_weaver/university/demos/decision_form.rb +143 -0
- data/lib/stream_weaver/university/demos.rb +50 -0
- data/lib/stream_weaver/university/listener.rb +467 -0
- data/lib/stream_weaver/university/progress.rb +201 -0
- data/lib/stream_weaver/university/runner.rb +138 -0
- data/lib/stream_weaver/university/scripts/growing_doc.rb +609 -0
- data/lib/stream_weaver/university/scripts/growing_doc_state.rb +91 -0
- data/lib/stream_weaver/utils.rb +31 -0
- data/lib/stream_weaver/version.rb +5 -0
- data/lib/stream_weaver/views/canvas/reader_layout.erb +800 -0
- data/lib/stream_weaver/views.rb +3940 -0
- data/lib/stream_weaver.rb +122 -0
- data/llms.txt +1269 -0
- data/sig/stream_weaver.rbs +4 -0
- metadata +598 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require_relative "shell"
|
|
4
|
+
require_relative "../css"
|
|
5
|
+
require_relative "../theme"
|
|
6
|
+
|
|
7
|
+
module StreamWeaver
|
|
8
|
+
module Opal
|
|
9
|
+
class OpalBuilder
|
|
10
|
+
# opal is the heavyweight compiler behind `streamweaver opal-build` --
|
|
11
|
+
# not a runtime dependency of the gem (see stream_weaver.gemspec), so a
|
|
12
|
+
# doc-shelf-only install must never be forced to have it. Load it here,
|
|
13
|
+
# on first actual use, instead of at file-load time.
|
|
14
|
+
def self.require_opal!
|
|
15
|
+
require "opal"
|
|
16
|
+
rescue LoadError
|
|
17
|
+
raise LoadError, "Opal features need the opal gem: gem install opal"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Convenience entry point
|
|
21
|
+
def self.build(app_file, output_dir: "dist", title: nil, theme: nil)
|
|
22
|
+
require_opal!
|
|
23
|
+
new(app_file, output_dir: output_dir, title: title, theme: theme).call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(app_file, output_dir: "dist", title: nil, theme: nil)
|
|
27
|
+
@app_file = app_file
|
|
28
|
+
@output_dir = output_dir
|
|
29
|
+
@title = title || derive_title
|
|
30
|
+
@theme = theme
|
|
31
|
+
@lib_root = File.expand_path(File.join(__dir__, "../.."))
|
|
32
|
+
@stubs_root = File.join(__dir__, "stubs")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
FileUtils.mkdir_p(@output_dir)
|
|
37
|
+
write_app_js
|
|
38
|
+
copy_morphdom
|
|
39
|
+
copy_marked
|
|
40
|
+
copy_browser_assets
|
|
41
|
+
write_theme_css
|
|
42
|
+
write_index_html
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Assets committed under opal/stubs, plus mermaid from the gitignored
|
|
48
|
+
# vendor dir (see bin/vendor_browser_assets -- it is too large to commit).
|
|
49
|
+
# Anything missing falls back to its CDN in OpalShell, which keeps builds
|
|
50
|
+
# working for web deployment; only offline and browser-extension hosts
|
|
51
|
+
# actually require the local copies.
|
|
52
|
+
#
|
|
53
|
+
# sw-mermaid-zoom.js is different from the rest: it is StreamWeaver's own
|
|
54
|
+
# code, not a third-party library, so it has no CDN to fall back to and
|
|
55
|
+
# is always committed (unlike mermaid.min.js itself). It is copied
|
|
56
|
+
# unconditionally -- since Adapter::Static#render_mermaid (shared by both
|
|
57
|
+
# adapters) always writes the mermaid source into a data attribute rather
|
|
58
|
+
# than element text, this engine is the only thing that can read it. A
|
|
59
|
+
# build missing it would render an empty box, not a diagram
|
|
60
|
+
# (stream_weaver-mermaid-extension).
|
|
61
|
+
def copy_browser_assets
|
|
62
|
+
{
|
|
63
|
+
"prism.min.js" => File.join(@stubs_root, "prism.min.js"),
|
|
64
|
+
"prism-tomorrow.min.css" => File.join(@stubs_root, "prism-tomorrow.min.css"),
|
|
65
|
+
"diff.min.js" => File.join(@stubs_root, "diff.min.js"),
|
|
66
|
+
"mermaid.min.js" => mermaid_source_path,
|
|
67
|
+
"sw-mermaid-zoom.js" => File.join(@lib_root, "stream_weaver", "assets", "js", "sw-mermaid-zoom.js")
|
|
68
|
+
}.each do |name, src|
|
|
69
|
+
next unless src && File.exist?(src)
|
|
70
|
+
|
|
71
|
+
FileUtils.cp(src, output_path(name))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return if mermaid_source_path && File.exist?(mermaid_source_path)
|
|
75
|
+
|
|
76
|
+
warn "[OpalBuilder] mermaid.min.js not vendored -- falling back to CDN. " \
|
|
77
|
+
"Run bin/vendor_browser_assets for an offline build."
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def mermaid_source_path
|
|
81
|
+
@mermaid_source_path ||= File.join(@lib_root, "..", "vendor", "js", "mermaid.min.js")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def local_asset(name)
|
|
85
|
+
File.exist?(output_path(name)) ? name : nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def write_app_js
|
|
89
|
+
File.write(output_path("app.js"), compile.to_s)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def copy_morphdom
|
|
93
|
+
src = File.join(@stubs_root, "morphdom.min.js")
|
|
94
|
+
FileUtils.cp(src, output_path("morphdom.min.js")) if File.exist?(src)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def copy_marked
|
|
98
|
+
src = File.join(@stubs_root, "marked.umd.js")
|
|
99
|
+
FileUtils.cp(src, output_path("marked.umd.js")) if File.exist?(src)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def write_theme_css
|
|
103
|
+
css = StreamWeaver::CSS.full_stylesheet
|
|
104
|
+
css += "\n" + StreamWeaver::Theme.visual_skills_css
|
|
105
|
+
css += "\n" + StreamWeaver::CSS.animation_css
|
|
106
|
+
if @theme
|
|
107
|
+
unless StreamWeaver::Theme::Presets.get(@theme.to_sym)
|
|
108
|
+
warn "[OpalBuilder] Unknown theme preset: #{@theme}"
|
|
109
|
+
end
|
|
110
|
+
css += "\n" + StreamWeaver::Theme::Presets.generate_preset_css(@theme.to_sym)
|
|
111
|
+
end
|
|
112
|
+
File.write(output_path("sw-theme.css"), css)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def write_index_html
|
|
116
|
+
morphdom_js = File.exist?(output_path("morphdom.min.js")) ? "morphdom.min.js" : nil
|
|
117
|
+
marked_js = File.exist?(output_path("marked.umd.js")) ? "marked.umd.js" : nil
|
|
118
|
+
File.write(output_path("index.html"),
|
|
119
|
+
OpalShell.render(
|
|
120
|
+
title: @title,
|
|
121
|
+
app_js: "app.js",
|
|
122
|
+
morphdom_js: morphdom_js,
|
|
123
|
+
marked_js: marked_js,
|
|
124
|
+
prism_js: local_asset("prism.min.js"),
|
|
125
|
+
prism_css: local_asset("prism-tomorrow.min.css"),
|
|
126
|
+
diff_js: local_asset("diff.min.js"),
|
|
127
|
+
mermaid_js: local_asset("mermaid.min.js"),
|
|
128
|
+
mermaid_zoom_js: local_asset("sw-mermaid-zoom.js"),
|
|
129
|
+
theme_css: File.exist?(output_path("sw-theme.css")) ? "sw-theme.css" : nil,
|
|
130
|
+
google_fonts_url: google_fonts_url_for_build,
|
|
131
|
+
body_theme: @theme ? "sw-theme-default sw-theme-#{@theme}" : "sw-theme-default",
|
|
132
|
+
dark_mode_script: StreamWeaver::Theme::AutoMode.inline_script
|
|
133
|
+
))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def google_fonts_url_for_build
|
|
137
|
+
if @theme && (preset = StreamWeaver::Theme::Presets.get(@theme.to_sym))
|
|
138
|
+
StreamWeaver::Theme::Presets.google_fonts_url(preset)
|
|
139
|
+
else
|
|
140
|
+
"https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;500;600&family=Source+Sans+3:wght@400;500;600;700&display=swap"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def compile
|
|
145
|
+
builder = build_opal_bundle
|
|
146
|
+
builder.build_str(stripped_source, File.basename(@app_file))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def build_opal_bundle
|
|
150
|
+
::Opal::Builder.new(missing_require_severity: :ignore).tap do |b|
|
|
151
|
+
b.append_paths(@stubs_root)
|
|
152
|
+
b.append_paths(@lib_root)
|
|
153
|
+
b.build("opal")
|
|
154
|
+
# Must come immediately after "opal" (which defines Opal.regexp) --
|
|
155
|
+
# see regexp_anchor_patch.rb's own header comment for what bug
|
|
156
|
+
# this fixes and why the position matters (every compiled file
|
|
157
|
+
# captures its own local reference to the function it patches, at
|
|
158
|
+
# load time). This build (`streamweaver opal-build`, the AOT
|
|
159
|
+
# per-file compile path) doesn't currently hit the bug this
|
|
160
|
+
# patches -- nothing in its output builds a runtime-interpolated
|
|
161
|
+
# \A/\z-anchored regex today -- but bin/build_extension's bundle
|
|
162
|
+
# does, and keeping both Opal builders in sync means the fix
|
|
163
|
+
# doesn't silently stop applying the moment that changes.
|
|
164
|
+
b.build("stream_weaver/opal/regexp_anchor_patch")
|
|
165
|
+
build_stdlib(b)
|
|
166
|
+
b.build("stream_weaver/opal_entry")
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def build_stdlib(builder)
|
|
171
|
+
%w[set cgi json digest thread].each do |lib|
|
|
172
|
+
builder.build(lib)
|
|
173
|
+
rescue => e
|
|
174
|
+
warn "[OpalBuilder] Could not build stdlib '#{lib}': #{e.message}"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def stripped_source
|
|
179
|
+
File.read(@app_file)
|
|
180
|
+
.gsub(/^\s*require_relative\s+['"][^'"]+['"]\s*$/, "")
|
|
181
|
+
.gsub(/^\s*require\s+['"]stream_weaver['"]\s*$/, "")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def output_path(filename)
|
|
185
|
+
File.join(@output_dir, filename)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def derive_title
|
|
189
|
+
File.basename(@app_file, ".rb").tr("_-", " ").split.map(&:capitalize).join(" ")
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# backtick_javascript: true
|
|
3
|
+
|
|
4
|
+
module StreamWeaver
|
|
5
|
+
module Opal
|
|
6
|
+
# Answers "is there a DOM here?" for code that is compiled once and then
|
|
7
|
+
# runs in two very different hosts.
|
|
8
|
+
#
|
|
9
|
+
# The same bundle powers a browser tab (where `document` and `window` exist
|
|
10
|
+
# and the runtime patches live nodes) and a bare Node process (where neither
|
|
11
|
+
# exists and the runtime can only build strings). Rather than making every
|
|
12
|
+
# caller take a "browser or not" argument -- which would change the shape of
|
|
13
|
+
# `app()` and break existing builds -- the browser-only paths ask here and
|
|
14
|
+
# no-op when the answer is no.
|
|
15
|
+
#
|
|
16
|
+
# Under MRI this is always false: MRI is neither host, and short-circuiting
|
|
17
|
+
# before the backtick keeps the JS probe from being evaluated as a shell
|
|
18
|
+
# command.
|
|
19
|
+
module Env
|
|
20
|
+
def self.dom?
|
|
21
|
+
return false unless RUBY_ENGINE == "opal"
|
|
22
|
+
|
|
23
|
+
# :nocov:
|
|
24
|
+
`typeof document !== 'undefined' && document !== null && typeof document.createElement === 'function'`
|
|
25
|
+
# :nocov:
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Opal
|
|
5
|
+
class ReactiveState
|
|
6
|
+
def initialize(hash = {})
|
|
7
|
+
@data = hash.transform_keys(&:to_sym)
|
|
8
|
+
@watchers = Hash.new { |h, k| h[k] = [] }
|
|
9
|
+
@tracking = nil
|
|
10
|
+
@track_map = Hash.new { |h, k| h[k] = [] }
|
|
11
|
+
@change_hooks = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def [](key)
|
|
15
|
+
key = key.to_sym
|
|
16
|
+
@track_map[key] << @tracking if @tracking && !@track_map[key].include?(@tracking)
|
|
17
|
+
@data[key]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def []=(key, value)
|
|
21
|
+
key = key.to_sym
|
|
22
|
+
old = @data[key]
|
|
23
|
+
@data[key] = value
|
|
24
|
+
return if old == value
|
|
25
|
+
notify_watchers(key)
|
|
26
|
+
@change_hooks.each { |h| h.call(key) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def watch(key, &block)
|
|
30
|
+
@watchers[key.to_sym] << block
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_any_change(&block)
|
|
34
|
+
@change_hooks << block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def track(region_id)
|
|
38
|
+
prev, @tracking = @tracking, region_id
|
|
39
|
+
yield
|
|
40
|
+
ensure
|
|
41
|
+
@tracking = prev
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reset_tracking
|
|
45
|
+
@track_map.clear
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dependencies_for(region_id)
|
|
49
|
+
@track_map.each_with_object([]) { |(key, ids), arr| arr << key if ids.include?(region_id) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def dependencies_for_key(key)
|
|
53
|
+
@track_map[key.to_sym].dup
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ==(other)
|
|
57
|
+
other.is_a?(ReactiveState) ? @data == other.to_h : @data == other
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h
|
|
61
|
+
@data.dup
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def key?(key)
|
|
65
|
+
@data.key?(key.to_sym)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def notify_watchers(key)
|
|
71
|
+
@watchers[key].each { |w| w.call(@data[key]) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# backtick_javascript: true
|
|
3
|
+
|
|
4
|
+
# Fixes a real Opal bug: \A and \z anchors are only translated to JS's ^/$
|
|
5
|
+
# for a regex LITERAL known at compile time. An INTERPOLATED regex literal
|
|
6
|
+
# (`/foo#{bar}baz/`) goes through Opal.regexp, which joins the source parts
|
|
7
|
+
# and passes them straight to `new RegExp(...)` with zero anchor
|
|
8
|
+
# translation. \A/\z aren't valid JS regex escapes, so they silently match
|
|
9
|
+
# nothing instead of anchoring -- no error, just a pattern that quietly
|
|
10
|
+
# never matches.
|
|
11
|
+
#
|
|
12
|
+
# Regexp.new(string) is NOT the same bug and does not need this patch --
|
|
13
|
+
# it has its own, separate corelib-level translation
|
|
14
|
+
# (`regexp.replace('\A', '^').replace('\z', '$')`, no interpolation
|
|
15
|
+
# involved at all). That handling has its own narrower limitations (no /g,
|
|
16
|
+
# so only the first \A and first \z in a pattern get translated; \Z isn't
|
|
17
|
+
# handled) but is unaffected by, and doesn't fix, the Opal.regexp bug this
|
|
18
|
+
# file patches.
|
|
19
|
+
#
|
|
20
|
+
# Found via StreamWeaver::Org::Reader#callout_marker_match, which builds
|
|
21
|
+
# /\A\*(#{Regexp.union(VARIANT_EMOJI.keys)})...\*\z/ at runtime -- every
|
|
22
|
+
# emoji-marked callout silently misclassified as a card. Not specific to
|
|
23
|
+
# Regexp.union, emoji, or that one call site: reproduced with a plain
|
|
24
|
+
# string interpolated into an anchored pattern, no union/escape involved --
|
|
25
|
+
# it's specifically about interpolation, not about Regexp.new.
|
|
26
|
+
#
|
|
27
|
+
# Must load AFTER `opal` (which defines Opal.regexp) and BEFORE anything
|
|
28
|
+
# else in the build sequence -- every compiled file captures its OWN
|
|
29
|
+
# `$regexp = Opal.regexp` local alias at module-load time (a per-file
|
|
30
|
+
# Opal codegen optimization), so patching Opal.regexp from a script loaded
|
|
31
|
+
# after the whole bundle has already executed is too late: those aliases
|
|
32
|
+
# already point at the original, unpatched function. Compiling this as
|
|
33
|
+
# part of the same Opal::Builder sequence (see bin/build_extension),
|
|
34
|
+
# positioned right after `opal` and before any other `builder.build` call,
|
|
35
|
+
# is what makes every later file's alias capture pick up the fix instead.
|
|
36
|
+
#
|
|
37
|
+
# \A -> ^ is an exact translation as long as the pattern never reaches JS's
|
|
38
|
+
# `m` (multiline) flag -- confirmed by reading Opal.regexp's own source: it
|
|
39
|
+
# only ever sets JS's `s` (dotall) for Ruby's /m modifier, never JS's `m`,
|
|
40
|
+
# so ^ stays equivalent to \A for every pattern this function is ever
|
|
41
|
+
# called with. \z -> $ is the same story for \z specifically; \Z is treated
|
|
42
|
+
# the same way here for simplicity, though it isn't exact (\Z also matches
|
|
43
|
+
# just before a trailing newline, $ without /m doesn't) -- fine for this
|
|
44
|
+
# codebase's actual patterns, worth knowing if a future one relies on that
|
|
45
|
+
# distinction.
|
|
46
|
+
#
|
|
47
|
+
# Not yet filed upstream -- tracked as stream_weaver-mg6b.
|
|
48
|
+
if RUBY_ENGINE == "opal"
|
|
49
|
+
%x{
|
|
50
|
+
(function () {
|
|
51
|
+
var original = Opal.regexp;
|
|
52
|
+
Opal.regexp = function (parts, flags) {
|
|
53
|
+
for (var i = 0; i < parts.length; i++) {
|
|
54
|
+
if (typeof parts[i] === "string") {
|
|
55
|
+
parts[i] = parts[i].replace(/\\A/g, "^").replace(/\\[zZ]/g, "$");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return original(parts, flags);
|
|
59
|
+
};
|
|
60
|
+
})();
|
|
61
|
+
}
|
|
62
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module StreamWeaver
|
|
6
|
+
module Opal
|
|
7
|
+
class OpalRenderer
|
|
8
|
+
attr_reader :adapter
|
|
9
|
+
|
|
10
|
+
def initialize(adapter, state)
|
|
11
|
+
@adapter = adapter
|
|
12
|
+
@state = state
|
|
13
|
+
@output = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Block tags — open tag, yield, close tag.
|
|
17
|
+
# If the block returns a String and nothing was added to @output inside the block,
|
|
18
|
+
# treat the return value as plain text (mirrors Phlex behavior for simple text blocks).
|
|
19
|
+
%w[div span p ul ol li h1 h2 h3 h4 h5 h6 form label select textarea
|
|
20
|
+
nav header footer main section article aside table thead tbody tr th td
|
|
21
|
+
button fieldset legend details summary strong em a
|
|
22
|
+
pre code blockquote figure figcaption caption style script small
|
|
23
|
+
time mark sub sup abbr dl dt dd].each do |tag|
|
|
24
|
+
define_method(tag) do |**attrs, &block|
|
|
25
|
+
@output << "<#{tag}#{attrs_to_html(attrs)}>"
|
|
26
|
+
if block
|
|
27
|
+
before_len = @output.length
|
|
28
|
+
result = block.call
|
|
29
|
+
# If block didn't add anything (returned a string), emit it as plain text
|
|
30
|
+
if @output.length == before_len && result.is_a?(String)
|
|
31
|
+
@output << html_escape(result)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
@output << "</#{tag}>"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Void (self-closing) tags
|
|
39
|
+
%w[input hr br img link meta].each do |tag|
|
|
40
|
+
define_method(tag) do |**attrs|
|
|
41
|
+
@output << "<#{tag}#{attrs_to_html(attrs)}>"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def plain(text)
|
|
46
|
+
@output << html_escape(text.to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def raw(html)
|
|
50
|
+
@output << html.to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Phlex marks trusted strings with #safe before #raw will emit them.
|
|
54
|
+
# Here #raw already emits verbatim, so this is just the identity function
|
|
55
|
+
# -- it exists so shared renderers can call view.raw(view.safe(css))
|
|
56
|
+
# unchanged in both adapters.
|
|
57
|
+
def safe(html) = html.to_s
|
|
58
|
+
|
|
59
|
+
def to_html
|
|
60
|
+
@output.join
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def attrs_to_html(attrs)
|
|
66
|
+
return "" if attrs.empty?
|
|
67
|
+
" " + attrs.filter_map do |k, v|
|
|
68
|
+
next if v == false
|
|
69
|
+
key = k.to_s.tr("_", "-")
|
|
70
|
+
v == true ? key : "#{key}=\"#{html_escape(v.to_s)}\""
|
|
71
|
+
end.join(" ")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def html_escape(str) = CGI.escapeHTML(str.to_s)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "reactive_state"
|
|
4
|
+
require_relative "shell"
|
|
5
|
+
|
|
6
|
+
module StreamWeaver
|
|
7
|
+
module Opal
|
|
8
|
+
class OpalRuntime
|
|
9
|
+
class << self
|
|
10
|
+
attr_accessor :current
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :state
|
|
14
|
+
|
|
15
|
+
def initialize(adapter:)
|
|
16
|
+
@adapter = adapter
|
|
17
|
+
@state = ReactiveState.new
|
|
18
|
+
@callbacks = {}
|
|
19
|
+
@block = nil
|
|
20
|
+
@start_hooks = []
|
|
21
|
+
@start_hooks_fired = false
|
|
22
|
+
@watchers_initialized = false
|
|
23
|
+
@sync_rendering = false
|
|
24
|
+
@rerender_pending = false
|
|
25
|
+
@state.on_any_change { schedule_rerender }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def watchers_initialized? = @watchers_initialized
|
|
29
|
+
|
|
30
|
+
def set_block(&block)
|
|
31
|
+
@block = block
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update_state(key, value)
|
|
35
|
+
@state[key] = value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def register_callback(dom_id, &proc)
|
|
39
|
+
@callbacks[dom_id] = proc
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def invoke_callback(dom_id)
|
|
43
|
+
@callbacks[dom_id]&.call(@state)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def register_start_hook(block)
|
|
47
|
+
@start_hooks << block unless @start_hooks_fired
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_html
|
|
51
|
+
@callbacks.clear
|
|
52
|
+
@state.reset_tracking
|
|
53
|
+
OpalRuntime.current = self
|
|
54
|
+
|
|
55
|
+
# First pass: build components and determine count (no tracking yet)
|
|
56
|
+
app = StreamWeaver::App.new("__opal__", &@block)
|
|
57
|
+
app.rebuild_with_state(@state)
|
|
58
|
+
@watchers_initialized = true
|
|
59
|
+
n = app.components.length
|
|
60
|
+
|
|
61
|
+
register_component_callbacks(app.components)
|
|
62
|
+
|
|
63
|
+
# Second pass: render each component inside its own track region.
|
|
64
|
+
# rebuild_with_state is called inside each track block so that state
|
|
65
|
+
# reads in the DSL block (e.g. `text state[:name].to_s`) are recorded
|
|
66
|
+
# against the correct region_id.
|
|
67
|
+
# DSL-time reads (state[:key] inside the app block) happen during rebuild_with_state,
|
|
68
|
+
# so we re-build once per region inside track() to attribute reads to the correct region.
|
|
69
|
+
parts = (0...n).map do |i|
|
|
70
|
+
region_html = @state.track("sw-region-#{i}") do
|
|
71
|
+
scoped_app = StreamWeaver::App.new("__opal__", &@block)
|
|
72
|
+
scoped_app.rebuild_with_state(@state)
|
|
73
|
+
component = scoped_app.components[i]
|
|
74
|
+
sub = OpalRenderer.new(@adapter, @state)
|
|
75
|
+
component.render(sub, @state) if component
|
|
76
|
+
sub.to_html
|
|
77
|
+
end
|
|
78
|
+
"<div id=\"sw-region-#{i}\">#{region_html}</div>"
|
|
79
|
+
end
|
|
80
|
+
parts.join
|
|
81
|
+
ensure
|
|
82
|
+
OpalRuntime.current = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# --- DOM-free rendering -------------------------------------------------
|
|
86
|
+
#
|
|
87
|
+
# render_html already builds the whole document without touching the DOM;
|
|
88
|
+
# everything below is what a non-browser host (Node, a CLI) needs around
|
|
89
|
+
# it. Nothing here references `window` or `document`, directly or through
|
|
90
|
+
# a callee, so the same compiled bundle renders in a bare Node process.
|
|
91
|
+
|
|
92
|
+
# The document body, as a string. Same markup the browser paints.
|
|
93
|
+
def render_body_html
|
|
94
|
+
render_html
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Per-component CSS gathered during the last render.
|
|
98
|
+
#
|
|
99
|
+
# In a browser the adapter appends this to <head> and the caller never
|
|
100
|
+
# needs it. With no <head> to append to, the caller has to place it, so
|
|
101
|
+
# the collected text is handed back rather than dropped -- a document
|
|
102
|
+
# rendered to a file is styled because of this.
|
|
103
|
+
def collected_css
|
|
104
|
+
return "" unless @adapter.respond_to?(:collected_css_text)
|
|
105
|
+
|
|
106
|
+
@adapter.collected_css_text
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# A complete, standalone HTML document: body markup baked in, component
|
|
110
|
+
# CSS inlined, no app.js and no runtime boot. This is the artifact a
|
|
111
|
+
# `streamweaver-render doc.rb > doc.html` CLI writes out.
|
|
112
|
+
#
|
|
113
|
+
# stylesheet: framework CSS to inline ahead of the component CSS. Defaults
|
|
114
|
+
# to whatever is reachable from the current host (see CSS.base_stylesheet)
|
|
115
|
+
# -- a host that ships the full theme file can pass its contents instead.
|
|
116
|
+
# Remaining options are forwarded to OpalShell.render.
|
|
117
|
+
def render_document(title: "StreamWeaver Document", stylesheet: nil, **shell_options)
|
|
118
|
+
body = render_html
|
|
119
|
+
css = [stylesheet || StreamWeaver::CSS.base_stylesheet, collected_css]
|
|
120
|
+
.reject { |c| c.nil? || c.to_s.strip.empty? }.join("\n")
|
|
121
|
+
|
|
122
|
+
OpalShell.render(
|
|
123
|
+
title: title,
|
|
124
|
+
app_js: nil,
|
|
125
|
+
body_html: body,
|
|
126
|
+
inline_css: css,
|
|
127
|
+
**shell_options
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def register_component_callbacks(components)
|
|
132
|
+
Array(components).each do |c|
|
|
133
|
+
c.register_callbacks(@callbacks)
|
|
134
|
+
register_component_callbacks(c.children)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def render_and_patch
|
|
139
|
+
@sync_rendering = true
|
|
140
|
+
html = render_html
|
|
141
|
+
patch_dom(html)
|
|
142
|
+
ensure
|
|
143
|
+
@sync_rendering = false
|
|
144
|
+
fire_start_hooks_once
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def invoke_and_patch(dom_id)
|
|
148
|
+
@sync_rendering = true
|
|
149
|
+
invoke_callback(dom_id)
|
|
150
|
+
patch_dom(render_html)
|
|
151
|
+
ensure
|
|
152
|
+
@sync_rendering = false
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def update_and_patch(key, value)
|
|
156
|
+
@sync_rendering = true
|
|
157
|
+
update_state(key, value)
|
|
158
|
+
affected_regions = @state.dependencies_for_key(key.to_sym)
|
|
159
|
+
if affected_regions.empty?
|
|
160
|
+
patch_dom(render_html)
|
|
161
|
+
else
|
|
162
|
+
html = render_html
|
|
163
|
+
patch_regions(affected_regions, html)
|
|
164
|
+
end
|
|
165
|
+
ensure
|
|
166
|
+
@sync_rendering = false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def patch_regions(region_ids, full_html)
|
|
170
|
+
# :nocov:
|
|
171
|
+
%x{
|
|
172
|
+
var parser = new DOMParser();
|
|
173
|
+
var doc = parser.parseFromString('<div id="sw-app">' + #{full_html} + '</div>', 'text/html');
|
|
174
|
+
var ids = #{region_ids};
|
|
175
|
+
for (var i = 0; i < ids.length; i++) {
|
|
176
|
+
var id = ids[i];
|
|
177
|
+
var newRegion = doc.getElementById(id);
|
|
178
|
+
var oldRegion = document.getElementById(id);
|
|
179
|
+
if (newRegion && oldRegion) { morphdom(oldRegion, newRegion); }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
# :nocov:
|
|
183
|
+
announce_render
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def fire_start_hooks_once
|
|
187
|
+
return if @start_hooks_fired
|
|
188
|
+
@start_hooks_fired = true
|
|
189
|
+
hooks = @start_hooks.dup
|
|
190
|
+
schedule_start_hooks(hooks)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def schedule_start_hooks(hooks)
|
|
194
|
+
# :nocov:
|
|
195
|
+
runtime = self
|
|
196
|
+
%x{
|
|
197
|
+
setTimeout(function() {
|
|
198
|
+
#{hooks.each(&:call)};
|
|
199
|
+
#{runtime.schedule_rerender};
|
|
200
|
+
}, 0);
|
|
201
|
+
}
|
|
202
|
+
# :nocov:
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def schedule_rerender
|
|
206
|
+
# State mutations during a sync render are dropped — app blocks should not
|
|
207
|
+
# write state as a side effect of building the view.
|
|
208
|
+
return if @sync_rendering || @rerender_pending
|
|
209
|
+
@rerender_pending = true
|
|
210
|
+
return unless RUBY_ENGINE == "opal"
|
|
211
|
+
runtime = self
|
|
212
|
+
# :nocov:
|
|
213
|
+
%x{
|
|
214
|
+
setTimeout(function() {
|
|
215
|
+
#{runtime.perform_async_render};
|
|
216
|
+
}, 0);
|
|
217
|
+
}
|
|
218
|
+
# :nocov:
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def perform_async_render
|
|
222
|
+
@rerender_pending = false
|
|
223
|
+
render_and_patch
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def patch_dom(html)
|
|
227
|
+
# :nocov:
|
|
228
|
+
%x{ morphdom(document.getElementById('sw-app'), '<div id="sw-app">' + #{html} + '</div>') }
|
|
229
|
+
# :nocov:
|
|
230
|
+
announce_render
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Signals that freshly patched markup is in the DOM.
|
|
234
|
+
#
|
|
235
|
+
# Libraries that decorate rendered output rather than produce it --
|
|
236
|
+
# Prism, Mermaid -- have to run after a patch, and again after every
|
|
237
|
+
# subsequent one, because morphdom replaces nodes and takes their
|
|
238
|
+
# decoration with it. Start hooks and re-renders are scheduled through
|
|
239
|
+
# setTimeout, so there is no moment after start() when a caller can
|
|
240
|
+
# simply assume the DOM is settled; this event is that signal.
|
|
241
|
+
def announce_render
|
|
242
|
+
return unless RUBY_ENGINE == "opal"
|
|
243
|
+
|
|
244
|
+
# :nocov:
|
|
245
|
+
%x{ document.dispatchEvent(new CustomEvent("sw:render")) }
|
|
246
|
+
# :nocov:
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|