How links, transclusions, images, and attachments work throughout Steel Notes,
end to end: syntax β editor token model β rendering β indexing β navigation β
lifecycle (rename/move/delete/GC).
Verified against the codebase on 2026-07-30. The [[wikilink]] grammar is
fully retired (tokenizer, classifier, extractors, and renderers recognize only
the forms below); VaultManager.migrateLegacyLinks() converts legacy content
once per vault, gated by a .steel/link-migration-v1 marker
(VaultManager.kt:50-75). History and rationale: LINK_FORMAT_MIGRATION_PLAN.md.
One markdown syntax family, distinguished only by the URL:
| Syntax | Meaning | Target semantics |
|---|---|---|
Title | Note link | Stable note **id** (= filename stem) |
!Title | Transclusion (embed) | Stable note id |
text | External link | http/https/mailto β never indexed as a reference |
text | File link | Vault-**root**-relative path |
!alt | Image | Vault-root-relative path, in practice attachments/<file> |
Frontmatter photo: / photos: / cover_image: / thumbnail: | Structured image refs | Vault-root-relative path |
Frontmatter resources: [id, β¦], companion: id, Quote resource: id | Structured note refs | Note/Resource id |
Key asymmetry: **steel:// targets identity (note id), path links target
location.** Ids never change (title renames don't touch the filename), so
note links survive any reorganization with zero content rewriting. Path
targets are only stable because attachments/ is flat and its generated
names (below) are never renamed.
The link title is display-only and may go stale after a rename;
resolution consults the id alone. Titles are sanitized at insertion
([, ], (, ), newlines β space) so they can't break the surrounding
markdown.
steel:// scheme β SteelUri.ktshared/src/commonMain/kotlin/com/steelnotes/parser/SteelUri.kt is the single
source of truth: scheme detection (hasScheme), id extraction (noteId β
trims, rejects empty host), builders (noteLink, transclusion,
sanitizeTitle), and the canonical regexes every layer matches through
(noteLinkPattern, transclusionPattern, and per-id variants for whole-vault
rewrites). Never inline a steel:// regex elsewhere β the patterns used to be
four hand-rolled copies that disagreed on case and whitespace.
Two deliberate properties:
steel:// (no id) is treated as a mistyped note reference,never as a file path β nothing tries to open a file named steel://.
[^)\s]+, so extracted ids are never blank or padded.There is no OS-registered steel:// URL scheme β the scheme lives only
inside .md files; nothing outside the app can open one.
```
vault/
notes/<id>.md (plus user folders: notes/<folder>/β¦/<id>.md)
resources/<id>.md quotes/<id>.md
attachments/<YYMMDD-8hex>.<ext> .steel/ (local markers)
```
notes/** (listFolders,moveNote β VaultManager.kt:958-999); resources, quotes,
attachments, notes, inbox, .steel are structural, never user
folders.
note-2026-07-22-cce1abf7,share-2026-07-20-154839). Ids never contain /.
AttachmentNaming.generate(ext)(model/AttachmentNaming.kt): flat YYMMDD-{8hex}.{ext}. A one-time
vault migration flattened legacy attachment subdirectories and rewrote
the referencing markdown (VaultManager.kt:126, 227-290).
shared/src/commonMain/kotlin/com/steelnotes/editor/
InlineTokens.kt β flat styled-runs model (sealed interface InlineToken):MarkerToken (hidden syntax chars), TextToken (visible text with style
flags), and AtomicToken(raw, kind, text, payload) with
Kind { LINK, NOTE_LINK }:
NOTE_LINK: text = title, payload = bare note id (scheme stripped viaSteelUri).
LINK: text = link text, payload = URL (external or path).raw reconstructs the resourceline exactly β the offset map (resourceβdisplay) is built on it. Any new
link syntax must preserve this.
InlineTokenizer.kt β inside the text branch, a steel target emitsNOTE_LINK (:216), everything else LINK. A whole-line
!β¦ is not an inline token β transclusions are block-level.
MarkdownLineClassifier.kt:91-95 β whole-line !β¦ classifiesas TRANSCLUSION, other whole-line !β¦ as IMAGE; a malformed steel
target is neither.
FormattingEngine.kt:584 insertNoteLink(text, cursor, targetId, title) β<title> (blank title falls back to the id).
FormattingEngine.kt:596 insertTransclusion(β¦) β !<title>on its own line.
[[-typed autocomplete):NoteLinkPickerView.swift and QuotePickerView.swift deliver (id, title)
through UnifiedEditorViewController to the engine.
EditorAttachmentController.swift:220 insertImageMarkdown(path:)inserts !β¦; photo capture writes bytes to
vault/attachments/ under a generated name, then inserts (:274-281).
On load, the controller hydrates missing attachment bytes from sync (:40).
ParagraphBuilder.swift)Hidden-marker WYSIWYG: syntax characters become hidden offset-map segments
(displayLength 0).
:452-478): the leading [ renders as a one-glyphNSTextAttachment icon β an .attachment(1,1) map segment, the tested
atomic-cursor primitive β showing the linked note's type icon and color
(via itemLookup, generic note icon as fallback); the title is visible in
blue; the ](steel://id) suffix is hidden. Anchoring the icon on [ keeps
edits near it on the full-rebuild path.
:479-495): [ hidden, text blue + underlined,](url) suffix hidden.
textView(_:shouldInteractWith:) anywhere.!alt β parseImageLine (:514) β inline image (rejects steeltargets).
!Title β parseTransclusionLine (:524) β line collapsed;the note surfaces as a "Linked Notes" card below the editor
(UnifiedEditorViewController), ids via TransclusionExtractor; tapping a
card navigates.
InlineMarkdownParser.swift)Same KMP tokenizer rendered to SwiftUI Text: NOTE_LINK = type icon +
accent title; LINK = accent + underline. Not tappable.
Tap-to-navigate surfaces today: transclusion cards and Resource
table-of-contents rows. Navigation is AppNavigation navPath.append(noteId)
resolved by getNoteById.
links table (backlinks graph)SteelNotes.sq:39: links(resource_id, target_path, link_type) βdespite the column name, target_path holds the note id.
NoteIndexer.kt:118-140 per index pass, matching throughSteelUri.ids(...) over code-stripped prose:
note links β 'reference', transclusions β 'transclusion', structured
refs (Note.resources, Quote.resourceId) β 'structured'.
Markdown path links are not indexed here.
getBacklinks (SteelNotes.sq:163) βVaultManager.getBacklinks β SharedCoreService.getBacklinks
(SharedCoreService.swift:331). No UI consumes it yet.
file_reference table (dependency graph)SteelNotes.sq:87; populated by ReferenceIndexer.kt fromReferenceParser.kt.
ReferenceParser extracts LINK / TRANSCLUSION / IMAGE / ATTACHMENT frombody and frontmatter (photo, photos, cover_image, thumbnail).
Steel targets are stored as the bare id; path targets are resolved via
resolveRelativePath (./, ../, else vault-root-relative); external URLs
are excluded; code fences and inline code are skipped.
Serialized by NoteSerializer.kt, parsed back by NoteFactory.kt:
resources: [<id>, β¦] β notes citing Resources (reference list).companion: <id> + resource_url: <url> β the identity fact that a Note *is*the notes-tab companion of a Resource (Note.companionOf / resourceUrl,
Note.kt:36-50). resource_url is stamped once at creation so the raw .md
names a reachable article, not just an opaque id. See COMPANION_NOTES.md.
resource: <id>; Resources carry cover_image / thumbnailpaths and url.
Both keys are omitted when unset so ordinary notes serialize byte-identically
(no reindex churn).
elsewhere go stale (accepted, Obsidian-alias-style). No staleness refresh
exists.
VaultManager.moveNote): patches the file_reference index only;file contents never rewritten. Safe for steel links (id unchanged); would
break body path links, which in practice only target the never-moving
attachments/.
VaultManager.deleteNote:769): removes file + index rows, thendeletes the note's attachments if no other note references them. It does
not rewrite other notes' content β inbound steel:// links dangle
(render fine; navigation target missing). ReferenceRewriter.rewrite /
removeReferences (whole-vault content rewrites incl. frontmatter fields)
exist and are tested but have no production caller β dormant machinery
for id-rewrites/unlinking if ever needed.
VaultManager.kt:819): scans attachments/ against theunion of all references parsed by ReferenceParser (body + frontmatter),
with an mtime safety window, a pending-sync-push exclusion, and a
continuous-orphan grace period before deletion.
No server or CLI parsing of links.
1. Nothing is tappable inline β note links, path links, and web links all
render as links but only transclusion cards navigate.
2. Two disjoint index systems (links id-keyed, file_reference
path-keyed) built from two parsers (SteelUri extractors vs
ReferenceParser).
3. Backlinks are computed but never shown.
4. Deleting a note leaves dangling inbound links (the unlink rewriter is dead
code).
5. Body image paths are vault-root-relative while notes live in notes/, so
the raw files don't render in strict external md tooling β see
MD_EXPORT_EVALUATION.md.