unaltraweb 0.4.0 → 0.5.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +5 -5
  3. data/README.md +19 -7
  4. data/_plugins/figure_captions.rb +47 -10
  5. data/_sass/_documentation.scss +7 -5
  6. data/_sass/_manual.scss +7 -0
  7. data/docs/_documentation/en/02-tools.md +4 -4
  8. data/docs/_documentation/en/03-usage.md +61 -0
  9. data/docs/_documentation/en/13-unaltremanual.md +1 -1
  10. data/docs/_documentation/en/20-syntax.md +14 -0
  11. data/docs/_documentation/en/25-caption-credits.md +120 -0
  12. data/docs/_documentation/en/26-image-backgrounds.md +103 -0
  13. data/docs/_documentation/en/31-template.md +1 -1
  14. data/docs/_documentation/en/32-development.md +1 -1
  15. data/docs/_documentation/en/40-distribution.md +25 -5
  16. data/docs/_documentation/en/42-docker-image.md +7 -7
  17. data/docs/_documentation/en/43-workspace-path-policies.md +232 -0
  18. data/docs/_documentation/en/44-editorial-review.md +237 -0
  19. data/docs/agents/action-prompts/00-start-site-session.txt +10 -5
  20. data/docs/agents/action-prompts/22-manual-style-audit.txt +3 -1
  21. data/docs/agents/manual-authoring-components.md +38 -0
  22. data/docs/agents/mcp-contract.md +102 -10
  23. data/docs/agents/visual-companions-0.4.0.md +74 -0
  24. data/docs/assets/img/caption-credits-demo.svg +19 -0
  25. data/scripts/editorial_check.py +12 -0
  26. data/scripts/image_background_check.py +12 -0
  27. data/scripts/manual/build_pdf.py +94 -16
  28. data/scripts/manual/filters/figure-captions.lua +65 -10
  29. data/scripts/manual/templates/manual.tex +2 -1
  30. data/scripts/test_gem_build.py +32 -2
  31. data/scripts/test_reproducible_jekyll_build.py +1 -1
  32. data/scripts/test_wheel_install.py +28 -0
  33. data/scripts/unaltraweb-mcp-bootstrap.sh +1 -1
  34. data/scripts/validate_distribution.py +18 -3
  35. data/src/unaltraweb_mcp/component-contract.json +28 -28
  36. data/src/unaltraweb_mcp/editorial.py +495 -0
  37. data/src/unaltraweb_mcp/editorial_sources.py +504 -0
  38. data/src/unaltraweb_mcp/image_backgrounds.py +334 -0
  39. data/src/unaltraweb_mcp/image_probe.py +149 -0
  40. data/src/unaltraweb_mcp/processes.py +146 -0
  41. metadata +15 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f06f4a02759cbf9874ee57a4a15f26f4eb0f5247bad7778b04811b47f7d0cbce
4
- data.tar.gz: 8ca81f6f199be8023e4651efe8886499ef67ecf49fbcac6f202a760d0dbfbbcc
3
+ metadata.gz: e9b1d06c849d3893c28fa69a879e93f56acef7a742aa8e2d8ae7df78fa53d4a2
4
+ data.tar.gz: '0995fe545e4466b531edc89ed8c8a37ca707e847cc31a8a3acd3647d6b77ecdd'
5
5
  SHA512:
6
- metadata.gz: e3950450a0339c08473ec1ff3dd1edcdfb5336982990a441c80c94928b9e606bb51a5f4247fa8e09ab89a7621e56debeac4e479b98cdadbb68081caac7253552
7
- data.tar.gz: 7253948b9cb8adf42e43cdf45b92c67ed179b3c84d35ceccd280e883727369573e3e01e787b9df98eaf3970f5446b20be2bb17b3e0368f843da36106de9a43d1
6
+ metadata.gz: 06dd87a7428129eb5ab81655c892abddf159d223aefec27cbdf5e7b5512a72fe7940061313cec5ccee5032ac1b0343cc6b8390617106c59b76d979b109d73cfc
7
+ data.tar.gz: e2a1cc1fe578e4df0968d1f9b9fbc1abbb0cfed254d38c192e7ec24a55e254adf14c9ed57128255f5a18cbaacf68b4fc64a8c519c99d5f87e1a98798db42a1ca
data/Makefile CHANGED
@@ -1,9 +1,9 @@
1
1
  PYTHON ?= python3
2
2
  override PROJECT := $${MCP_CONSUMER_WORKSPACE:?MCP_CONSUMER_WORKSPACE is required}
3
3
  override PROJECT_ROOT := $(PROJECT)
4
- MCP_RUNTIME_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb:0.4.0
5
- MCP_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.4.0
6
- MCP_RELEASE_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:f3ab5542e6ece56487d4b8238a5e7abd89f36a0b8d87bf7bab19645e3ced1e58
4
+ MCP_RUNTIME_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb:0.5.0
5
+ MCP_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.5.0
6
+ MCP_RELEASE_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:389bc585cdb4fc89d3372f4896a55fe26e15df38b46bc114ce44fdb3f1c8deb9
7
7
  MCP_DOCKER_BUILD_NETWORK ?= default
8
8
  INIT_SITE_PROFILE ?= unaltreselfie
9
9
  NEW_WEB_PROFILE ?= unaltreselfie
@@ -40,8 +40,8 @@ WEB_CAPTURE_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-web-capture@sha256:0bf1b
40
40
  WEB_CAPTURE_DEV_IMAGE ?= unaltraweb-web-capture:dev
