spaces: a page can show another page, live (transclusion) - #428
Open
nyblnet wants to merge 1 commit into
Open
Conversation
Adds an `embed` block: transclusion of another page, or of one heading's section of it, attributed to its source and clickable through to it. The block stores a REFERENCE and never a copy, so the source page stays the single copy of the words. This closes a hole in the Obsidian import that nothing reported. markdown.ts had parsed `![[Page]]` since the importer was written and carried a comment saying "an embed of a note is just a link to it, because there is no transclusion in the model", so a vault arrived with every embed silently demoted to a plain link. A whole line of `![[Page]]` / `![[Page#Section]]` is an embed now and exports back as itself; an `![[...]]` inside a sentence stays a link, because a block cannot live in the middle of one. The reader is never shown a blank box: a loop renders as a named placeholder saying which page repeats, a chain is followed at most three pages deep, a deleted target says so, and an `anchor` matching no heading says that rather than quietly widening to the whole page. validate() names all four. An embed backlinks like a pagelink, and survives extract and graft - a target that did not travel becomes the same honest [[Name]] text. Nearly all the logic is in a new pure module, spaces/src/embed.ts, so it is asserted in node rather than clicked at; render.ts, editor.ts, model.ts, portable.ts, agent.ts and markdown.ts each gain a small, localized change. Embedded content is never editable, carries no data-block-id, and is parked while the editor wires the page, so a checkbox ticked inside an embed cannot commit to a page the editor is not showing. scripts/test-spaces-model.ts is a shared-surface file; the coverage for this feature lands there, flagged as the cross-zone touch it is.
Build size
Updated: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Transclusion: a page can show another page, live
A new
embedblock renders another page — or one heading's section of it —inline, attributed to its source and clickable through to it. It stores a
reference and never a copy: there is no cached content on the block,
nothing to invalidate, and editing the source changes every embed of it.
{ "id": "b9", "type": "embed", "page": "p-design", // the target page id — the SAME field pagelink uses "anchor": "Rollout", // OPTIONAL, matched by NAME, not by position "html": "<a href=\"#p/p-design\">Design notes</a>" } // the old-build fallbackThe gap this closes
markdown.ts:121carried the comment "an embed of a note is just a link to it,because there is no transclusion in the model". The importer had parsed
Obsidian's
![[Page]]since it was written and silently downgraded every one toa plain
<a>— so importing a vault lost every embed with no warning. A wholeline of
![[Page]]or![[Page#Section]]is now an embed and exports back asitself; an
![[…]]inside a sentence is still a link, because a block cannotlive in the middle of one.
The hard parts
viewEmbedtakes the chain of pages already open above the block.A target on that chain renders a named placeholder saying which page
repeats — never a blank, never recursion. There is also a depth cap of 3
distinct pages, which is the shape a cycle check cannot see (a hundred pages
each embedding the next is not a cycle).
validate()reportsbroken-embed(error),embed-cycle(error) andno-section(warning). Nothing throws.pageandanchorcome out of a mailed file. Targetlookup is a linear scan of
doc.pages, never an object index; the one plainobject keyed on document data (
HEADINGS, keyed onb.type) goes throughObject.hasOwnin a single sharedrankOf. Asserted withtoString,__proto__,constructor,valueOf,hasOwnProperty.with the target and the section intact.
portable.tsfollows the same path aspagelinkthrough a sharedisPageRefpredicate: a grafted embed is remappedonto the renumbered page id (asserted by resolving it in the document it
landed in), and a target that did not travel becomes the same honest
[[Name]]text a page link becomes,anchordeleted with it.reference in the model: the page is not merely mentioned, it is being shown
somewhere else, so it is the reference you most want before rewriting a page.
Two correctness details worth calling out
Embedded content is rendered
editable: false, stripped ofdata-block-id, andthe embed bodies are parked (detached) while the editor wires the page. The
editor's paint sweeps every
[data-block-id],.sp-check,.sp-b-codeandtable cell and hangs handlers that commit through
store.block(id), whichresolves any id in the document — so without this a checkbox ticked inside an
embed would have committed to a page the editor was not showing. Measured in the
browser: ticking an embedded to-do leaves the source page's
doneunchanged,while the host page's own 7 blocks keep all 7 gutters.
Format additivity.
anchorabsent = the whole page, and returning a sectionto "the whole page" deletes the key.
htmlis written alongsidepageso abuild that has never heard of
embedrenders a link to the source page.Measured on the shipped renderer with a block carrying the exact shape under an
unknown type: visible clickable link, and
page/anchor/an unknown key allsurvive the round trip.
Where the code went
Nearly all of it is a new pure module,
spaces/src/embed.ts(no DOM), so itis asserted in node rather than clicked at. The touched files each get a small,
localized change:
render.ts+1 case +1 helper,editor.tsa picker and awire()wrapper,model.tsone field and one condition,portable.tstwoconditions,
agent.tsone validate branch,markdown.tsone parse branch.Verification
The model rig went 947 → 1010 checks (+63 for this feature).
Size: +4,252 B on the built shell, measured directly (
origin/mainbuilds at277,769 B, this branch at 282,021 B). The size rig's "+23,567 B since reference"
is against a watermark that predates several merged branches; main alone is
+19,315 B over it.
i18n: 11 new strings in all 8 catalogs, then
packed.tsgrepped for eachone — all 11 present, each with 8 non-zero columns. (100% coverage is reported
from swept keys and is not evidence; the block
label/hintare swept fromblocks.tsby the packer's second pass, which is why they land.)Sabotage
Every assertion added here was watched to fail. 17 mutations, each applied
alone, rig run, restored, rig green again:
>→>=)Object.hasOwn→inon the heading table<instead of<=rankisPageRefforgetsembedlinkEmbedskeeps a staleanchorplanImportskipslinkEmbeds!prefixbroken-embed/embed-cycle/no-sectionTwo of these failed to fail on the first run, and both were fixed rather than
explained away:
Object.hasOwn→ininsidesectionOfchanged nothing observable: thenative function it hands back fails
> 0anyway, so the guard was real andits proof was not. Both readers now share one
rankOf, and the sabotage isvisible through
headingsOf.htmllink,which the inline sweep already finds — so it never touched
isPageRefat all.It now uses an agent-written embed with no
html, which is exactly theblock that would silently have had no backlink.
Browser verification
Built, copied to a uniquely-named file, served over
http://127.0.0.1:8931(marker:
.sp-embedcomputesborder-inline-start-width: 3px, and nested pagecontent renders at all — impossible on the old build). Measured with
getBoundingClientRect/elementFromPoint, not by reading back a property:106px with just that section; header reads
Beta › Risksand is an<a>to#p/Bno blank boxes, no hang
[data-block-id], 0contenteditable, 0 gutters inside embed bodies; thehost page keeps all 7 of its own
page, and writes
{type, page, anchor, html}Cross-zone touch, flagged not hidden
scripts/test-spaces-model.tsis an ops-owned shared-surface file. Thecoverage for this feature lands there because that is where the spaces model rig
lives; no other ops file is touched.
docs/spaces-agents.mdgains an Embedssection (it is a contract with people outside this repo) and the changelog entry
is in
spaces/CHANGELOG.md, not the root one.Not verified
page/anchorare ordinary flat LWW registers, sothey behave like any other block field, but no two-replica convergence run was
done for this block type specifically.
renderPage, sothey draw embeds; neither was rendered to paper or through
qlmanagehere.accent-folded —
CaféandCafeare different sections. That is deliberate(a fold is a locale decision), but it is untested behaviour worth naming.