Embedding

You can embed files, using ![[..]] - a syntax inspired by Obsidian. The HTML can be fully customized for each embed types.

Warning

The embed wiki-link syntax must appear on a paragraph of its own, with no other text added next to it.1 Recursive embeds are supported (see [#cyclic-embeds] below for the one safety stop).

Notes

Embedding a note will simply inline it. For example, using ![[start]] displays the following:

Getting Started

Follow these steps to get started with Emanote.

  1. Install Emanote
  2. Use your existing notebook, or create one from emanote-template1.
  3. Run emanote run --port=8080 (or just emanote) in terminal after cd’ing to that notebook folder; this will launch the live server.
    • Or, if you only want to generate the HTML files (for deployment), run mkdir /tmp/output; emanote gen /tmp/output.
  4. Visit Guide to learn more about Emanote, or Examples to get inspired first.2

Cyclic embeds

Embeds compose recursively — an embedded note may itself embed further notes — but a chain that closes back on itself (a → b → a, or a note that embeds itself) cannot be expanded without a fixpoint. Emanote detects the cycle and substitutes an inline ↺ Cyclic embed: <title> (via …) placeholder at the point the loop would close, naming both the offending note and the chain that led there.

The note Cyclic embed demo embeds itself. Embedding it here renders one level of its content, then the placeholder where the inner self-embed would have resolved:

Cyclic embed demo

This note exists only to demonstrate Emanote’s cyclic-embed detection (![[..]]). The line below is a self-embed of this same note — Emanote stops at the point the loop would close and emits a placeholder instead of expanding the embed forever.

↺ Cyclic embed: Cyclic embed demo (via Cyclic embed demo, Embedding)

Files

Embedding of File WikiLinks, as indicated in the aforementioned Obsidian help page, will eventually be supported; for now, certain file types already work.

Progress

See https://github.com/srid/emanote/issues/24 for progress on this feature.

Images

Embedding image files as, say, ![[disaster-girl.jpg]] is equivalent to ![](path/to/disaster-girl.jpg) (this example links to this image).

See also

It is also posible to add images inline (example, here’s the site favicon: [[favicon.svg]]) say in the middle of a paragraph.

Videos

The following is the result of using ![[death-note.mp4]] (note that ![](death-note.mp4) also works).

Audio

The following is the result of using ![[cat.ogg]] (note that ![](cat.ogg) also works).

PDFs

PDFs can be embedded using the same syntax. The following is the result of using ![[git-cheat-sheet-education.pdf]] (note that ![](git-cheat-sheet-education.pdf) also works):

Open pdf

Code files

Source-code, markup, and configuration files can be embedded using the same syntax. The file’s extension is matched against skylighting’s bundled syntax map, and the content is highlighted at build time through the same pipeline used for fenced code blocks (see Syntax Highlighting) — an embedded .hs file renders identically to a fenced Haskell code block in a regular note.

The following is the result of using ![[haskell-code.hs]] (the regular Markdown form ![](haskell-code.hs) also works):

module HaskellCode where

main :: IO ()
main = do
  print "Hello World"

A C file:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");

    return 0;
}

A JSON file:

{
  "name": "emanote",
  "languages": ["haskell", "nix"],
  "embeds": {
    "code": true,
    "math": true,
    "mermaid": true
  }
}

A TOML file:

[package]
name = "emanote"
version = "2.0.0"

[features]
default = ["live-server", "mcp"]

[server]
host = "127.0.0.1"
port = 8080

A CSS snippet:

/* A small CSS sample used to demonstrate the ![[..]] code embed feature. */
.emanote-code-embed {
  border-radius: 0.5rem;
  margin-bottom: 1.5rem;
}

.emanote-code-embed pre {
  font-family: "Space Mono", monospace;
}

A bundled Lua filter:

--- wordcount.lua - append a word/character count footer to the document.
---
--- Derived from https://github.com/pandoc/lua-filters/tree/master/wordcount,
--- but adapted for Emanote's live-server pipeline: the upstream filter
--- prints to stdout and calls `os.exit(0)`, both of which would corrupt or
--- terminate `emanote run`. This version walks the body, counts, and
--- appends a small styled footer block to the rendered document instead.
---
--- FORMAT-agnostic: produces the same output regardless of the writer.

local function new_counts()
  return {
    words = 0,
    characters = 0,
    characters_and_spaces = 0,
  }
end

local function count_filter(counts)
  return {
    Str = function(el)
      if el.text:match("%P") then
        counts.words = counts.words + 1
      end
      counts.characters = counts.characters + utf8.len(el.text)
      counts.characters_and_spaces = counts.characters_and_spaces + utf8.len(el.text)
    end,

    Space = function()
      counts.characters_and_spaces = counts.characters_and_spaces + 1
    end,

    Code = function(el)
      local _, n = el.text:gsub("%S+", "")
      counts.words = counts.words + n
      local nospace = el.text:gsub("%s", "")
      counts.characters = counts.characters + utf8.len(nospace)
      counts.characters_and_spaces = counts.characters_and_spaces + utf8.len(el.text)
    end,

    CodeBlock = function(el)
      local _, n = el.text:gsub("%S+", "")
      counts.words = counts.words + n
      local nospace = el.text:gsub("%s", "")
      counts.characters = counts.characters + utf8.len(nospace)
      counts.characters_and_spaces = counts.characters_and_spaces + utf8.len(el.text)
    end,
  }
end

local css = [[
<style>
.wordcount-footer {
  margin-top: 3rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--color-gray-200);
  display: flex;
  justify-content: flex-end;
  font-size: 0.8125rem;
  color: var(--color-gray-500);
}
.wordcount-footer dl {
  display: flex;
  gap: 1.5rem;
  margin: 0;
}
.wordcount-footer div { display: flex; gap: 0.375rem; align-items: baseline; }
.wordcount-footer dt { font-variant: small-caps; letter-spacing: 0.04em; }
.wordcount-footer dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--color-gray-800);
}
.dark .wordcount-footer { border-top-color: var(--color-gray-800); color: var(--color-gray-400); }
.dark .wordcount-footer dd { color: var(--color-gray-100); }
</style>
]]