41
41
  WEB_CAPTURE_DOCKER_BUILD_NETWORK ?= default
42
42
  VEGAVISUALS_CLI ?=
43
- DOCKER_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb:0.4.0
44
- MANUAL_PDF_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf:0.4.0
43
+ DOCKER_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb:0.5.0
44
+ MANUAL_PDF_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:9e0b3a45753c170b795e9a9d6df61580085c113436beac5bf6c8de69b6562097
45
45
  MANUAL_PDF_DEV_IMAGE ?= unaltraweb-manual-pdf:dev
46
46
  MCP_SMOKE_MANUAL_PDF_IMAGE ?= $(MANUAL_PDF_IMAGE)
47
47
  MANUAL_PDF_LANG ?=
data/README.md CHANGED
@@ -34,7 +34,7 @@ The default distribution is Docker-first. Generated sites run their normal build
34
34
 
35
35
  ## Current Status
36
36
 
37
- - Public release `v0.3.0` provides the Docker images, Ruby gem and Python wheel as one receipt-bound distribution.
37
+ - Public release `v0.4.0` provides the Docker images, Ruby gem and Python wheel as one receipt-bound distribution.
38
38
  - `ghcr.io/dosquartsdedocs/unaltraweb-mcp` is the canonical normal local runtime for generated sites; `ghcr.io/dosquartsdedocs/unaltraweb` is its lower-level Jekyll runtime base.
39
39
  - The Ruby gem supports native Bundler/Jekyll consumers. The modular MCP wheel supports native creation and inspection, and does not bundle the gem, factory checkout, worker images or companion renderers.
40
40
  - PDF, browser-capture and computation environments remain separate images so the normal site image does not carry every heavy toolchain.
@@ -74,11 +74,11 @@ Create a child site directly with the public Docker image:
74
74
  mkdir my-site && \
75
75
  docker run --rm --network none --user "$(id -u):$(id -g)" -e HOME=/tmp \
76
76
  --mount "type=bind,src=${PWD}/my-site,dst=/workspace" \
77
- ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:f3ab5542e6ece56487d4b8238a5e7abd89f36a0b8d87bf7bab19645e3ced1e58 \
77
+ ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:389bc585cdb4fc89d3372f4896a55fe26e15df38b46bc114ce44fdb3f1c8deb9 \
78
78
  --project /workspace new-web --site-profile unaltreselfie --title "My site" --default-lang en
79
79
  ```
80
80
 
81
- The chained `mkdir` requires a new destination, the digest binds creation to the reviewed `v0.3.0` receipt, and `--network none` keeps scaffold generation offline.
81
+ The chained `mkdir` requires a new destination, the digest binds creation to the reviewed `v0.4.0` receipt, and `--network none` keeps scaffold generation offline.
82
82
 
83
83
  If Python package tooling is already available, the PyPI adapter exposes the equivalent native command:
84
84
 
@@ -92,7 +92,9 @@ From this factory checkout, the equivalent command is:
92
92
  MCP_CONSUMER_WORKSPACE=./my-site make mcp-new-web NEW_WEB_PROFILE=unaltreselfie SITE_TITLE="My site" DEFAULT_LANG=en
93
93
  ```
94
94
 
