Reclaiming Emacs
In order to reclaim Emacs for the keeping of knowledge, I'll need to look into converting my Obsidian notes into org.
All of my Obsidian notes have the following template, more or less:
%% Title: Created: <% tp.date.now("YYYY-MM-DD HH:mm z") %> Status: Parent: [[templates/templates|templates]] Tags: Source: %% # Title content
I wrote some code at one point to parse files, their relative paths, their DTG, parent (to create a linkage), and some other metadata. However, it appears to be lost. I was able to recover the code I wrote to convert my dendron vault over a few years ago. It could be adapted relatively easily to build a database of the notes, and then emit them.
#!/usr/bin/env python3 import platform import os import shutil import uuid import yaml OBSIDIAN_DIR = "/home/kyle/Documents/Obsidian" def dendron_dir(): system = platform.system() if system == "Windows": return "D:/Sync/Dendron/vault" elif system == "Linux": import socket if socket.gethostname() == "archos": return "/mnt/d/Sync/Dendron/vault" return platform.os.getenv("HOME") + "/Documents/Dendron/vault" else: raise RuntimeError(f"Operating system {system} is not supported.") def collect_nodes(path=dendron_dir()): names = [ filename[:-3].split(".") for filename in platform.os.listdir(path) if filename.endswith(".md") ] return names def extract_yaml(contents): if contents[0].strip() != "---": raise ValueError( "Contents should start with a YAML header (leading '---' not found" ) header = None payload = None for i in range(1, len(contents)): if contents[i].strip() == "---": header = contents[1:i] payload = contents[i + 1 :] break if header is None: raise ValueError( "Contents should start with a YAML header (trailing '---' not found)" ) return (yaml.load("".join(header).encode("utf-8")), payload) class Header: __slots__ = [ "name", "id", "title", "desc", "updated", "created", "stub", "parent", "children", "path", ] def __init__(self, name, yobj=None): self.name = name if yobj is not None: self.id = yobj["id"] self.title = yobj["title"] self.desc = yobj["desc"] self.updated = yobj["updated"] self.created = yobj["created"] self.path = os.path.join(dendron_dir(), name + ".md") self.stub = False else: self.id = str(uuid.uuid4()) self.stub = True self.parent = None self.children = set() def __repr__(self): return str(self) def __str__(self): if self.stub: return f'{self.name} : {self.id} (stub)' return str( dict(zip(self.__slots__, map(self.__getattribute__, self.__slots__))) ) def parent_name(self): if self.name == "root": return "" pname = ".".join(self.name.split(".")[:-1]) if pname == "": pname = "root" return pname def is_root(self): return self.parent_name() == "" def copy(self): paths = self.name.split('.') paths[-1] = paths[-1] + '.md' dstpath = os.path.join(OBSIDIAN_DIR, *paths) os.makedirs(os.path.dirname(dstpath), exist_ok=True) shutil.copyfile(self.path, dstpath) class Node: def __init__(self, name, yobj=None, contents=None): self.header = Header(name, yobj) if contents is not None: self.contents = "".join(contents) else: self.contents = "" def parent_name(self): return self.header.parent_name() def is_stub(self): return self.header.stub def short_name(self): if self.header.name == "root": return "root" return self.header.name.split(".")[-1] def is_root(self): return self.header.is_root() def copy(self): return self.header.copy() def __repr__(self): return str(self) def __str__(self): return str(self.header) def load_node(root, name): path = os.path.join(root, name) + ".md" try: with open(path) as nodepath: yobj, contents = extract_yaml(nodepath.readlines()) return Node(name, yobj, contents) except FileNotFoundError: return Node(name) class Graph: def __init__(self): self.names = {} self.ids = {} self.realnodes = set() self.maxdepth = 0 def load_names(self, path=dendron_dir()): names = collect_nodes(path) for name in names: print(name) self.maxdepth = max(self.maxdepth, len(name)) for i in range(1, len(name) + 1): node_name = ".".join(name[:i]) node = load_node(path, node_name) self.names[node_name] = node if not node.is_stub(): self.realnodes.add(node_name) self.ids[node.header.id] = node.header.name for (name, node) in self.names.items(): if not node.is_root(): node.header.parent = self.names[node.parent_name()].header.id parent = self.names[node.parent_name()] parent.header.children.add(node.header.id) def by_name(self, name): if name is None: return None return self.names.get(name, None) def by_id(self, id): return self.by_name(self.ids.get(id, None)) def name_from_id(self, id): return self.ids.get(id, None) def clear(self): self.names.clear() self.ids.clear() self.realnodes.clear() def _graph_children(self, node): if node is None: return None if len(node.header.children) == 0: return None g = {} for child in node.header.children: g[self.name_from_id(child)] = self._graph_children(self.by_id(child)) return g def size(self): return len(self.realnodes) def name_graph(self): return {"root": self._graph_children(self.by_name("root"))} def as_dot(self, sanitize=False): relations = [] nodes = set() for (name, node) in self.names.items(): namer = lambda n: n.short_name() if sanitize: namer = lambda n: hash(n.short_name()) % 997 for child in node.header.children: r = f'"{node.header.name}" -> "{self.name_from_id(child)}"' nodes.add(f'"{node.header.name}" [label="{namer(node)}"]') child = self.by_id(child) nodes.add(f'"{child.header.name}" [label="{namer(child)}"]') relations.append(r) nodes = sorted(nodes) return ( "digraph kortex {\n\t" + "\n\t".join(nodes) + "\n\n\t" + "\n\t".join(relations) + "\n}" ) if __name__ == "__main__": g = Graph() g.load_names() print(f"Node count: {len(g.realnodes)}") print(f"Max depth: {g.maxdepth}") with open("graph.dot", "wt") as gdot: gdot.write(g.as_dot())