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.
- checksums.yaml +4 -4
- data/Makefile +5 -5
- data/README.md +19 -7
- data/_plugins/figure_captions.rb +47 -10
- data/_sass/_documentation.scss +7 -5
- data/_sass/_manual.scss +7 -0
- data/docs/_documentation/en/02-tools.md +4 -4
- data/docs/_documentation/en/03-usage.md +61 -0
- data/docs/_documentation/en/13-unaltremanual.md +1 -1
- data/docs/_documentation/en/20-syntax.md +14 -0
- data/docs/_documentation/en/25-caption-credits.md +120 -0
- data/docs/_documentation/en/26-image-backgrounds.md +103 -0
- data/docs/_documentation/en/31-template.md +1 -1
- data/docs/_documentation/en/32-development.md +1 -1
- data/docs/_documentation/en/40-distribution.md +25 -5
- data/docs/_documentation/en/42-docker-image.md +7 -7
- data/docs/_documentation/en/43-workspace-path-policies.md +232 -0
- data/docs/_documentation/en/44-editorial-review.md +237 -0
- data/docs/agents/action-prompts/00-start-site-session.txt +10 -5
- data/docs/agents/action-prompts/22-manual-style-audit.txt +3 -1
- data/docs/agents/manual-authoring-components.md +38 -0
- data/docs/agents/mcp-contract.md +102 -10
- data/docs/agents/visual-companions-0.4.0.md +74 -0
- data/docs/assets/img/caption-credits-demo.svg +19 -0
- data/scripts/editorial_check.py +12 -0
- data/scripts/image_background_check.py +12 -0
- data/scripts/manual/build_pdf.py +94 -16
- data/scripts/manual/filters/figure-captions.lua +65 -10
- data/scripts/manual/templates/manual.tex +2 -1
- data/scripts/test_gem_build.py +32 -2
- data/scripts/test_reproducible_jekyll_build.py +1 -1
- data/scripts/test_wheel_install.py +28 -0
- data/scripts/unaltraweb-mcp-bootstrap.sh +1 -1
- data/scripts/validate_distribution.py +18 -3
- data/src/unaltraweb_mcp/component-contract.json +28 -28
- data/src/unaltraweb_mcp/editorial.py +495 -0
- data/src/unaltraweb_mcp/editorial_sources.py +504 -0
- data/src/unaltraweb_mcp/image_backgrounds.py +334 -0
- data/src/unaltraweb_mcp/image_probe.py +149 -0
- data/src/unaltraweb_mcp/processes.py +146 -0
- metadata +15 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9b1d06c849d3893c28fa69a879e93f56acef7a742aa8e2d8ae7df78fa53d4a2
|
|
4
|
+
data.tar.gz: '0995fe545e4466b531edc89ed8c8a37ca707e847cc31a8a3acd3647d6b77ecdd'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
5
|
-
MCP_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.
|
|
6
|
-
MCP_RELEASE_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:
|
|
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.
|
|
44
|
-
MANUAL_PDF_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf:
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
data/_plugins/figure_captions.rb
CHANGED
|
@@ -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])
|
|
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])
|
|
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
|
|
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
|
|
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
|
|
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
|
data/_sass/_documentation.scss
CHANGED
|
@@ -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
|
-
|
|
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
|
|
30
|
+
The current public core runtime image is:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
ghcr.io/dosquartsdedocs/unaltraweb: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.
|
|
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.
|
|
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.
|
|
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
|

|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
Separate attribution from the description with `data-caption-source`:
|
|
106
|
+
|
|
107
|
+
```markdown
|
|
108
|
+
{: 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
|
+
{: data-figure-width-web="34rem" data-figure-width-pdf="78%" data-caption-source="Source: original schematic for this example. Credits: unaltraweb."}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
{: 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
|
+
{: data-caption-source="Credits: unaltraweb."}
|
|
85
|
+

|
|
86
|
+
:::
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
::: subfigures a+b "Two views of the same schematic" {: data-caption-source="Source: original demonstration schematic."}
|
|
90
|
+
{: data-caption-source="Credits: unaltraweb."}
|
|
91
|
+

|
|
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.
|