local function dlEntry(label, value)
  return string.format(
    "<div><dt>%s</dt><dd>%d</dd></div>",
    label, value
  )
end

function Pandoc(doc)
  local counts = new_counts()
  pandoc.walk_block(pandoc.Div(doc.blocks), count_filter(counts))
  local html = table.concat({
    "<dl>",
    dlEntry("words", counts.words),
    dlEntry("chars", counts.characters),
    dlEntry("chars + spaces", counts.characters_and_spaces),
    "</dl>",
  })
  local footer = pandoc.Div(
    { pandoc.RawBlock("html", html) },
    pandoc.Attr("", { "wordcount-footer" }, {})
  )
  table.insert(doc.blocks, pandoc.RawBlock("html", css))
  table.insert(doc.blocks, footer)
  return doc
end

Emanote source files that also have structural meaning are still available to wikilinks. For example, index.yaml continues to feed the metadata cascade, but ![[index.yaml]] can also inline the topmost file from the layer stack as a highlighted YAML block:

template:
  urlStrategy: pretty

Template sources are linkable the same way, so a guide can point readers at a live override such as after-note.tpl.

Supported extensions

Anything skylighting’s syntaxesByExtension recognises will highlight — that’s hundreds of languages spanning Ada through Zsh, including the common programming languages, shells, markup (.html, .tex, .rst, …), data formats (.json, .yaml, .toml, .xml, …), and config files (.ini, .css, .scss, …). The image, audio, video, and PDF extensions listed above take precedence and embed via their dedicated templates instead.

To add support for a new language, contribute a Kate XML syntax file upstream to skylighting — Emanote picks it up automatically the next time the dependency is bumped.

#emanote/syntax/demo #cyclic-embeds