- Three-phase overview
- Phase 1: element traversal decision tree
- Phase 2 pull-up and Phase 3 push-down
- Core data model
A concrete map with a root scope, a product child scope, and a widget grandchild scope.
The root and product both define logo; the root definition wins after push-down.
- Frame 1 — Phase 1: local keys only
- Frame 2 — Phase 2: pull-up adds qualified aliases
- Frame 3 — Phase 3: push-down propagates and resolves conflict
This document describes how key space construction works in the current Python implementation, centered on keyspacemgr.py and supported by keyspace.py and keyspacevisitors.py.
The algorithm constructs a tree of KeySpace objects and populates them with KeyDefinition objects from a resolved DITA map (resolvemap.resolveMap() output). It also prepares peer-map key spaces for deferred (lazy) construction.
The implementation performs construction in three phases:
- Populate key spaces by walking the resolved map tree.
- Pull up descendant keys as scope-qualified names.
- Push down ancestor keys into descendant spaces (with override precedence).
KeyspaceManager owns all known key spaces and keeps two global indexes:
keyspacesByMapUri: map URI -> key spacekeyspacesByDefiner: defining element -> key space
When created with a resolved map, the manager:
- Creates a root
KeySpacewith initial scope name#annonymous. - Registers it in manager indexes.
- Calls
constructKeySpace(rootKeySpace, resolvedMap).
A KeySpace contains:
keyScopeNames: set of scope names for this space.keydefsByKeyName: key name -> list ofKeyDefinitionin priority order.keyspacesByScopeName: child scope name -> list of child key spaces (priority order).peerKeyscopes: peer scope name -> peer mapref element(s).
Priority is represented by list ordering. Earlier entries are higher priority.
Entry point: constructKeySpace(rootKeySpace, resolvedMap).
If resolvedMap is an ElementTree, the code uses .getroot(). If it is already an element, it is used directly.
The root element is bound as the space definer for rootKeySpace, then recursive traversal starts at _handleElement(rootKeySpace, rootElem).
Traversal is depth-first and pre-order. Behavior by element type:
map/topicmeta: ignored.map/reltable: ignored.map/map:- If
@keyscopeis present, tokens are added to current key space. - Continue with children in same key space.
- If
map/topicref:- If
@scope="peer": treat as peer map key scope (_addPeerMapKeySpace), do not traverse into peer map content here. - Else if
@keyscopeexists: create a child key space (_addChildKeySpace). - Else if
@keysexists: add key definitions to current key space (_addKeyDefinition). - Else: recurse into children in same key space.
- If
- Other elements: recurse into children.
For a key-defining element:
- Read
@keys, split into individual key names. - For each key name, create a
KeyDefinitiontied to that element. - Append to
keydefsByKeyName[keyName]list.
Appending preserves traversal priority (earlier encountered definitions stay first).
After adding the key definitions, children of that key-defining element are still traversed in the same key space.
For topicref with @keyscope:
- Split
@keyscopeinto scope names. - Create child
KeySpacewithappendKeySpace(elem, *scopeNames). - If same element also has
@keys, add those keys to the new child space. - Otherwise recurse through children within that child space.
For topicref with @scope="peer":
- Record peer scope names in current key space (
addPeerMapref). - Ask manager to register deferred key space (
addDeferredKeyspace(elem)).
Deferred key spaces are keyed by absolute target map URI (urljoin(elem.base, @href)).
If that map URI already has a key space, scope names are merged into the existing space. Otherwise a new deferred KeySpace is created and indexed.
Entry point: PullUpVisitor().visit(rootKeySpace).
This visitor performs a post-order walk and propagates scope-qualified aliases upward.
For each key space:
- Visit children and gather key definitions collected from descendants.
- Add gathered definitions to current key space (
addKeyDefinition). - For each key definition in current key space and each local scope name except
#annonymous:- Copy the key definition object.
- Rename key as
scopeName.originalKey. - Add this renamed key definition to a collection passed up to ancestors.
- Special case for root space:
- Add all collected qualified key definitions to root.
Effect: ancestors can resolve descendant keys using scope-qualified names (for example subscope.key).
Entry point: PushDownVisitor().visit(rootKeySpace).
For each parent space:
- Add all parent key definitions to each child (
child.addKeyDefinitions(parent.getKeyDefinitions())). - Recurse into children.
addKeyDefinition merges by key name. If key already exists, incoming key-defining elements are prepended to existing definers, giving incoming definitions higher priority.
Because push-down adds ancestor definitions to child spaces via this prepend behavior, ancestor keys override same-name keys in descendants in the final merged view.
At the end of constructKeySpace, rootKeySpace.unsetDeferred() is called. This marks the key space as fully constructed.
For peer spaces, construction may happen later via constructDeferredKeyspace(keySpace), which:
- Resolves peer map URI from peer mapref.
- Verifies map file exists and is readable.
- Resolves that map with
resolvemap.resolveMap(). - Calls
constructKeySpace()on the deferred key space.
The effective precedence results from three mechanics:
- Initial population appends key definitions in traversal order.
- Pull-up adds qualified aliases from descendants into ancestors.
- Push-down prepends inherited ancestor definitions into descendants, so inherited ancestor definitions win for duplicate names.
For any key name, a KeySpace stores a list of definers in priority order; index 0 is the highest-priority definer used by resolution.
constructKeySpace(rootSpace, resolvedMap):
rootElem = normalize_to_element(resolvedMap)
bind rootElem as rootSpace definer
walk(rootSpace, rootElem):
if topicmeta or reltable: skip
elif map:
add map keyscope names to current space
walk children in current space
elif topicref:
if scope=peer:
register peer mapref + deferred space
elif has keyscope:
child = append child space
if has keys: add keys in child
else walk children in child
elif has keys:
add keys in current space
walk children in current space
else:
walk children in current space
else:
walk children in current space
PullUpVisitor(rootSpace)
PushDownVisitor(rootSpace)
mark rootSpace non-deferred
- A single physical map can back one shared key space even if referenced as peer from multiple places.
- Scope-qualified aliases are synthesized during pull-up, not read directly from map source.
- Final key visibility in each scope is a merged view, not only locally declared keys.
- Peer key spaces can exist as placeholders until the first resolution requires actual construction.