Skip to main content

transclude.js

Transclusion is a feature (inspired by Project Xanadu, English Wikipedia, Everything2, and Urticator.net) that lets you embed content from other pages inline rather than just.

Pathjs/transclude.js
LanguageJavaScript
Lines2587
Sourcejs/transclude.js
at 406d3e423

Read this when Use this page when tracing browser-side behavior, runtime state, event handling, popups, or UI code around transclude.

Overview​

Transclusion is a feature (inspired by Project Xanadu, English Wikipedia, Everything2, and Urticator.net) that lets you embed content from other pages inline rather than just linking to them. When you put an include-link like <a class="include" href="/sidenote#comparisons"></a> in a page, this module replaces that link with the actual content of the #comparisons section from /sidenote.

The module handles three major concerns:

  1. Content fetching - Lazy or immediate loading of source content via the Content/Annotations data providers
  2. Content slicing - Extracting specific portions using anchors, range syntax (#start:end), CSS selectors, or block context
  3. Content injection - Replacing the include-link with processed content, handling footnotes, TOC updates, and structure rectification

A key design decision is the lazy loading default: include-links only trigger when scrolled into view (with a margin). This prevents expensive transclusions from blocking initial page load. The include-strict class overrides this for content that must load immediately.

The template system adds another layer: instead of transcluding raw HTML, you can use templates that get filled with data extracted from the source content. This powers how annotations are displayed in popups.


Public API​

The main entry point. Processes an include-link, fetching and injecting its content.

Transclude.transclude(includeLink, now = false);
  • now=false (default): Uses lazy loading unless include-strict is set
  • now=true: Loads immediately

Called by: handleTranscludes content handler, self (for delayed loads) Calls: dataProvider.load, sliceContentFromDocument, includeContent

Tests whether a link is an include-link by checking for include* classes.

Transclude.isIncludeLink(link)
// Checks for: include, include-annotation, include-content, include-strict, etc.

Called by: Everywhere that processes links

Transclude.allIncludeLinksInContainer(container) → Array​

Returns all include-links within a container element.

Called by: handleTranscludes, triggerTranscludesInContainer

Determines if the link should transclude an annotation (vs. raw content).

Decision logic:

  • If transcludeAnnotationsByDefault is true AND link has full annotation: transclude annotation unless include-content class is set
  • Otherwise: transclude annotation only if include-annotation class is set

Called by: dataProviderForLink, contentTypeIdentifierForIncludeLink

Extracts the appropriate portion of content based on the include-link's URL and options.

Handles:

  • Full page (extracts #markdownBody or body)
  • Single anchor (#section-id)
  • Range syntax (#start:end)
  • Block context (include-block-context)
  • CSS selector filtering (data-include-selector, data-include-selector-not)

Called by: transclude (after data loads) Calls: blockContext, targetElementInDocument

Transclude.triggerTranscludesInContainer(container, eventInfo, options) → void​

Triggers all include-links in a container. Used for batch processing, e.g., before printing.

Called by: beforeprint listener

fillTemplate(template, data, context, options) → DocumentFragment​

Fills a template string with data. Supports conditionals, variable substitution, comments, and escapes.

let content = fillTemplate(
"<div><{title}></div><[IF abstract]><p><{abstract}></p><[IFEND]>",
{ title: "Hello", abstract: "World" }
);

Called by: Transclude.transclude (via processData)

templateDataFromHTML(html) → object​

Extracts template data from HTML using data-template-field and data-template-fields attributes.

<span data-template-field="foo">Bar</span>  <!-- { foo: "Bar" } -->
<span data-template-fields="foo:$title" title="Baz"></span> <!-- { foo: "Baz" } -->

Called by: fillTemplate


Internal Architecture​

ClassEffect
includeBasic include-link marker
include-annotationForce transclude annotation
include-contentForce transclude content (not annotation)
include-strictLoad immediately, not lazily
include-lazyExtra-lazy: only when visible in viewport
include-even-when-collapsedLoad even inside collapsed blocks
include-unwrapDiscard container element, keep contents
include-block-contextInclude surrounding block, not just target
include-rectify-notSkip HTML structure rectification
include-localize-notDon't integrate footnotes/TOC

Content Slicing Flow​

URL: /page#start:end
│
▼
sliceContentFromDocument()
│
├─► No hash → Extract #markdownBody or body
│
├─► Two-part hash (#start:end) → Range extraction
│ │
│ ├─► #:end → Start of page to #end
│ ├─► #start: → #start to end of page
│ ├─► #start:end → #start to #end (exclusive)
│ └─► #: → Entire page content
│
└─► Single hash (#target) → Element extraction
│
├─► include-block-context? → blockContext()
│ │
│ └─► Find containing block
│ (figure, li, p, section, etc.)
│
└─► include-unwrap? → Return children only

▼
Apply data-include-selector-not (remove matching elements)
▼
Apply data-include-selector (keep only matching elements)
▼
Return DocumentFragment

Transclusion Lifecycle​

1. handleTranscludes (content load handler)
│
▼
2. Transclude.transclude(link)
│
├─► Check: already loading/complete? → exit
│
├─► Check: in collapsed block? → wait for expand
│
├─► Set loading state (spinner)
│
├─► Lazy? (not include-strict)
│ │
│ └─► lazyLoadObserver → wait for scroll
│
▼
3. dataProvider.load(link) [Content or Annotations]
│
▼
4. dataProvider.waitForDataLoad()
│
├─► Template specified? → Transclude.doWhenTemplateLoaded()
│
▼
5. processData() → fillTemplate() if needed
│
▼
6. sliceContentFromDocument()
│
▼
7. includeContent() → inject into DOM
│
├─► Wrap content
├─► Delete metadata sections (if transcluding into full page)
├─► Rectify heading levels
├─► Fire GW.contentDidInject
├─► Rectify HTML structure (shift wrapper up tree)
├─► Update footnotes
├─► Update TOC
└─► Unwrap and fire Rewrite.contentDidChange

Template System​

Templates use a custom syntax:

<{fieldName}>              <!-- Variable substitution -->
<[IF condition]>...<[IFEND]> <!-- Conditional -->
<[IF cond]>...<[ELSE]>...<[IFEND]>
<[IF1 nested]>...<[IF1END]> <!-- Nested conditional (numbered) -->
<( comment )> <!-- Comment (removed) -->
>\ <!-- Line continuation -->
<
\x <!-- Character escape -->

Conditional expressions support:

  • & (AND), | (OR), ! (NOT)
  • [brackets] for grouping
  • 'quoted' or "quoted" string literals
  • Whitespace-separated comparison: fieldA fieldB (equality test)

Key Patterns

Lazy Loading with Intersection Observer​

if (includeLink.classList.contains("include-strict") == false) {
includeLink.delayed = true;
requestIdleCallback(() => {
lazyLoadObserver(() => {
Transclude.transclude(includeLink, true);
}, includeLink, {
root: scrollContainerOf(includeLink),
rootMargin: (includeLink.classList.contains("include-lazy")
? "0px"
: Transclude.defaultLoadViewportMargin) // "110%"
});
});
}

The 110% margin means content starts loading before it's actually visible.

Block Context Detection​

Finding the appropriate containing block for a target element:

// Specific blocks (always used if matched)
specificBlockElementSelectors: [
".footnote, .sidenote",
".aux-links-append",
".epigraph"
],

// General blocks (checked with length limit)
generalBlockElementSelectors: [
"figure",
"li",
"p",
"blockquote",
"section, .markdownBody > *"
],

blockContextMaximumLength: 250 // Characters - prevents huge blocks

Alias Classes​

Compound behaviors are expressed as alias classes that expand:

Transclude.addIncludeLinkAliasClass("include-annotation-partial", (includeLink) => {
includeLink.classList.add("include-annotation");
includeLink.dataset.includeSelectorNot = ".annotation-abstract, .file-includes, figure, ...";
includeLink.dataset.templateFields = "annotationClassSuffix:$";
includeLink.dataset.annotationClassSuffix = "-partial";
});

HTML Structure Rectification​

After injection, block content gets shifted up the tree until it's in an allowed parent:

let allowedParentSelector = ["section", "blockquote", "div", ".include-wrapper"];
if (wrapper.querySelector("section") == null)
allowedParentSelector.push("li", "figcaption");

while (wrapper.parentElement.matches(allowedParentSelector) == false) {
// Shift wrapper up, splitting parent if needed
}

This prevents invalid HTML like <p><div>...</div></p>.


Configuration

Transclude Object Properties​

Transclude.transcludeAnnotationsByDefault: true  // Prefer annotations over content
Transclude.defaultLoadViewportMargin: "110%" // Intersection observer margin
Transclude.blockContextMaximumLength: 250 // Max chars for block context
AttributePurpose
data-include-templateTemplate name or $key into data
data-include-selectorCSS selector for elements to include
data-include-selector-notCSS selector for elements to exclude
data-include-selector-optionsfirst to use querySelector vs querySelectorAll
data-block-context-optionsexpanded to skip <p> as block boundary
data-template-fieldSingle field name, innerHTML is value
data-template-fieldsComma-separated fieldName:$attr or fieldName:.prop

Integration Points​

Events Fired​

  • GW.contentDidLoad - After content is fetched and processed
    • source: "transclude", contentType, includeLink, loadLocation
  • GW.contentDidInject - After content is injected into DOM
    • source: "transclude", container (wrapper), flags, includeLink
  • Rewrite.contentDidChange - After delayed transclusion completes
    • source: "transclude", nodes, where
  • Transclude.templateDidLoad / Transclude.templateLoadDidFail - Listened for by Transclude.doWhenTemplateLoaded(); not actually fired anywhere in the current codebase, since templates are pre-baked into transclude-templates-GENERATED.js

Events Listened​

  • Collapse.collapseStateDidChange - For delayed transcludes in collapsed blocks
  • GW.contentDidLoad / GW.contentDidInject - handleTranscludes registered as content handler

Data Providers​

  • Content - For include-content or default content transcludes
  • Annotations - For include-annotation transcludes

The provider is selected via Transclude.dataProviderForLink(includeLink).

Shared State​

  • includeLink.eventInfo - Contains document, container, context, loadLocation
  • includeLink.delayed - Set true if loading was deferred
  • includeLink.footnote - Reference to associated footnote (during footnote processing)
  • Transclude.templates - Cache of loaded template strings
  • Content (content.js) - Provides content for non-annotation transcludes
  • Annotations (annotations.js) - Provides annotation data
  • Popups (popups.js) - Uses transclusion for popup content
  • Collapse (collapse.js) - Coordinates with collapsed blocks

See Also
  • content.js - Content loading and caching; one of two data providers for transclusion
  • rewrite.js - Content handlers and event system triggered after transclusion
  • initial.js - Notification center used for transclude events
  • annotations.js - Annotation data provider; alternative to Content for transclusion
  • popups.js - Popup system that uses transclusion for popup content
  • LinkMetadata.hs - Backend annotation generation that produces transcludable content