Outline consisting of blocks

In Obsidian, I can’t find any plugin that supports outlining AND block-level referencing, embedding, etc. Of course, we have plugins like Better Search Views, Copy Block Link, Outliner, etc. but to represent a markdown file as a tree-like, hierarchical data structure, (e.g., .json object with hierarchy from top to bottom H1→H2→…→H6→bullet (indented 0x)→bullet (indented 1x)→bullet (indented 2x)→…→etc.) seems super useful because it would allow for programmatic traversing/searching that tree via depth-first, breadth-first, according to certain criteria, etc. It seems like this is the way an AI agent for example should be able to search through your notes rather than just reading an entire .md file

Is there any plugin that takes markdown and represents it this way or maps it to this kind of data structure?

1 Like

It seems what you’re looking for is already part of Obsidian’s API. app.metadataCache.getFileCache(file) returns a CachedMetadata object - the parsed structure of the note.

Any plugin can read this. Any agent with shell access can read it too, via the CLI:

obsidian eval code='JSON.stringify(app.metadataCache.getFileCache(app.workspace.getActiveFile()), null, 2)'

The result is flat so to get the nested tree you need a small wrapper that merges them by line number and parent pointers. You can try it in the dev console

(async () => {
  const file = app.workspace.getActiveFile();
  if (!file) return console.log('No active file');

  const cache = app.metadataCache.getFileCache(file) ?? {};
  const lines = (await app.vault.cachedRead(file)).split('\n');

  const nodes = [
    ...(cache.headings ?? []).map(h => ({
      kind: 'heading', level: h.level, text: h.heading, line: h.position.start.line
    })),
    ...(cache.listItems ?? []).map(l => ({
      kind: 'list',
      text: lines[l.position.start.line].replace(/^\s*(?:[-*+]|\d+\.)\s+(?:\[[ x]\]\s+)?/, ''),
      line: l.position.start.line,
      parent: l.parent  // line number of parent; negative for top-level
    }))
  ].sort((a, b) => a.line - b.line);

  // Fold into a tree
  const root = { type: 'file', name: file.basename, level: 0, children: [] };
  const stack = [root];
  const listByLine = new Map();

  for (const n of nodes) {
    if (n.kind === 'heading') {
      while (stack[stack.length - 1].level >= n.level) stack.pop();
      const node = { type: 'heading', level: n.level, text: n.text, children: [] };
      stack[stack.length - 1].children.push(node);
      stack.push(node);
    } else {
      const item = { type: 'list', text: n.text, line: n.line, children: [] };
      listByLine.set(n.line, item);
      const parent = n.parent >= 0 ? listByLine.get(n.parent) : stack[stack.length - 1];
      (parent ?? stack[stack.length - 1]).children.push(item);
    }
  }

  console.log(root);
  copy(JSON.stringify(root, null, 2));
  console.log('JSON copied to clipboard ✔');
})();