PhoenixKitKnowledgeBase.Markdown (PhoenixKitKnowledgeBase v0.1.0)

Copy Markdown View Source

Everything this module knows how to read out of a note's markdown: YAML front matter, tags, [[wiki-links]], and the rendered HTML.

The markdown is the single source of truth. title_key, frontmatter, tags and the whole link index are derived from it on save, which is what keeps notes portable — the database holds nothing you could not recover by re-reading the text.

Code is not scanned

Tags and wiki-links are only read from outside code, so a [[Foo]] inside a fenced block or a backtick span stays literal instead of quietly becoming a link and a phantom entry in the vault's unresolved list. Fenced blocks (```, ~~~) and inline spans are handled; four-space indented code blocks are not, because telling one apart from a wrapped list item needs a real block parser.

Summary

Types

One parsed [[wiki-link]].

Functions

A short plain-text excerpt for note lists — front matter, markdown syntax and wiki-link brackets removed.

All tags for a note: front matter tags: merged with inline #tags in the body, downcased, de-duplicated and sorted.

The canonical form of a note title — downcased with runs of whitespace collapsed.

Parses the inside of one [[...]] into a wiki_link/0, or nil if there is no target left after trimming.

Splits YAML front matter off the top of a note.

Renders a note to sanitized HTML, with [[wiki-links]] turned into real links.

Every distinct [[wiki-link]] in a body, in order of first appearance.

Types

wiki_link()

@type wiki_link() :: %{key: String.t(), target: String.t(), label: String.t()}

One parsed [[wiki-link]].

  • :key — normalised target, what resolution matches on
  • :target — the target as written (without any #heading part)
  • :label — what to display: the alias after |, or the target

Functions

excerpt(content, length \\ 160)

@spec excerpt(String.t() | nil, pos_integer()) :: String.t()

A short plain-text excerpt for note lists — front matter, markdown syntax and wiki-link brackets removed.

Cosmetic only: it is never stored, and never used for search (that is the tsvector column's job).

extract_tags(frontmatter, body)

@spec extract_tags(map(), String.t()) :: [String.t()]

All tags for a note: front matter tags: merged with inline #tags in the body, downcased, de-duplicated and sorted.

Front matter accepts either a list (tags: [a, b]) or a single string (tags: a, b), which are the two spellings Obsidian itself accepts.

Tags are downcased so #Project and #project are one tag — they are a filtering key here, not a display string.

normalize_title(title)

@spec normalize_title(String.t() | nil) :: String.t() | nil

The canonical form of a note title — downcased with runs of whitespace collapsed.

This is what the per-vault unique index constrains and what [[wiki-links]] match on, so "My Note", "my note" and " My Note " are all the same note.

parse_wiki_link(inner)

@spec parse_wiki_link(String.t()) :: wiki_link() | nil

Parses the inside of one [[...]] into a wiki_link/0, or nil if there is no target left after trimming.

[[Target#Heading|Alias]] keeps Alias as the label and resolves against Target — the heading anchor is dropped, since this module has no heading-level addressing yet.

split_frontmatter(content)

@spec split_frontmatter(String.t() | nil) :: {map(), String.t()}

Splits YAML front matter off the top of a note.

Returns {properties, body}. A note with no front matter returns {%{}, note} unchanged — and so does one whose front matter is invalid YAML, deliberately: a typo in a property block must not swallow the writing underneath it. The broken block stays visible in the body, where the author can see and fix it.

to_html(content, resolver)

@spec to_html(String.t() | nil, (wiki_link() -> {:resolved | :unresolved, String.t()})) ::
  String.t()

Renders a note to sanitized HTML, with [[wiki-links]] turned into real links.

No caller in the UI, on purpose

There is one editing surface — Leaf — and it renders markdown itself, so nothing in the LiveView calls this today. It is kept because it is the server half of wiki-link rendering: when Leaf learns to display [[…]], this is the function that tells it which links resolve, where they point, and which are dangling. Its tests pin that contract, which is the thing worth not losing.

resolver is called once per distinct link with a wiki_link/0 and must return {:resolved, href} or {:unresolved, href}. Unresolved links get a muted, dotted-underline style so a vault's gaps are visible while reading — the class survives sanitization, which allows class on any tag.

Front matter is stripped rather than rendered; the properties are shown by the UI, not by the prose.

wiki_links(body)

@spec wiki_links(String.t() | nil) :: [wiki_link()]

Every distinct [[wiki-link]] in a body, in order of first appearance.

De-duplicated by :key, because writing [[Foo]] five times in one note is one link, not five — the same thing the (source_note_uuid, target_key) unique index enforces in the database.