95
- All three creation paths use only assets shipped in `unaltraweb_mcp`, either in the image, wheel or factory checkout. They preflight all managed paths, write `.unaltraweb/scaffold.json`, and never overwrite differing files. Later `scaffold_sync` calls can update unchanged baseline controls or adopt exact current package bytes, including the collaboration contract, Dependabot policy, pull-request template, dependency pins, and deploy caller. They never touch config, README prose, agent guidance, or content. `dosquartsdedocs/unaltraweb-template` remains available when a full multi-profile demo with Playwright tests is more useful than a clean profile-specific site.
95
+ All three creation paths use only assets shipped in `unaltraweb_mcp`, either in the image, wheel or factory checkout. They preflight all managed paths, write `.unaltraweb/scaffold.json`, and never overwrite differing files. Later `scaffold_sync` calls can update unchanged baseline controls or adopt exact current package bytes, including the collaboration contract, Dependabot policy, pull-request template, dependency pins, and deploy caller. Local customizations are preserved when the package has not changed that file since the baseline; conflicting local/upstream changes remain blocked. Synchronization never touches config, README prose, agent guidance, or content. `dosquartsdedocs/unaltraweb-template` remains available when a full multi-profile demo with Playwright tests is more useful than a clean profile-specific site.
96
+
97
+ The guided update flow reports `site_context.update_status` at session start so the agent can explain available package/scaffold changes and ask whether to apply them. Accepted updates use the existing `scaffold_sync` transaction with the reviewed `plan_sha256` as `expected_plan_sha256`; an older MCP cannot downgrade known newer consumer pins. See [Guided Consumer Updates](docs/_documentation/en/03-usage.md#guided-consumer-updates). This functionality needs a reviewed MCP/package release containing it: updating a discovery checkout alone does not replace the already-running or digest-pinned public image.
96
98
 
97
99
  After creation, there are two supported editing paths:
98
100
 
@@ -127,7 +129,7 @@ make docs-serve DOCKER_IMAGE=unaltraweb:dev
127
129
  make docs-build DOCKER_IMAGE=unaltraweb:dev
128
130
  ```
129
131
 
130
- The candidate distribution contract selects `ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.4.0` for normal generated-site commands after coordinated publication. Until that release completes, `v0.3.0` and the digest-pinned `MCP_RELEASE_IMAGE` remain the public runtime. The mutable `:main` channel is reserved for explicit maintainer testing; locally built core images use the `:dev` name.
132
+ The public distribution contract selects `ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.4.0` for normal generated-site commands. `MCP_RELEASE_IMAGE` pins the reviewed MCP digest from the same `v0.4.0` receipt for gContExt (formerly ContExt), the GNOME Shell extension. The mutable `:main` channel is reserved for explicit maintainer testing; locally built core images use the `:dev` name.
131
133
 
132
134
  The lower-level `unaltraweb` image supplies Ruby, Jekyll and runtime dependencies. The MCP image layers the full reviewed factory and installed Python package on top; generated Make targets load the theme as a path gem from `/opt/unaltraweb`. Native Bundler consumers can instead resolve the independently published gem, and native Python users can install the wheel.
133
135
 
@@ -165,7 +167,7 @@ Doctor is offline. The optional `--docker` mode only calls local Docker version/
165
167
 
166
168
  ## Global Dockerized MCP
167
169
 
168
- `unaltraweb` provides one global, on-demand stdio MCP whose containers are scoped to the current consumer workspace. Each client session gets an independent Docker-generated container name plus stable factory, role, and project labels, so concurrent processes for the same project do not collide. This runtime capability does not authorize overlapping editors: the collaboration control plane uses one primary mutable checkout and one active editing session per repository. ContExt runs `mcp-build` to prepare the exact public image selected by `MCP_RELEASE_IMAGE`, and `mcp-stdio` launches that same digest. To build and test a development image explicitly from this checkout instead:
170
+ `unaltraweb` provides one global, on-demand stdio MCP whose containers are scoped to the current consumer workspace. Each client session gets an independent Docker-generated container name plus stable factory, role, and project labels, so concurrent processes for the same project do not collide. This runtime capability does not authorize overlapping editors: the collaboration control plane uses one primary mutable checkout and one active editing session per repository. gContExt runs `mcp-build` to prepare the exact public image selected by `MCP_RELEASE_IMAGE`, and `mcp-stdio` launches that same digest. To build and test a development image explicitly from this checkout instead:
169
171
 
170
172
  ```bash
171
173
  make mcp-image
@@ -174,7 +176,7 @@ make mcp-smoke-prebuilt MCP_IMAGE=unaltraweb-mcp:dev
174
176
 
175
177
  Source builds use the explicit local names `unaltraweb:dev` and `unaltraweb-mcp:dev` by default, avoiding shadowed public references. After each coordinated release, `MCP_RELEASE_IMAGE` advances to its recorded digest in a separate post-release change; candidate source never embeds its unknown future self-digest.
176
178
 
177
- ContExt reads the canonical manifest transport `make -C ${factoryRoot} mcp-stdio` and supplies `MCP_CONSUMER_WORKSPACE=${workspaceFolder}` through the process environment. The manifest continues to use `make`, an allowed container host launcher, but no consumer path is parsed by Make or interpolated into shell source. The collaboration control plane requests one top-level MCP, selects its declared dependency closure, preserves unrelated user registrations, and runs a read-only checkout preflight before editing. When a process-held cooperative lease is required, it launches the editing command through its `exec` wrapper; it never manipulates Git worktrees implicitly. An equivalent direct MCP launch is:
179
+ gContExt reads the canonical manifest transport `make -C ${factoryRoot} mcp-stdio` and supplies `MCP_CONSUMER_WORKSPACE=${workspaceFolder}` through the process environment. The manifest continues to use `make`, an allowed container host launcher, but no consumer path is parsed by Make or interpolated into shell source. The collaboration control plane requests one top-level MCP, selects its declared dependency closure, preserves unrelated user registrations, and runs a read-only checkout preflight before editing. When a process-held cooperative lease is required, it launches the editing command through its `exec` wrapper; it never manipulates Git worktrees implicitly. An equivalent direct MCP launch is:
178
180
 
179
181
  ```bash
180
182
  MCP_CONSUMER_WORKSPACE="$PWD" make --silent --no-print-directory -C /path/to/unaltraweb mcp-stdio
@@ -195,6 +197,16 @@ env -u MCP_CONSUMER_WORKSPACE MCP_PROJECT_ID=0123456789abcdef make mcp-down
195
197
  When the workspace is live, a supplied `MCP_PROJECT_ID` must match its canonical path. A retained ID is accepted only without a live workspace, making stale-resource cleanup explicit. Cleanup selects only resources carrying both `io.context.mcp-factory=unaltraweb` and that project's stable `io.context.mcp-project` label. Maintainers can deliberately clean every labelled unaltraweb MCP resource with `make mcp-down-all`; neither target deletes images or touches unlabelled containers and networks.
196
198
  Replace the example retained ID with the 16-hex value from that project's `io.context.mcp-project` Docker label.
197
199
 
200
+ ## Workspace Path Policies
201
+
202
+ For consumer filesystem ownership, the discovery manifest declares literal
203
+ `workspace_rule.path_policies`. The central manager's read-only `workspace-check`
204
+ checks the selected consumer plus the installable `diavisuals` and `vegavisuals`
205
+ dependency closure. It does not run provider commands or clean files. In
206
+ particular, ignored PDF recovery state and `tmp` require explicit review, not
207
+ blanket deletion. See [Workspace Path Policies And Consumer Updates](docs/_documentation/en/43-workspace-path-policies.md)
208
+ for the audited paths, package/scaffold compatibility and migration procedure.
209
+
198
210
  ## Bibliometrics
199
211
 
200
212
  Normal Jekyll builds must stay static. External metrics are fetched only through explicit update commands and written back to local data files before build time.
@@ -8,6 +8,7 @@ module Unaltraweb
8
8
 
9
9
  PANEL_SEQUENCE = ("a".."z").to_a.freeze
10
10
  FENCED_CODE_BLOCK = /^[ \t]*(`{3,}|~{3,})[^\n]*\n.*?^[ \t]*\1[ \t]*$/m.freeze
