samagotchi 0.154.1 → 0.156.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/CHANGELOG.md +29 -1
- data/bin/chi +1 -1
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +5 -5
- data/docs/internals/attached-context.md +9 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +642 -831
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +13 -6
- data/docs/internals/remote-sessions.md +3 -1
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/docs/system-one.md +3 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +7 -51
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/config.rb +17 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_window.rb +3 -3
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/engine/context_judge.rb +21 -119
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +30 -80
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +26 -13
- data/lib/samagotchi/engine/toolkit.rb +35 -191
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +166 -98
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +28 -11
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +215 -313
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +87 -49
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +7 -9
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +6 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/note_command.rb +1 -2
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/plugin/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +3 -3
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +3 -3
- data/lib/samagotchi/self_report.rb +7 -7
- data/lib/samagotchi/session.rb +50 -7
- data/lib/samagotchi/session_access/local.rb +7 -5
- data/lib/samagotchi/session_access/remote.rb +17 -3
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +138 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +189 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +26 -1540
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +3 -3
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +3 -3
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +15 -7
- data/lib/samagotchi/tool_declarations.rb +6 -6
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +96 -102
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/delegate.rb +10 -5
- data/lib/samagotchi/tools/delegate_result.rb +4 -4
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/registry.rb +64 -44
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +150 -102
- data/lib/samagotchi/tools/tool_surface.rb +187 -0
- data/lib/samagotchi/turn_context.rb +68 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
- data/lib/samagotchi/turn_stages/objectives.rb +150 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
- data/lib/samagotchi/turn_stages/warmup.rb +82 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/app.rb +15 -8
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +3 -3
- data/lib/samagotchi/web/session_summary.rb +3 -4
- data/lib/samagotchi/worker.rb +55 -27
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +29 -1
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -353,10 +353,10 @@ module Samagotchi
|
|
|
353
353
|
# "..", a %2F) is an unknown session before any handler makes a path.
|
|
354
354
|
# (/api/sessions/resolve and /summaries take no id.)
|
|
355
355
|
if req.path_info.start_with?("/api/sessions/") && !m.captures.empty? && !Session.valid_id?(m.captures.first)
|
|
356
|
-
return
|
|
356
|
+
return session_not_found(m.captures.first)
|
|
357
357
|
end
|
|
358
358
|
|
|
359
|
-
return session_route(principal) { send(handler, req, *m.captures) } if kind == :session
|
|
359
|
+
return session_route(principal, m.captures.first) { send(handler, req, *m.captures) } if kind == :session
|
|
360
360
|
|
|
361
361
|
return send(handler, req, *m.captures)
|
|
362
362
|
end
|
|
@@ -383,11 +383,14 @@ module Samagotchi
|
|
|
383
383
|
# raises (Session.load, the manager's find) is a 404 not_found with
|
|
384
384
|
# its message. Only these routes: elsewhere an ArgumentError is a bug,
|
|
385
385
|
# #route's 500. A handler's own rescues run first.
|
|
386
|
-
def session_route(principal = nil)
|
|
386
|
+
def session_route(principal = nil, id = nil)
|
|
387
387
|
yield
|
|
388
388
|
rescue ArgumentError => e
|
|
389
|
-
|
|
390
|
-
|
|
389
|
+
return error_response(404, "not_found", e.message) if principal.nil? || principal.owner?
|
|
390
|
+
|
|
391
|
+
# A participant's 404 never carries a lookup's words (a path): it
|
|
392
|
+
# names their own session's id (valid_id? passed), nothing else.
|
|
393
|
+
session_not_found(id)
|
|
391
394
|
end
|
|
392
395
|
|
|
393
396
|
# API requests at debug level (not the page and its assets): method,
|
|
@@ -1152,7 +1155,7 @@ module Samagotchi
|
|
|
1152
1155
|
Degrade.call(:web, "model_sampling_unreadable") do
|
|
1153
1156
|
return if registry.nil? || models.empty?
|
|
1154
1157
|
|
|
1155
|
-
settings =
|
|
1158
|
+
settings = ModelSettings.all
|
|
1156
1159
|
models.each do |model|
|
|
1157
1160
|
target = registry.resolve(model[:name])
|
|
1158
1161
|
names = registry.lookup_names(model[:name], target: target)
|
|
@@ -1214,7 +1217,7 @@ module Samagotchi
|
|
|
1214
1217
|
@models_mutex.synchronize do
|
|
1215
1218
|
next @registry if @registry_given
|
|
1216
1219
|
|
|
1217
|
-
hosts =
|
|
1220
|
+
hosts = HostConfig.all
|
|
1218
1221
|
if @registry.nil? || hosts != @registry_hosts
|
|
1219
1222
|
@registry = HostRegistry.new(hosts_config: hosts)
|
|
1220
1223
|
@registry_hosts = hosts
|
|
@@ -1560,7 +1563,7 @@ module Samagotchi
|
|
|
1560
1563
|
begin
|
|
1561
1564
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
1562
1565
|
rescue ArgumentError
|
|
1563
|
-
return
|
|
1566
|
+
return session_not_found(id)
|
|
1564
1567
|
end
|
|
1565
1568
|
# Its terminal's alone: a tab's stream would keep its worker up.
|
|
1566
1569
|
return scratch_refused(id) if session.scratch
|
|
@@ -1976,6 +1979,10 @@ module Samagotchi
|
|
|
1976
1979
|
error_response(404, "not_found", "not found: #{path}")
|
|
1977
1980
|
end
|
|
1978
1981
|
|
|
1982
|
+
# 404 not_found for an id (or prefix) that names no session, in
|
|
1983
|
+
# Session::NotFound's words (SessionAccess::Remote maps it back).
|
|
1984
|
+
def session_not_found(given) = error_response(404, "not_found", Session::NotFound.new(given).message)
|
|
1985
|
+
|
|
1979
1986
|
# One tab's GET /api/events: a bounded queue behind a lambda sink,
|
|
1980
1987
|
# subscribed and snapshotted as one step under the hub's lock, then
|
|
1981
1988
|
# drained onto the socket. The loop ends when the hub stops or the
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "nokogiri"
|
|
4
4
|
require "uri"
|
|
5
5
|
require_relative "../block_renderers"
|
|
6
|
+
require_relative "../inline_widgets"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
module Web
|
|
@@ -25,6 +26,12 @@ module Samagotchi
|
|
|
25
26
|
extension: { header_ids: nil }
|
|
26
27
|
}.freeze
|
|
27
28
|
|
|
29
|
+
# A code block as the parse tree has it: its language (nil for an indented
|
|
30
|
+
# block or a bare fence) and its literal, the source between the fences.
|
|
31
|
+
Fence = Data.define(:language, :literal)
|
|
32
|
+
# A JSON string's < > & as \u escapes: the same JSON once parsed.
|
|
33
|
+
JSON_MARKUP = { "<" => "\\u003c", ">" => "\\u003e", "&" => "\\u0026" }.freeze
|
|
34
|
+
|
|
28
35
|
def initialize(enabled: false)
|
|
29
36
|
@enabled = enabled
|
|
30
37
|
end
|
|
@@ -61,7 +68,7 @@ module Samagotchi
|
|
|
61
68
|
})
|
|
62
69
|
return nil unless html
|
|
63
70
|
|
|
64
|
-
sanitize(html,
|
|
71
|
+
sanitize(html, fences(markdown.to_s))
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
private
|
|
@@ -81,17 +88,17 @@ module Samagotchi
|
|
|
81
88
|
@commonmarker_installed = Gem::Specification.find_all_by_name("commonmarker").any?
|
|
82
89
|
end
|
|
83
90
|
|
|
84
|
-
# Every code block
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
def
|
|
91
|
+
# Every code block, in document order: the highlighter drops the language
|
|
92
|
+
# from the HTML, the parse tree has it. [] for a text with no fence: its
|
|
93
|
+
# blocks have no language.
|
|
94
|
+
def fences(markdown)
|
|
88
95
|
return [] unless markdown.include?("```") || markdown.include?("~~~")
|
|
89
96
|
|
|
90
|
-
|
|
97
|
+
fences = []
|
|
91
98
|
Commonmarker.parse(markdown, options: OPTIONS).walk do |node|
|
|
92
|
-
|
|
99
|
+
fences << Fence.new(language: language_of(node.fence_info), literal: node.string_content) if node.type == :code_block
|
|
93
100
|
end
|
|
94
|
-
|
|
101
|
+
fences
|
|
95
102
|
end
|
|
96
103
|
|
|
97
104
|
def language_of(info)
|
|
@@ -100,16 +107,34 @@ module Samagotchi
|
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
# After the walk, which drops every attribute it doesn't know. Blocks and
|
|
103
|
-
#
|
|
110
|
+
# fences pair by position; a count that differs says the two parses
|
|
104
111
|
# disagree, and then no block gets one (it shows as source).
|
|
105
|
-
def mark_languages(fragment,
|
|
112
|
+
def mark_languages(fragment, fences)
|
|
106
113
|
blocks = fragment.css("pre")
|
|
107
|
-
return unless blocks.size ==
|
|
114
|
+
return unless blocks.size == fences.size
|
|
115
|
+
|
|
116
|
+
blocks.zip(fences) do |pre, fence|
|
|
117
|
+
next unless fence.language
|
|
118
|
+
|
|
119
|
+
pre["data-lang"] = fence.language
|
|
120
|
+
mark_widget(pre, fence.literal) if fence.language == InlineWidgets::LANG
|
|
121
|
+
end
|
|
122
|
+
end
|
|
108
123
|
|
|
109
|
-
|
|
124
|
+
# A chi-ui block carries what the page draws or why it doesn't. The JSON
|
|
125
|
+
# is an attribute value (Nokogiri escapes its quotes), and its < > & are
|
|
126
|
+
# \u escapes, so the HTML holds no model markup even as attribute text.
|
|
127
|
+
def mark_widget(pre, literal)
|
|
128
|
+
widget = InlineWidgets.parse(literal)
|
|
129
|
+
if widget.ok?
|
|
130
|
+
pre["data-widget"] = widget.widget_json.gsub(/[<>&]/, JSON_MARKUP)
|
|
131
|
+
else
|
|
132
|
+
pre["data-widget-error"] = widget.error
|
|
133
|
+
end
|
|
134
|
+
pre["data-widget-lenient"] = "" if widget.lenient?
|
|
110
135
|
end
|
|
111
136
|
|
|
112
|
-
def sanitize(html,
|
|
137
|
+
def sanitize(html, fences)
|
|
113
138
|
fragment = Nokogiri::HTML5.fragment(html)
|
|
114
139
|
fragment.traverse do |node|
|
|
115
140
|
next unless node.element?
|
|
@@ -122,7 +147,7 @@ module Samagotchi
|
|
|
122
147
|
sanitize_attributes(node)
|
|
123
148
|
end
|
|
124
149
|
end
|
|
125
|
-
mark_languages(fragment,
|
|
150
|
+
mark_languages(fragment, fences)
|
|
126
151
|
fragment.to_html
|
|
127
152
|
end
|
|
128
153
|
|
|
@@ -172,7 +172,7 @@ module Samagotchi
|
|
|
172
172
|
# id, which a card's stamp names (Bridge::CardPlaces#opening_of).
|
|
173
173
|
opening_id = m[:opening_id] || m["opening_id"]
|
|
174
174
|
message[:opening_id] = opening_id.to_s if norm_role == "user" && opening_id && !turn_id && !message[:merged]
|
|
175
|
-
# The turn it revised (a Revise stack,
|
|
175
|
+
# The turn it revised (a Revise stack, TurnStages::VerifyAnchors#stamp_opening), and
|
|
176
176
|
# whether it was a Verify turn (the page offers no Revise on its
|
|
177
177
|
# answer).
|
|
178
178
|
if norm_role == "user" && turn_id && !message[:merged]
|
|
@@ -185,7 +185,7 @@ module Samagotchi
|
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
def unknown_session(id)
|
|
188
|
-
|
|
188
|
+
session_not_found(id)
|
|
189
189
|
end
|
|
190
190
|
|
|
191
191
|
# 403 not_permitted for a participant (the owner is never refused by
|
|
@@ -388,7 +388,7 @@ module Samagotchi
|
|
|
388
388
|
def participant_resolve(grant, given)
|
|
389
389
|
return json_response(200, { id: grant.session_id }) if grant.session_id.start_with?(given)
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
session_not_found(given)
|
|
392
392
|
end
|
|
393
393
|
|
|
394
394
|
# POST …/wait/prepare for a participant (an agent's chi --remote
|
|
@@ -71,6 +71,7 @@ import { createAnnotateBar } from "./annotate_bar.js";
|
|
|
71
71
|
import { VERIFY_PRESET, VERIFY_UNAVAILABLE, verifyWorkerUnavailable, withVerify } from "./verify_preset.js";
|
|
72
72
|
import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
|
|
73
73
|
import { createBlockHost } from "./block_host.js";
|
|
74
|
+
import { createWidgetDrawer } from "./inline_widgets.js";
|
|
74
75
|
import { copyText, installCopy } from "./copy.js";
|
|
75
76
|
import { installAnswerFold, sweepAnswerFolds } from "./answer_fold.js";
|
|
76
77
|
import { installDetailsMotion } from "./details_motion.js";
|
|
@@ -1856,7 +1857,10 @@ historyEl.addEventListener("click", (e) => {
|
|
|
1856
1857
|
});
|
|
1857
1858
|
turnDom.scrollEl.addEventListener("scroll", annotateBar.schedule, true);
|
|
1858
1859
|
turnRoots().forEach((root) => installCopy(root));
|
|
1859
|
-
|
|
1860
|
+
// chi-ui blocks (inline_widgets.js) are a core drawer, ahead of the bundles'
|
|
1861
|
+
// renderers; a picks' Fill reply only fills the composer.
|
|
1862
|
+
const widgets = createWidgetDrawer({ roots: turnRoots(), composerText: () => promptEl.value, fillComposer });
|
|
1863
|
+
createBlockHost({ roots: turnRoots(), sessionId: () => store.selected, drawers: [widgets] });
|
|
1860
1864
|
// A steps block, a thinking block and the recap open and close by hand
|
|
1861
1865
|
// with their height animated (details_motion.js), in the history and on
|
|
1862
1866
|
// the stage.
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
// Draws a bundle's fenced blocks in the page (docs/internals/block-renderers.md)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// answer_display, Revise's clones, a step's swap) arrives the same way, and
|
|
5
|
-
// cards, the asking slot and question replies are never touched.
|
|
1
|
+
// Draws a bundle's fenced blocks in the page (docs/internals/block-renderers.md),
|
|
2
|
+
// in a hidden frame per language. Finding and watching the blocks is
|
|
3
|
+
// block_watch.js's; this is its frame drawer, after any core drawers.
|
|
6
4
|
//
|
|
7
5
|
// The DOM is the state: a <figure class="block-drawn"> right before the <pre>
|
|
8
6
|
// (block_source.js) says it is drawn, so a Revise clone needs nothing, and
|
|
@@ -16,16 +14,13 @@
|
|
|
16
14
|
import { html, render as litRender } from "./vendor/lit-html.js";
|
|
17
15
|
import { createBlockFrame, FRAME_TIMEOUT_MS } from "./block_frame.js";
|
|
18
16
|
import { DRAWN, figureOf, setSourceShown } from "./block_source.js";
|
|
17
|
+
import { createBlockWatch, failBlock, holdingView } from "./block_watch.js";
|
|
19
18
|
import { copyText } from "./copy.js";
|
|
20
|
-
import { keepFollowing, shouldFollowScroll } from "./scroll.js";
|
|
21
|
-
import { TEXTS } from "./text_roots.js";
|
|
22
19
|
|
|
23
|
-
const BLOCKS = "pre[data-lang]";
|
|
24
20
|
// MessageDisplay::MAX_RENDER_BYTES's number: a source over it stays source.
|
|
25
21
|
export const MAX_SOURCE_BYTES = 64 * 1024;
|
|
26
22
|
export const CACHE_SIZE = 200;
|
|
27
23
|
export const MARK_PREFIX = "chi_block_rendering:";
|
|
28
|
-
const IO_MARGIN = "600px 0px";
|
|
29
24
|
const MAX_RETRIES = 5;
|
|
30
25
|
// The biggest picture a page shows, whatever the renderer: a model-written
|
|
31
26
|
// directive can ask Mermaid for an SVG tens of millions of px tall, which breaks
|
|
@@ -126,43 +121,6 @@ export function currentTheme(doc = document, win = window) {
|
|
|
126
121
|
};
|
|
127
122
|
}
|
|
128
123
|
|
|
129
|
-
// The nearest scroller holding +el+, +root+ at the latest. IntersectionObserver
|
|
130
|
-
// needs it named: with root:null a rootMargin doesn't pass an overflow:auto
|
|
131
|
-
// between, so a block just under #history's fold wouldn't count.
|
|
132
|
-
function scrollerOf(el, root, win) {
|
|
133
|
-
for (let at = el.parentElement; at && at !== root; at = at.parentElement) {
|
|
134
|
-
const { overflowY } = win.getComputedStyle(at);
|
|
135
|
-
if (overflowY === "auto" || overflowY === "scroll") return at;
|
|
136
|
-
}
|
|
137
|
-
return root;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// One IntersectionObserver per scroller. +el+ moved to another scroller (the
|
|
141
|
-
// stage's hand-off to the history) moves with it.
|
|
142
|
-
function ioWatcher(win) {
|
|
143
|
-
const observers = new Map();
|
|
144
|
-
const watched = new WeakMap();
|
|
145
|
-
return (el, scroller, onNear) => {
|
|
146
|
-
let io = observers.get(scroller);
|
|
147
|
-
if (!io) {
|
|
148
|
-
io = new win.IntersectionObserver((entries) => {
|
|
149
|
-
for (const entry of entries) {
|
|
150
|
-
if (!entry.isIntersecting) continue;
|
|
151
|
-
io.unobserve(entry.target);
|
|
152
|
-
const near = watched.get(entry.target);
|
|
153
|
-
watched.delete(entry.target);
|
|
154
|
-
near?.onNear();
|
|
155
|
-
}
|
|
156
|
-
}, { root: scroller, rootMargin: IO_MARGIN });
|
|
157
|
-
observers.set(scroller, io);
|
|
158
|
-
}
|
|
159
|
-
const before = watched.get(el);
|
|
160
|
-
if (before && before.io !== io) before.io.unobserve(el);
|
|
161
|
-
watched.set(el, { io, onNear });
|
|
162
|
-
io.observe(el);
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
124
|
function guardedStorage(get) {
|
|
167
125
|
const use = (fn, fallback) => { try { return fn(get()); } catch (_) { return fallback; } };
|
|
168
126
|
return {
|
|
@@ -172,26 +130,22 @@ function guardedStorage(get) {
|
|
|
172
130
|
};
|
|
173
131
|
}
|
|
174
132
|
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
133
|
+
|
|
134
|
+
// The drawer of a bundle's renderers (block_watch.js has the contract): one
|
|
135
|
+
// frame per language, waiting for /api/block_renderers to say who draws what.
|
|
136
|
+
// @param roots the turn roots, for the view held and the zoom's listeners
|
|
137
|
+
// The rest are createBlockHost's.
|
|
138
|
+
export function createFrameDrawer({
|
|
139
|
+
roots, listRenderers = fetchRendererList, fetchFile = fetchRendererFile, makeFrame = createBlockFrame,
|
|
181
140
|
decodeImage = async (url) => { const img = new win.Image(); img.src = url; await img.decode(); return { width: img.naturalWidth, height: img.naturalHeight }; },
|
|
182
|
-
decodeTimeoutMs = DECODE_TIMEOUT_MS, theme = currentTheme,
|
|
183
|
-
|
|
141
|
+
decodeTimeoutMs = DECODE_TIMEOUT_MS, theme = currentTheme,
|
|
142
|
+
doc = document, win = window, storage = guardedStorage(() => globalThis.sessionStorage),
|
|
184
143
|
}) {
|
|
185
|
-
const near = watch || ioWatcher(win);
|
|
186
144
|
const cache = new Map();
|
|
187
145
|
const frames = new Map();
|
|
188
146
|
// A language whose renderer didn't load, and why: until the session changes,
|
|
189
147
|
// its blocks don't fetch it again one by one.
|
|
190
148
|
const broken = new Map();
|
|
191
|
-
// Blocks with a draw on its way (not a drawn state: that is the figure): pre =>
|
|
192
|
-
// {target, scroller, start, drawing}. Until it starts, a block whose scroller
|
|
193
|
-
// changed (the stage's hand-off to the history) gets watched there.
|
|
194
|
-
const waiting = new WeakMap();
|
|
195
149
|
// The marks of the renders running now: mark => when it started. Such a mark isn't
|
|
196
150
|
// a leftover from an earlier load.
|
|
197
151
|
const inFlight = new Map();
|
|
@@ -199,17 +153,19 @@ export function createBlockHost({
|
|
|
199
153
|
// render has its blocks wait for that one, and each draws into whichever is connected.
|
|
200
154
|
const coming = new Map();
|
|
201
155
|
const retries = new WeakMap();
|
|
156
|
+
// What needsDraw checked, so draw draws that: pre => {lang, source, renderer}.
|
|
157
|
+
const checked = new WeakMap();
|
|
202
158
|
let list = null;
|
|
203
159
|
let listLoad = null;
|
|
204
|
-
let session = sessionId();
|
|
205
160
|
|
|
206
|
-
|
|
207
|
-
|
|
161
|
+
// The list once loaded; till then a promise of it, asked for once per page load.
|
|
162
|
+
function renderers() {
|
|
163
|
+
if (list) return list;
|
|
208
164
|
listLoad ??= listRenderers().then(
|
|
209
|
-
(
|
|
165
|
+
(got) => { list = new Map((got || []).map((r) => [r.lang, r])); return list; },
|
|
210
166
|
() => { list = new Map(); return list; }, // a failed list: no drawing
|
|
211
167
|
);
|
|
212
|
-
listLoad
|
|
168
|
+
return listLoad;
|
|
213
169
|
}
|
|
214
170
|
|
|
215
171
|
function dropFrames() {
|
|
@@ -218,13 +174,6 @@ export function createBlockHost({
|
|
|
218
174
|
broken.clear();
|
|
219
175
|
}
|
|
220
176
|
|
|
221
|
-
function noticeSession() {
|
|
222
|
-
const now = sessionId();
|
|
223
|
-
if (now === session) return;
|
|
224
|
-
session = now;
|
|
225
|
-
dropFrames();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
177
|
function frameFor(lang, renderer) {
|
|
229
178
|
let frame = frames.get(lang);
|
|
230
179
|
if (frame && !frame.dead()) return frame;
|
|
@@ -242,24 +191,7 @@ export function createBlockHost({
|
|
|
242
191
|
|
|
243
192
|
const cacheKey = (lang, themeName, source) => `${lang}\u0000${themeName}\u0000${source}`;
|
|
244
193
|
const markKey = (lang, source) => `${MARK_PREFIX}${lang}:${sourceHash(source)}`;
|
|
245
|
-
|
|
246
|
-
function rootOf(el) {
|
|
247
|
-
return roots.find((root) => root.contains(el)) || roots[0];
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// +mutate+ with the view held: a viewer at the bottom stays there, and a
|
|
251
|
-
// block above a reader's view keeps what they read where it is. Browsers that
|
|
252
|
-
// anchor scrolling have done it by the second read, and there is nothing left.
|
|
253
|
-
function holdingView(pre, mutate, bottomOf) {
|
|
254
|
-
const scroller = scrollerOf(pre, rootOf(pre), win);
|
|
255
|
-
if (shouldFollowScroll(scroller)) return keepFollowing(scroller, mutate);
|
|
256
|
-
const before = pre.getBoundingClientRect().bottom;
|
|
257
|
-
if (before > scroller.getBoundingClientRect().top) return mutate();
|
|
258
|
-
const result = mutate();
|
|
259
|
-
const after = bottomOf();
|
|
260
|
-
if (after !== before) scroller.scrollTop += after - before;
|
|
261
|
-
return result;
|
|
262
|
-
}
|
|
194
|
+
const langOf = (pre) => pre.getAttribute("data-lang");
|
|
263
195
|
|
|
264
196
|
function setMark(mark) {
|
|
265
197
|
storage.set(mark);
|
|
@@ -347,28 +279,10 @@ export function createBlockHost({
|
|
|
347
279
|
return;
|
|
348
280
|
}
|
|
349
281
|
const figure = makeFigure(picture, themeName, lang);
|
|
350
|
-
holdingView(pre, () => {
|
|
282
|
+
holdingView(pre, roots, () => {
|
|
351
283
|
pre.before(figure);
|
|
352
284
|
setSourceShown(figure, pre, false);
|
|
353
|
-
}, () => figure.getBoundingClientRect().bottom);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// The <pre> stays as it is, under one muted line: never markup (the words are
|
|
357
|
-
// a renderer's, driven by the model's input), and shown by CSS, so a bubble's
|
|
358
|
-
// textContent, its dedupe and an annotation's text stay what they were (copy.js's
|
|
359
|
-
// rule for its buttons).
|
|
360
|
-
function fail(pre, lang, message) {
|
|
361
|
-
if (!pre.isConnected) return;
|
|
362
|
-
pre.dataset.blockFailed = "1";
|
|
363
|
-
const note = doc.createElement("div");
|
|
364
|
-
note.className = "block-note";
|
|
365
|
-
const first = String(message ?? "").split("\n")[0].trim();
|
|
366
|
-
const words = `Couldn't draw this ${lang} block${first ? `: ${first}` : ""}`;
|
|
367
|
-
note.dataset.text = words;
|
|
368
|
-
note.setAttribute("role", "note");
|
|
369
|
-
note.setAttribute("aria-label", words);
|
|
370
|
-
note.title = "Click to copy";
|
|
371
|
-
pre.after(note);
|
|
285
|
+
}, () => figure.getBoundingClientRect().bottom, win);
|
|
372
286
|
}
|
|
373
287
|
|
|
374
288
|
function pictureFor(key, lang, source, renderer, now) {
|
|
@@ -407,21 +321,37 @@ export function createBlockHost({
|
|
|
407
321
|
}
|
|
408
322
|
}
|
|
409
323
|
|
|
410
|
-
|
|
324
|
+
// A claimed language has a renderer: the list is loaded by then.
|
|
325
|
+
function needsDraw(pre, figure) {
|
|
326
|
+
const lang = langOf(pre);
|
|
327
|
+
const source = pre.textContent;
|
|
328
|
+
const now = theme(doc, win);
|
|
329
|
+
if (figure && figure.dataset.theme === now.name) return false;
|
|
330
|
+
const hit = cache.get(cacheKey(lang, now.name, source));
|
|
331
|
+
if (hit) { place(pre, hit, now.name, lang); return false; } // no flash: this frame
|
|
332
|
+
if (!figure) {
|
|
333
|
+
if (new TextEncoder().encode(source).length > MAX_SOURCE_BYTES) { failBlock(pre, lang, "it is over 64 KB"); return false; }
|
|
334
|
+
const mark = markKey(lang, source);
|
|
335
|
+
if (storage.has(mark) && !inFlight.has(mark)) { failBlock(pre, lang, "it hung or took too long the last time"); return false; }
|
|
336
|
+
}
|
|
337
|
+
checked.set(pre, { lang, source, renderer: list.get(lang) });
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function draw(pre, { reconsider }) {
|
|
342
|
+
const { lang, source, renderer } = checked.get(pre);
|
|
411
343
|
const now = theme(doc, win);
|
|
412
344
|
const key = cacheKey(lang, now.name, source);
|
|
413
345
|
const hit = cache.get(key);
|
|
414
|
-
if (hit) {
|
|
415
|
-
if (broken.has(lang)) {
|
|
346
|
+
if (hit) { place(pre, hit, now.name, lang); return; }
|
|
347
|
+
if (broken.has(lang)) { failBlock(pre, lang, broken.get(lang)); return; }
|
|
416
348
|
let retry = false;
|
|
417
349
|
try {
|
|
418
350
|
place(pre, await pictureFor(key, lang, source, renderer, now), now.name, lang);
|
|
419
351
|
} catch (e) {
|
|
420
352
|
// A redraw that failed keeps the picture it has.
|
|
421
|
-
if (!e?.dropped && !figureOf(pre))
|
|
353
|
+
if (!e?.dropped && !figureOf(pre)) failBlock(pre, lang, e?.message);
|
|
422
354
|
retry = !!e?.dropped;
|
|
423
|
-
} finally {
|
|
424
|
-
waiting.delete(pre);
|
|
425
355
|
}
|
|
426
356
|
// Queued behind a frame that died (another block's timeout): a new frame's
|
|
427
357
|
// turn. A session switch's frames go with their blocks, which aren't connected.
|
|
@@ -430,100 +360,45 @@ export function createBlockHost({
|
|
|
430
360
|
if (!retry) return;
|
|
431
361
|
const tries = (retries.get(pre) || 0) + 1;
|
|
432
362
|
retries.set(pre, tries);
|
|
433
|
-
if (tries <= MAX_RETRIES)
|
|
434
|
-
else
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// A copy.js wrap took the <pre> away from a figure placed first: back with it.
|
|
438
|
-
function rejoin(pre) {
|
|
439
|
-
if (figureOf(pre) || !pre.hidden) return;
|
|
440
|
-
const wrap = pre.parentElement;
|
|
441
|
-
const stray = wrap?.classList.contains("code-wrap") && wrap.previousElementSibling;
|
|
442
|
-
if (stray?.classList?.contains(DRAWN)) wrap.insertBefore(stray, pre);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function watchFor(entry) {
|
|
446
|
-
const scroller = scrollerOf(entry.target, rootOf(entry.target), win);
|
|
447
|
-
if (scroller === entry.scroller) return;
|
|
448
|
-
entry.scroller = scroller;
|
|
449
|
-
near(entry.target, scroller, entry.start);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function consider(pre) {
|
|
453
|
-
if (!pre.isConnected || !pre.closest(TEXTS) || pre.dataset.blockFailed) return;
|
|
454
|
-
const pending = waiting.get(pre);
|
|
455
|
-
if (pending) {
|
|
456
|
-
if (!pending.drawing) watchFor(pending);
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
rejoin(pre);
|
|
460
|
-
const lang = pre.getAttribute("data-lang");
|
|
461
|
-
const source = pre.textContent;
|
|
462
|
-
if (!source.trim()) return; // an empty fence is left alone
|
|
463
|
-
withRenderers((renderers) => {
|
|
464
|
-
const renderer = renderers.get(lang);
|
|
465
|
-
if (!renderer || !pre.isConnected || waiting.has(pre) || pre.dataset.blockFailed) return;
|
|
466
|
-
const now = theme(doc, win);
|
|
467
|
-
const figure = figureOf(pre);
|
|
468
|
-
if (figure && figure.dataset.theme === now.name) return;
|
|
469
|
-
const hit = cache.get(cacheKey(lang, now.name, source));
|
|
470
|
-
if (hit) { place(pre, hit, now.name, lang); return; } // no flash: this frame
|
|
471
|
-
if (!figure) {
|
|
472
|
-
if (new TextEncoder().encode(source).length > MAX_SOURCE_BYTES) { fail(pre, lang, "it is over 64 KB"); return; }
|
|
473
|
-
const mark = markKey(lang, source);
|
|
474
|
-
if (storage.has(mark) && !inFlight.has(mark)) { fail(pre, lang, "it hung or took too long the last time"); return; }
|
|
475
|
-
}
|
|
476
|
-
// A drawn block's <pre> is hidden, which never intersects: its figure does.
|
|
477
|
-
const entry = {
|
|
478
|
-
target: figure || pre,
|
|
479
|
-
scroller: null,
|
|
480
|
-
drawing: false,
|
|
481
|
-
start: () => { entry.drawing = true; draw(pre, lang, source, renderer); },
|
|
482
|
-
};
|
|
483
|
-
waiting.set(pre, entry);
|
|
484
|
-
watchFor(entry);
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function scan(el) {
|
|
489
|
-
if (el.matches?.(BLOCKS)) consider(el);
|
|
490
|
-
for (const pre of el.querySelectorAll?.(BLOCKS) || []) consider(pre);
|
|
363
|
+
if (tries <= MAX_RETRIES) reconsider(pre);
|
|
364
|
+
else failBlock(pre, lang, "its renderer keeps stopping");
|
|
491
365
|
}
|
|
492
366
|
|
|
493
367
|
for (const root of roots) {
|
|
494
|
-
new win.MutationObserver((records) => {
|
|
495
|
-
noticeSession();
|
|
496
|
-
for (const record of records) {
|
|
497
|
-
for (const node of record.addedNodes) if (node.nodeType === 1) scan(node);
|
|
498
|
-
}
|
|
499
|
-
}).observe(root, { childList: true, subtree: true });
|
|
500
|
-
scan(root);
|
|
501
|
-
|
|
502
368
|
root.addEventListener("mousedown", (e) => {
|
|
503
|
-
if (e.target.closest?.(".block-
|
|
369
|
+
if (e.target.closest?.(".block-zoom")) e.preventDefault();
|
|
504
370
|
});
|
|
505
371
|
// Delegated, so a Revise clone's work too.
|
|
506
372
|
root.addEventListener("click", (e) => {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
e.stopPropagation();
|
|
511
|
-
const figure = toggle.parentElement;
|
|
512
|
-
setSourceShown(figure, figure.nextElementSibling, !figure.classList.contains("source"));
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
const note = e.target.closest?.(".block-note");
|
|
516
|
-
if (note) { copy(note.dataset.text); return; }
|
|
517
|
-
if (e.target.closest?.(".block-zoom, .block-img")) {
|
|
518
|
-
const figure = e.target.closest(`.${DRAWN}`);
|
|
519
|
-
setZoomed(figure, !figure.classList.contains("natural"));
|
|
520
|
-
}
|
|
373
|
+
if (!e.target.closest?.(".block-zoom, .block-img")) return;
|
|
374
|
+
const figure = e.target.closest(`.${DRAWN}`);
|
|
375
|
+
setZoomed(figure, !figure.classList.contains("natural"));
|
|
521
376
|
});
|
|
522
377
|
}
|
|
523
378
|
|
|
379
|
+
return {
|
|
380
|
+
claims(lang) {
|
|
381
|
+
const known = renderers();
|
|
382
|
+
return known.then ? known.then((got) => got.has(lang)) : known.has(lang);
|
|
383
|
+
},
|
|
384
|
+
needsDraw,
|
|
385
|
+
draw,
|
|
386
|
+
sessionChanged: dropFrames,
|
|
387
|
+
dropFrames,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// @param roots the turn roots (app.js turnRoots())
|
|
392
|
+
// @param sessionId () => the open session's id: the frames go on a change, to
|
|
393
|
+
// limit what they hold (docs/internals/block-renderers.md, residual risk)
|
|
394
|
+
// @param drawers core drawers, asked before the bundle renderers
|
|
395
|
+
// The rest are seams for the specs.
|
|
396
|
+
export function createBlockHost({ roots, sessionId, drawers = [], copy = copyText, watch = null, win = window, ...frameOpts }) {
|
|
397
|
+
const frames = createFrameDrawer({ roots, win, ...frameOpts });
|
|
398
|
+
const watcher = createBlockWatch({ roots, sessionId, drawers: [...drawers, frames], watch, copy, win });
|
|
399
|
+
// A system theme change redraws through the figure (needsDraw's theme check).
|
|
524
400
|
win.matchMedia?.("(prefers-color-scheme: dark)").addEventListener?.("change", () => {
|
|
525
|
-
for (const root of roots) scan(root);
|
|
401
|
+
for (const root of roots) watcher.scan(root);
|
|
526
402
|
});
|
|
527
|
-
|
|
528
|
-
return { scan, dropFrames };
|
|
403
|
+
return { scan: watcher.scan, dropFrames: frames.dropFrames };
|
|
529
404
|
}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
.block-img{display:block;max-width:100%;max-height:50000px;object-fit:contain;height:auto;margin:0 auto;cursor:zoom-in}
|
|
9
9
|
.block-drawn.natural .block-img{max-width:50000px;margin:0;cursor:zoom-out}
|
|
10
10
|
.block-drawn.source .block-scroll{display:none}
|
|
11
|
+
/* A chi-ui widget (inline_widgets.js): its host holds the shadow root, whose own sheet is widgets.css; the source hides it as the picture. */
|
|
12
|
+
.block-drawn.source>.widget-host{display:none}
|
|
11
13
|
.block-toggle,.block-zoom{position:absolute;top:5px;right:5px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-3);opacity:0;transition:opacity .12s}
|
|
12
14
|
.code-wrap>.block-drawn>.block-toggle{right:34px}
|
|
13
15
|
.block-zoom{right:34px}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// A drawn block's two faces (block_host.js): the picture, or its source. The
|
|
1
|
+
// A drawn block's two faces (block_watch.js, block_host.js): the picture, or its source. The
|
|
2
2
|
// source <pre> stays in the DOM right after the figure, `hidden` while the
|
|
3
3
|
// picture shows, so whatever reads text (a claim's walk, copy) sees what it
|
|
4
4
|
// always did. The DOM is the state: a Revise clone carries both with it.
|
|
@@ -18,15 +18,18 @@ export function figureOf(pre) {
|
|
|
18
18
|
return before?.classList?.contains(DRAWN) ? before : null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// The source shown (and the picture hidden), or the other way round.
|
|
21
|
+
// The source shown (and the picture hidden), or the other way round. A
|
|
22
|
+
// figure that isn't a picture names its own face (data-toggle-picture: a
|
|
23
|
+
// chi-ui widget's "Show the widget").
|
|
22
24
|
export function setSourceShown(figure, pre, shown) {
|
|
23
25
|
pre.hidden = !shown;
|
|
24
26
|
figure.classList.toggle("source", shown);
|
|
25
27
|
const toggle = figure.querySelector(":scope > .block-toggle");
|
|
26
28
|
if (!toggle) return;
|
|
27
29
|
toggle.setAttribute("aria-pressed", shown ? "true" : "false");
|
|
28
|
-
|
|
29
|
-
toggle.
|
|
30
|
+
const hide = figure.dataset.togglePicture || HIDE;
|
|
31
|
+
toggle.setAttribute("aria-label", shown ? hide : SHOW);
|
|
32
|
+
toggle.title = shown ? hide : SHOW;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
// A jump that lands in a drawn block's source shows it (history_reveal.js).
|