#!/usr/bin/env python3 """ # Script ob-rand ## Metadata #obsidian/ob-script #python **Parent**:: [[Obsidian Chore Scripts]], [[Executable Markdown File]] ## Synopsis Generate [[,rand|Random Board]] for review. Recognize zettel tags in frontmatter `tags`. Requires PyYAML (`python -m pip install PyYAML`). ## Script ```python #""" from collections.abc import Hashable import datetime import os from pathlib import Path import random import re import sys import yaml class FrontmatterLoader(yaml.SafeLoader): def construct_mapping(self, node, deep=False): mapping = {} for key_node, value_node in node.value: key = self.construct_object(key_node, deep=deep) if not isinstance(key, Hashable) or key in mapping: raise yaml.constructor.ConstructorError( None, None, "Invalid or duplicate property key", key_node.start_mark ) mapping[key] = self.construct_object(value_node, deep=deep) return mapping # Obsidian does not expand YAML merge keys into frontmatter properties. FrontmatterLoader.yaml_implicit_resolvers = { key: [(tag, regex) for tag, regex in resolvers if tag != "tag:yaml.org,2002:merge"] for key, resolvers in yaml.SafeLoader.yaml_implicit_resolvers.items() } def matches_tag(text, pattern): match = re.match(r"\A---\n(.*?)\n---(?:\n|$)", text, re.DOTALL) if not match: return False try: properties = yaml.load(match[1], Loader=FrontmatterLoader) except yaml.YAMLError: return False values = properties.get("tags") if isinstance(properties, dict) else None if isinstance(values, str): values = [values] if not isinstance(values, list): return False return any( isinstance(value, str) and re.search(pattern, "#" + value.strip(), re.IGNORECASE) for value in values ) def tagged_files(roots, pattern): matches = [] for folder in roots: for root, dirs, files in os.walk(folder): for file in files: if file.endswith(".md"): path = os.path.join(root, file) if matches_tag(Path(path).read_text(encoding="utf-8"), pattern): matches.append(path) return matches def candidates(): permanent = tagged_files(["dock", "para"], r"#zettel/(literature|permanent|index)") for folder in ["output/§ Blog/Posts", "output/§ Blog/Games"]: for root, dirs, files in os.walk(folder): permanent.extend( os.path.join(root, file) for file in files if file.startswith("♯ ") ) fleeting = tagged_files(["dock", "para", "robot"], r"#zettel/fleeting") return permanent, fleeting def render_board(permanent, fleeting, now, rng=random): lines = [f"## {now}", "", f"**Generated**:: [[{now:%Y-%m-%d}]]", ""] for title, files in [("Permanent", permanent), ("Fleeting", fleeting)]: lines.extend([f"### {title}", ""]) files = list(files) rng.shuffle(files) lines.extend( f"- [ ] [[{os.path.splitext(os.path.basename(file))[0]}]]" for file in files[:11] ) lines.append("") return "\n".join(lines) + "\n" def main(): now = datetime.datetime.now() output = render_board(*candidates(), now) if os.environ.get("DEBUG"): sys.stdout.write(output) else: with open("shore/boards/,rand.md", "a", encoding="utf-8", newline="\n") as stream: stream.write(output) if __name__ == "__main__": main() """ # vim: ft=python ``` """