11
+ CAPTION_SOURCE_ATTRIBUTE = /(?:\A|\s)data-caption-source\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s}]+)/.freeze
11
12
 
12
13
  def enabled?(site)
13
14
  config = config_for(site)
@@ -104,6 +105,7 @@ module Unaltraweb
104
105
  out << figure_html(
105
106
  img: %(<img src="#{h(url.strip)}" alt="#{h(strip_liquid(alt))}"#{attrs.empty? ? "" : " #{attrs}"}>),
106
107
  caption: render_inline_markdown(caption),
108
+ source: render_inline_markdown(kramdown_attr_value(attrs_raw, "data-caption-source")),
107
109
  label: label,
108
110
  lang: lang,
109
111
  count: count,
@@ -196,7 +198,7 @@ module Unaltraweb
196
198
 
197
199
  line_end = source.index("\n", start_index) || source.length
198
200
  opening = source[start_index...line_end]
199
- match = opening.match(/\A:::\s*subfigures(?:\s+([^\s"]+))?(?:\s+"([^"]+)")?\s*\z/)
201
+ match = opening.match(/\A:::\s*subfigures(?:\s+([^\s"{]+))?(?:\s+"([^"]+)")?(?:\s+(\{:[^\n]*\}))?\s*\z/)
200
202
  return nil unless match
201
203
 
202
204
  body_start = line_end == source.length ? line_end : line_end + 1
@@ -206,6 +208,7 @@ module Unaltraweb
206
208
  {
207
209
  layout: match[1].to_s,
208
210
  caption: match[2].to_s,
211
+ attrs: match[3],
209
212
  body: source[body_start...closing.begin(0)],
210
213
  raw: source[start_index...closing.end(0)],
211
214
  end_idx: closing.end(0)
@@ -217,7 +220,7 @@ module Unaltraweb
217
220
 
218
221
  line_end = source.index("\n", start_index) || source.length
219
222
  opening = source[start_index...line_end]
220
- match = opening.match(/\A:::\s*table(?:\s+"([^"]+)")?\s*\z/)
223
+ match = opening.match(/\A:::\s*table(?:\s+"([^"]+)")?(?:\s+(\{:[^\n]*\}))?\s*\z/)
221
224
  return nil unless match
222
225
 
223
226
  body_start = line_end == source.length ? line_end : line_end + 1
@@ -226,6 +229,7 @@ module Unaltraweb
226
229
 
227
230
  {
228
231
  caption: match[1].to_s,
232
+ attrs: match[2],
229
233
  body: source[body_start...closing.begin(0)],
230
234
  raw: source[start_index...closing.end(0)],
231
235
  end_idx: closing.end(0)
@@ -271,7 +275,7 @@ module Unaltraweb
271
275
  image = images[slot[:index]]
272
276
  attrs = kramdown_attrs_to_html(image[:attrs])
273
277
  panel_attrs = subfigure_panel_attrs(image[:attrs])
274
- caption = render_inline_markdown(image[:caption])
278
+ caption = caption_content(render_inline_markdown(image[:caption]), render_inline_markdown(kramdown_attr_value(image[:attrs], "data-caption-source")))
275
279
  panel = h(slot[:label])
276
280
  caption_html = caption.to_s.strip.empty? ? "" : %Q{<p class="md-subfigure-caption"><span class="md-subfigure-label">#{panel}</span> #{caption}</p>}
277
281
  %Q{<div class="md-subfigure" data-panel="#{panel}"#{panel_attrs}>#{caption_html}<img src="#{h(image[:url].strip)}" alt="#{h(strip_liquid(image[:alt]))}"#{attrs.empty? ? "" : " #{attrs}"}></div>}
@@ -279,7 +283,7 @@ module Unaltraweb
279
283
  %Q{<div class="md-subfigure-row" data-count="#{row.length}">\n#{cells}\n</div>}
280
284
  end.join("\n")
281
285
 
282
- clean_caption = render_inline_markdown(block[:caption]).to_s.strip
286
+ clean_caption = caption_content(render_inline_markdown(block[:caption]), render_inline_markdown(kramdown_attr_value(block[:attrs], "data-caption-source")))
283
287
  figcaption = clean_caption.empty? ? "" : %(<figcaption class="md-figcaption"><span class="figlabel">#{h(label)} #{count}.</span> #{clean_caption}</figcaption>)
284
288
 
285
289
  html = <<~HTML.strip
@@ -301,7 +305,7 @@ module Unaltraweb
301
305
  return [block[:raw], count] unless parsed_table
302
306
 
303
307
  count += 1
304
- caption = render_inline_markdown(block[:caption]).to_s.strip
308
+ caption = caption_content(render_inline_markdown(block[:caption]), render_inline_markdown(kramdown_attr_value(block[:attrs], "data-caption-source")))
305
309
  caption_html = caption.empty? ? "" : %(<figcaption class="md-table-caption"><span class="figlabel">#{h(label)} #{count}.</span> #{caption}</figcaption>)
306
310
  thead = table_row_html(parsed_table[:headers], "th", parsed_table[:alignments])
307
311
  tbody = parsed_table[:rows].map { |row| table_row_html(row, "td", parsed_table[:alignments]) }.join("\n")
@@ -520,7 +524,7 @@ module Unaltraweb
520
524
 
521
525
  attrs_raw = nil
522
526
  if source[index] == "{"
523
- close_index = source.index("}", index)
527
+ close_index = attribute_block_end(source, index)
524
528
  return nil unless close_index
525
529
 
526
530
  attrs_raw = source[index..close_index].sub(/\A\{:\s*/, "{").strip
@@ -530,6 +534,28 @@ module Unaltraweb
530
534
  { alt: alt, url_and_title: url_and_title, attrs: attrs_raw, end_idx: index }
531
535
  end
532
536
 
537
+ def attribute_block_end(source, index)
538
+ quote = nil
539
+ index += 1
540
+ while index < source.length
541
+ char = source[index]
542
+ if char == "\\"
543
+ index += 2
544
+ next
545
+ elsif quote
546
+ quote = nil if char == quote
547
+ elsif char == '"' || char == "'"
548
+ quote = char
549
+ elsif char == "}"
550
+ return index
551
+ elsif char == "\n"
552
+ return nil
553
+ end
554
+ index += 1
555
+ end
556
+ nil
557
+ end
558
+
533
559
  def read_balanced(source, index, open_char, close_char)
534
560
  depth = 1
535
561
  start = index
@@ -594,10 +620,10 @@ module Unaltraweb
594
620
 
595
621
  def kramdown_attr_value(raw, name)
596
622
  source = raw.to_s.strip.sub(/\A\{:\s*/, "").sub(/\A\{\s*/, "").sub(/\s*\}\z/, "")
597
- match = source.match(/(?:\A|\s)#{Regexp.escape(name)}\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s}]+))/)
623
+ match = source.match(/(?:\A|\s)#{Regexp.escape(name)}\s*=\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^\s}]+))/)
598
624
  return nil unless match
599
625
 
600
- match[1] || match[2] || match[3]
626
+ (match[1] || match[2] || match[3]).gsub(/\\(["'\\])/, '\1')
601
627
  end
602
628
 
603
629
  def split_url_and_title(value)
@@ -630,6 +656,7 @@ module Unaltraweb
630
656
  figure_html(
631
657
  img: img_tag,
632
658
  caption: render_inline_markdown(CGI.unescapeHTML(caption)),
659
+ source: render_inline_markdown(CGI.unescapeHTML(extract_attr(img_tag, "data-caption-source").to_s)),
633
660
  label: label,
634
661
  lang: lang,
635
662
  count: count,
@@ -639,10 +666,17 @@ module Unaltraweb
639
666
  end
640
667
  end
641
668
 
642
- def figure_html(img:, caption:, label:, lang:, count:, classes: ["md-figure"], attrs: "")
669
+ def caption_content(caption, source = "")
670
+ parts = []
671
+ parts << %(<span class="md-caption-text">#{caption.to_s.strip}</span>) unless caption.to_s.strip.empty?
672
+ parts << %(<span class="md-caption-source">#{source.to_s.strip}</span>) unless source.to_s.strip.empty?
673
+ parts.join(" ")
674
+ end
675
+
676
+ def figure_html(img:, caption:, label:, lang:, count:, classes: ["md-figure"], attrs: "", source: "")
643
677
  classes = Array(classes)
644
678
  classes = ["md-figure"] if classes.empty?
645
- clean_caption = caption.to_s.strip
679
+ clean_caption = caption_content(caption, source)
646
680
  figcaption = if clean_caption.empty?
647
681
  ""
648
682
  else
@@ -736,6 +770,9 @@ module Unaltraweb
736
770
  source = raw.to_s.strip.sub(/\A\{:\s*/, "").sub(/\A\{\s*/, "").sub(/\s*\}\z/, "")
737
771
  return "" if source.empty?
738
772
 
773
+ # Credits are visible caption content, not attributes that can acquire
774
+ # nested HTML quotes when Liquid citations are expanded later.
775
+ source = source.gsub(CAPTION_SOURCE_ATTRIBUTE, " ")
739
776
  source = source.tr("“”’‘", %q{""''})
740
777
  classes = []
741
778
  id = nil
@@ -1461,7 +1461,9 @@ html[data-theme="cafe"] .site-profile-unaltredocs .documentation-sidebar-logo-ca
1461
1461
  flex: 1 0 auto;
1462
1462
  }
1463
1463
 
1464
- .documentation-content table {
1464
+ // Rouge uses a table for its gutter and code. Its preformatted whitespace and
1465
+ // sizing belong to .uw-code-block, not to prose/data-table wrapping rules.
1466
+ .documentation-content table:not(.rouge-table) {
1465
1467
  display: block;
1466
1468
  width: max-content;
1467
1469
  min-width: 100%;
@@ -1470,8 +1472,8 @@ html[data-theme="cafe"] .site-profile-unaltredocs .documentation-sidebar-logo-ca
1470
1472
  overflow-x: auto;
1471
1473
  }
1472
1474
 
1473
- .documentation-content table th,
1474
- .documentation-content table td {
1475
+ .documentation-content table:not(.rouge-table) th,
1476
+ .documentation-content table:not(.rouge-table) td {
1475
1477
  overflow-wrap: normal;
1476
1478
  word-break: normal;
1477
1479
  }
@@ -1485,8 +1487,8 @@ html[data-theme="cafe"] .site-profile-unaltredocs .documentation-sidebar-logo-ca
1485
1487
  overflow: visible;
1486
1488
  }
1487
1489
 
1488
- .documentation-content table a,
1489
- .documentation-content table code {
1490
+ .documentation-content table:not(.rouge-table) a,
1491
+ .documentation-content table:not(.rouge-table) code {
1490
1492
  white-space: normal;
1491
1493
  overflow-wrap: break-word;
1492
1494
  word-break: normal;
data/_sass/_manual.scss CHANGED
@@ -1201,6 +1201,13 @@ html[data-theme="dark"] .md-table {
1201
1201
  text-align: center;
1202
1202
  }
1203
1203
 
1204
+ .md-caption-source {
1205
+ font-size: 0.92em;
1206
+ font-style: italic;
1207
+ font-weight: normal;
1208
+ color: var(--global-text-color);
1209
+ }
1210
+
1204
1211
  .figlabel {
1205
1212
  display: inline-flex;
1206
1213
  min-height: 1.45rem;
@@ -27,13 +27,13 @@ nav_title: Local Tools
27
27
 
28
28
  ## Local Runtime
29
29
 
30
- The currently published integration template uses the shared `v0.3.0` runtime image by default:
30
+ The current public core runtime image is:
31
31
 
32
32
  ```bash
33
- ghcr.io/dosquartsdedocs/unaltraweb:0.3.0
33
+ ghcr.io/dosquartsdedocs/unaltraweb:0.4.0
34
34
  ```
35
35
 
36
- That image provides Ruby, Bundler, Jekyll system dependencies, ImageMagick, Node for ExecJS and Python tooling needed by local commands. The `v0.4.0` candidate source selects matching `0.4.0` runtime and MCP images for package-generated sites, but consumers must not adopt those references until coordinated publication completes. The GHCR package must be public before unauthenticated users can pull it.
36
+ That image provides Ruby, Bundler, Jekyll system dependencies, ImageMagick, Node for ExecJS and Python tooling needed by local commands. Package-generated `v0.4.0` sites select the matching public MCP image for normal local commands. The external integration fixture can advance from its older release pin independently.
37
37
 
38
38
  ## Generated Site Commands
39
39
 
@@ -143,4 +143,4 @@ When the core docs and all template profiles are running together, use this conv
143
143
  - Keep deploy workflows as thin `workflow_dispatch` wrappers pinned to a reviewed full commit SHA of `dosquartsdedocs/unaltraweb/.github/workflows/site-deploy.yml`.
144
144
  - The optional integration template may retain its local `gh-pages` publishing target for testing that separate workflow.
145
145
  - After the first Docker publish, make `ghcr.io/dosquartsdedocs/unaltraweb` public.
146
- - Confirm `docker pull ghcr.io/dosquartsdedocs/unaltraweb:0.3.0` works without `docker login`.
146
+ - Confirm `docker pull ghcr.io/dosquartsdedocs/unaltraweb:0.4.0` works without `docker login`.
@@ -88,6 +88,67 @@ The generated repository contains one profile. Create a separate temporary site
88
88
  - Reusable workflow updates change optional GitHub build and deploy behavior.
89
89
  - Package scaffold changes affect newly generated sites. Existing generated repositories can review `scaffold_sync`, which manages exactly `.gitignore`, `.unaltraweb/docker-mount.sh`, `.github/CONTRIBUTING.md`, `.github/dependabot.yml`, `Makefile`, `Gemfile`, `Gemfile.lock`, `.github/pull_request_template.md`, and `.github/workflows/deploy.yml` from `.unaltraweb/scaffold.json`. It can adopt bytes that already equal the current package payload without rewriting the file. A conflict aborts the entire apply, and retired entries are removed from the baseline without deleting project files. Synchronization never changes site-owned README/agent guidance, config, or content.
90
90
 
91
+ ### Guided Consumer Updates
92
+
93
+ At the start of a site session, the agent inspects `site_context.update_status`.
94
+ The MCP compares the consumer's literal MCP/gem pins and managed baseline with
95
+ the package active in this session. It reports the target version and integration
96
+ tuple, planned paths, preserved customizations, conflicts and a `plan_sha256`.
97
+ This is an offline check, not a query for the newest version on GitHub or PyPI.
98
+ A change to documentation or the factory discovery manifest alone does not imply
99
+ that the consumer needs updating.
100
+
101
+ When changes are available, the agent explains them and asks whether to proceed.
102
+ After acceptance and the normal issue, branch and path reservation, it calls:
103
+
104
+ ```text
105
+ scaffold_sync(dry_run=false, confirm_sync=true, expected_plan_sha256=<reviewed plan_sha256>)
106
+ ```
107
+
108
+ The equivalent native CLI flow is:
109
+
110
+ ```bash
111
+ unaltraweb-mcp --project . mcp site-context
112
+ unaltraweb-mcp --project . mcp scaffold-sync
113
+ unaltraweb-mcp --project . mcp scaffold-sync --apply --confirm-sync --expected-plan-sha256 <reviewed-sha256>
114
+ ```
115
+
116
+ The confirmation is bound to that consumer, package proposal, configuration and
117
+ managed-file snapshot. If the plan changes, inspect it again before confirming.
118
+ After applying, inspect context again, run `site_check`, then `build_site` or the
119
+ appropriate local build. Manual PDF review still uses managed preparation and
120
+ cleanup. The update does not commit, publish or regenerate editorial artefacts.
121
+ Declining the offer leaves the repository untouched and lets content work continue.
122
+
123
+ An extra local ignore rule need not block a runtime-pin update: if the incoming
124
+ `.gitignore` still equals its recorded baseline, synchronization preserves the
125
+ customized file and its original package baseline. The same rule applies to other
126
+ managed files. If both the local file and the incoming package changed, there is
127
+ a genuine conflict; no file is overwritten and the entire apply is blocked.
128
+ New-path collisions and missing/unsafe baselines also need review. Known newer
129
+ consumer versions are never silently downgraded by an older MCP. Custom image
130
+ digests and non-literal version expressions are not guessed or resolved online.
131
+
132
+ The planner and transaction are deterministic Python code. For the same consumer
133
+ path, package/BOM, configuration, baseline and managed bytes, they produce the same
134
+ plan digest and intended control-file bytes. No model generates the replacement
135
+ files. Diagnostic timestamps such as `site_context.generated_at`, temporary names
136
+ and filesystem timestamps are not reproducibility guarantees of the plan.
137
+
138
+ The agent initiates the check, presents its result, asks for acceptance and
139
+ coordinates the normal Git/review workflow. An automatic offer therefore depends
140
+ on the agent following the session instructions; the MCP does not force a popup
141
+ or run a background update. The CLI commands above use the same engine without
142
+ an agent. Reconciliation of a genuine conflict is a separate reviewed change,
143
+ after which the engine computes a new plan.
144
+
145
+ Existing consumers receive this flow
146
+ when they reconnect to an MCP release that includes it; their site-owned AGENTS
147
+ and README do not need to be replaced. Publishing that new MCP/package release
148
+ and advancing its reviewed runtime pin remain maintainer actions. An already
149
+ running v0.4.0 image will not gain the feature simply because the factory checkout
150
+ was updated.
151
+
91
152
  ## Safe MCP Editing
92
153
 
93
154
  `site_source_read`, `site_source_write`, and `site_source_delete` are deliberately restricted to `_config.yml`, Markdown/HTML content collections, YAML/JSON/CSV below `_data/`, and Markdown below `context/`. They do not expose generic filesystem access and cannot mutate workflows, runtime files, core overrides, bibliography, assets, or generated output. Writes default to dry-run and use SHA-256 optimistic concurrency; destructive deletes additionally require explicit confirmation and can never remove `_config.yml`.
@@ -166,7 +166,7 @@ Deleting or disabling a source leaves its lock record and generated artifacts as
166
166
 
167
167
  Static Vega-Lite and Vega figures use `*.vl.json` and `*.vg.json` sources declared in `.vegavisuals.yml`. A chapter references the specification as a captioned Markdown image; Jekyll and the PDF builder resolve it to the same single manifest output without changing the caption or figure attributes. Prefer SVG when the figure must work identically on the web and in print.
168
168
 
169
- Use the required companion `vegavisuals` MCP and its `initialize_project`, `visualization_status`, `render_visualizations`, and `visualization_check` tools for the source-to-output lifecycle. ContExt registers the companion separately rather than copying it into the site. Run `visualization_check` before PDF status, build, or publication when a manifest exists; the unaltraweb PDF tool does not proxy the separate MCP. Commit the manifest, lock, provider receipt, source data, and generated outputs together.
169
+ Use the required companion `vegavisuals` MCP and its `initialize_project`, `visualization_status`, `render_visualizations`, and `visualization_check` tools for the source-to-output lifecycle. gContExt registers the companion separately rather than copying it into the site. Run `visualization_check` before PDF status, build, or publication when a manifest exists; the unaltraweb PDF tool does not proxy the separate MCP. Commit the manifest, lock, provider receipt, source data, and generated outputs together.
170
170
 
171
171
  ## PDF Edition
172
172
 
@@ -102,6 +102,16 @@ For pages with figure captions enabled, use the Markdown image title as the capt
102
102
  ![Digitizing workflow]({{ site.baseurl }}/assets/img/workflow.svg "Main editing steps")
103
103
  ```
104
104
 
105
+ Separate attribution from the description with `data-caption-source`:
106
+
107
+ ```markdown
108
+ ![Accessible description](assets/img/map.svg "Distribution by municipality"){: data-caption-source="Source: verified dataset. Credits: map creator."}
109
+ ```
110
+
111
+ The label/number, description and credits have separate web elements. The source
112
+ continues inline with differentiated styling; PDF lists include the description
113
+ without its credits. See the [rendered caption and credit examples]({{ '/caption-credits/' | relative_url }}).
114
+
105
115
  To narrow the space assigned to one figure without changing its height, set a
106
116
  CSS width with `data-figure-width`. The figure remains centred and cannot exceed
107
117
  the available width. The PDF builder maps `rem` values against its 60-rem
@@ -137,6 +147,10 @@ Manual chapters can number teaching tables with localized labels:
137
147
  :::
138
148
  ```
139
149
 
150
+ Tables accept the same field on the opening line:
151
+ `::: table "Weekly work rhythm" {: data-caption-source="Source: course plan."}`.
152
+ Existing captions remain valid and are not automatically split.
153
+
140
154
  Cells may contain inline code spans such as `` `=SUM(A2:A10)` ``. The manual renderer preserves these as inline `<code>` elements on the web; if a consumer project still shows literal backticks inside numbered tables, update its `unaltraweb` lock file or use explicit HTML `<code>` as a temporary compatibility fallback.
141
155
 
142
156
  ## Diagram Sources
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: Figure And Table Captions With Credits
3
+ description: Separate automatic numbering, descriptive captions and source credits, with concise PDF indexes.
4
+ lang: en
5
+ ref: caption_credits
6
+ profiles: [unaltredocs]
7
+ documentation_profiles: [github-publishers, local-authors, site-designers, contributors]
8
+ section: Design And Customize
9
+ weight: 315
10
+ permalink: "/caption-credits/"
11
+ nav_title: Captions And Credits
12
+ ---
13
+
14
+ A caption has three distinct parts: its automatic label and number, the
15
+ description of the figure or table, and optional source or creator credits.
16
+ Credits remain part of the visible caption but use a smaller italic style. In a
17
+ manual PDF, only the number and description enter the list of figures or tables.
18
+
19
+ ## Figure
20
+
21
+ Keep the descriptive caption in the Markdown image title and add
22
+ `data-caption-source` to its attribute block. Alternative text describes the
23
+ image independently of its caption and credits.
24
+
25
+ ```markdown
26
+ ![Three boxes connected from left to right](assets/img/caption-credits-demo.svg "Three linked stages"){: data-figure-width-web="34rem" data-figure-width-pdf="78%" data-caption-source="Source: original schematic for this example. Credits: unaltraweb."}
27
+ ```
28
+
29
+ ![Three boxes A, B and C connected from left to right]({{ site.baseurl }}/assets/img/caption-credits-demo.svg "Three linked stages"){: data-figure-width-web="34rem" data-figure-width-pdf="78%" data-caption-source="Source: original schematic for this example. Credits: unaltraweb."}
30
+
31
+ The label receives the numbering style, the description is normal caption text,
32
+ and the source continues inline in its own styled span. Normal wrapping can move
33
+ the source onto the next line; it is not a separate numbered item.
34
+
35
+ ## Table
36
+
37
+ Use the same attribute after the opening caption. This example contains
38
+ synthetic values solely to illustrate the presentation.
39
+
40
+ ```markdown
41
+ ::: table "Example values" {: data-caption-source="Source: synthetic demonstration data. Credits: unaltraweb."}
42
+ | Element | Value |
43
+ | --- | ---: |
44
+ | A | 10 |
45
+ | B | 25 |
46
+ | C | 40 |
47
+ :::
48
+ ```
49
+
50
+ ::: table "Example values" {: data-caption-source="Source: synthetic demonstration data. Credits: unaltraweb."}
51
+ | Element | Value |
52
+ | --- | ---: |
53
+ | A | 10 |
54
+ | B | 25 |
55
+ | C | 40 |
56
+ :::
57
+
58
+ ## Links And Attribution
59
+
60
+ The credit field accepts inline Markdown, including links and emphasis, and the
61
+ existing bibliography citation syntax. Supply the label appropriate to the
62
+ content language, such as “Source”, “Font”, “Fuente” or “Credits”. Attribution
63
+ must describe the actual origin and licence of the material.
64
+
65
+ ::: table "Caption components" {: data-caption-source="Credits: [unaltraweb](https://github.com/dosquartsdedocs/unaltraweb), documentation example."}
66
+ | Component | Purpose |
67
+ | --- | --- |
68
+ | Label and number | Automatically identify the figure or table |
69
+ | Description | Explain what the reader should observe |
70
+ | Source or credits | Identify origin, authorship or licence |
71
+ :::
72
+
73
+ Use single quotes around the attribute if the credit text contains literal double
74
+ quotes. Liquid citations such as `{% raw %}{% cite verifiedKey %}{% endraw %}`
75
+ follow the usual verified-bibliography workflow. Keep credits in the attribute,
76
+ rather than concatenating them into the descriptive title.
77
+
78
+ ## Multi-Panel Figures
79
+
80
+ The attribute can credit the whole group or an individual panel:
81
+
82
+ ```markdown
83
+ ::: subfigures a+b "Two views of the same schematic" {: data-caption-source="Source: original demonstration schematic."}
84
+ ![First view](assets/img/caption-credits-demo.svg "First view"){: data-caption-source="Credits: unaltraweb."}
85
+ ![Second view](assets/img/caption-credits-demo.svg "Second view")
86
+ :::
87
+ ```
88
+
89
+ ::: subfigures a+b "Two views of the same schematic" {: data-caption-source="Source: original demonstration schematic."}
90
+ ![First view]({{ site.baseurl }}/assets/img/caption-credits-demo.svg "First view"){: data-caption-source="Credits: unaltraweb."}
91
+ ![Second view]({{ site.baseurl }}/assets/img/caption-credits-demo.svg "Second view")
92
+ :::
93
+
94
+ ## Existing Captions
95
+
96
+ Existing captions remain valid. The renderer does not guess where a source starts
97
+ in an old caption, since words such as “source” can be part of the description.
98
+ Move attribution into `data-caption-source` explicitly to obtain separate styling
99
+ and shorter PDF index entries.
100
+
101
+ ::: table "A caption without separate credits"
102
+ | Element | Category |
103
+ | --- | --- |
104
+ | A | Initial |
105
+ | B | Intermediate |
106
+ :::
107
+
108
+ ## PDF Index Entries
109
+
110
+ For the figure above, the list of figures contains **Three linked stages**. Its
111
+ source and creator attribution remain beside the full caption in the chapter.
112
+ For the first table, the list of tables contains **Example values**, without the
113
+ synthetic-data attribution. Figure/table numbering continues to follow the
114
+ manual's normal chapter numbering.
115
+
116
+ The web exposes `.figlabel`, `.md-caption-text` and `.md-caption-source` for these
117
+ roles. The PDF renderer preserves inline formatting and links in the full caption
118
+ and supplies a description-only short caption to LaTeX. These semantics apply to
119
+ normal images, diagrams and generated figures referenced through the same image
120
+ syntax; source generation and freshness remain owned by their respective tools.