From 79e93c167e31882974a36ad52fd85d1719c5aed9 Mon Sep 17 00:00:00 2001 From: Ignacio Ballesteros Date: Sat, 14 Feb 2026 18:00:32 +0100 Subject: [PATCH] quartz build --- README.md | 60 +++++ example-notes/concepts/atomic-notes.org | 38 +++ .../concepts/knowledge-management.org | 26 ++ example-notes/concepts/linking.org | 43 +++ example-notes/concepts/spaced-repetition.org | 41 +++ example-notes/concepts/zettelkasten.org | 38 +++ example-notes/daily/2024-01-20.org | 25 ++ example-notes/index.org | 17 ++ example-notes/projects/learning-system.org | 34 +++ example-notes/projects/website.org | 40 +++ example-notes/references/ahrens2017.org | 32 +++ example-notes/references/index.org | 31 +++ flake.nix | 254 +++++++++++++++++- src/org_to_quartz/main.py | 30 ++- src/org_to_quartz/markdown_writer.py | 20 +- 15 files changed, 710 insertions(+), 19 deletions(-) create mode 100644 README.md create mode 100644 example-notes/concepts/atomic-notes.org create mode 100644 example-notes/concepts/knowledge-management.org create mode 100644 example-notes/concepts/linking.org create mode 100644 example-notes/concepts/spaced-repetition.org create mode 100644 example-notes/concepts/zettelkasten.org create mode 100644 example-notes/daily/2024-01-20.org create mode 100644 example-notes/index.org create mode 100644 example-notes/projects/learning-system.org create mode 100644 example-notes/projects/website.org create mode 100644 example-notes/references/ahrens2017.org create mode 100644 example-notes/references/index.org diff --git a/README.md b/README.md new file mode 100644 index 0000000..bce4497 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# org-to-quartz + +Convert org-mode notes to a Quartz static site. + +## Quick Start + +### Build pages from your notes + +```bash +nix build .#example-pages +# Output in ./result/ +``` + +### Serve locally (development) + +```bash +nix run .#serve ./path/to/your/notes +# Opens http://localhost:8080 +``` + +### Convert org to markdown only + +```bash +nix run . -- ./input-notes ./output-dir +``` + +## Use in your own flake + +```nix +{ + inputs.org-notes-quartz.url = "github:your-user/org-notes-quartz"; + + outputs = { self, nixpkgs, org-notes-quartz, ... }: + let + system = "x86_64-linux"; + buildQuartzPages = org-notes-quartz.lib.${system}.buildQuartzPages; + in { + packages.${system}.site = buildQuartzPages { + contentDir = ./notes; + # quartzConfig = ./quartz.config.ts; # optional + # quartzLayout = ./quartz.layout.ts; # optional + }; + }; +} +``` + +## Available packages + +| Package | Description | +|-----------------------------|---------------------------------------------| +| `default` / `org-to-quartz` | CLI to convert org files to Quartz markdown | +| `quartz` | Quartz v4.5.2 static site generator | +| `example-pages` | Example build from `test-notes/` | + +## Available apps + +| App | Description | +|-----------|-------------------------------| +| `default` | Run org-to-quartz converter | +| `serve` | Serve notes with live preview | diff --git a/example-notes/concepts/atomic-notes.org b/example-notes/concepts/atomic-notes.org new file mode 100644 index 0000000..63655b6 --- /dev/null +++ b/example-notes/concepts/atomic-notes.org @@ -0,0 +1,38 @@ +#+title: Atomic Notes +#+date: 2024-01-11 +#+filetags: :concept:writing: + +An atomic note contains exactly one idea, fully expressed. + +* Characteristics + +- Self-contained +- Single focus +- Linkable +- Reusable + +* Benefits + +When notes are atomic: + +- Easy to link from multiple contexts +- Simple to reorganize +- Clear to understand +- Quick to write + +* Examples + +Good atomic note titles: +- "Spaced repetition improves long-term retention" +- "Links create unexpected connections" +- "Writing clarifies thinking" + +Bad titles: +- "Notes about learning" +- "Stuff I read" + +* Connection to Other Concepts + +Atomic notes are the foundation of [[file:zettelkasten.org][Zettelkasten]] and enable effective [[file:linking.org][Linking]]. + +This principle comes from software engineering - see cite:martin2008 on the Single Responsibility Principle. diff --git a/example-notes/concepts/knowledge-management.org b/example-notes/concepts/knowledge-management.org new file mode 100644 index 0000000..49bc813 --- /dev/null +++ b/example-notes/concepts/knowledge-management.org @@ -0,0 +1,26 @@ +#+title: Knowledge Management +#+date: 2024-01-10 +#+filetags: :concept:learning: + +Knowledge management is the process of creating, sharing, using, and managing information. + +* Core Principles + +1. *Capture* - Record ideas as they come +2. *Organize* - Structure for retrieval +3. *Connect* - Link related concepts +4. *Review* - Revisit and refine + +* Related Concepts + +- [[file:zettelkasten.org][Zettelkasten]] - A specific method for knowledge management +- [[file:atomic-notes.org][Atomic Notes]] - The building blocks +- [[file:linking.org][Linking]] - How connections create value + +* Tools + +See [[file:../projects/website.org][Website Project]] for how to publish your knowledge base. + +* References + +The field draws from library science cite:weinberger2007 and cognitive psychology cite:ahrens2017. diff --git a/example-notes/concepts/linking.org b/example-notes/concepts/linking.org new file mode 100644 index 0000000..179b0e0 --- /dev/null +++ b/example-notes/concepts/linking.org @@ -0,0 +1,43 @@ +#+title: Linking +#+date: 2024-01-13 +#+filetags: :concept:structure: + +Links between notes create a network of knowledge. + +* Types of Links + +** Hierarchical +Parent-child relationships (folders, outlines) + +** Associative +Related concepts ([[file:zettelkasten.org][Zettelkasten]] style) + +** Sequential +Reading order, workflows + +* Why Links Matter + +#+begin_src text + [Note A] --- related to --- [Note B] + | | + +--- supports --- [Note C] -+ +#+end_src + +Links reveal: +- Hidden connections +- Knowledge gaps +- Entry points + +* Backlinks + +When [[file:atomic-notes.org][Atomic Notes]] links here, this page knows about it. Backlinks show: + +- Who references this concept +- How the idea is used +- Related contexts + +* Tools Supporting Links + +See [[file:../projects/website.org][Website Project]] for publishing linked notes. + +The web itself is built on links cite:bernerslee1999. diff --git a/example-notes/concepts/spaced-repetition.org b/example-notes/concepts/spaced-repetition.org new file mode 100644 index 0000000..ad03d67 --- /dev/null +++ b/example-notes/concepts/spaced-repetition.org @@ -0,0 +1,41 @@ +#+title: Spaced Repetition +#+date: 2024-01-14 +#+filetags: :concept:learning:memory: + +Spaced repetition is a learning technique that involves reviewing material at increasing intervals. + +* The Forgetting Curve + +Without review, we forget: +- 50% within an hour +- 70% within 24 hours +- 90% within a week + +* How Spacing Helps + +Review schedule example: + +| Review | Interval | +|--------|----------| +| 1st | 1 day | +| 2nd | 3 days | +| 3rd | 1 week | +| 4th | 2 weeks | +| 5th | 1 month | + +* Software + +- Anki +- Org-drill (Emacs) +- RemNote + +* Connection to Note-Taking + +[[file:zettelkasten.org][Zettelkasten]] provides natural spaced repetition through: +- Random encounters while linking +- Review during searches +- Connections triggering recall + +* Research + +Based on memory research cite:ebbinghaus1885 and modern applications cite:pimsleur1967. diff --git a/example-notes/concepts/zettelkasten.org b/example-notes/concepts/zettelkasten.org new file mode 100644 index 0000000..465a344 --- /dev/null +++ b/example-notes/concepts/zettelkasten.org @@ -0,0 +1,38 @@ +#+title: Zettelkasten +#+date: 2024-01-12 +#+filetags: :concept:methodology:productivity: + +The Zettelkasten (German for "slip box") is a note-taking method developed by sociologist Niklas Luhmann. + +* Key Features + +- Each note contains one idea ([[file:atomic-notes.org][Atomic Notes]]) +- Notes link to each other extensively ([[file:linking.org][Linking]]) +- No strict hierarchy - emergence through connections + +* How It Works + +#+begin_quote +"One cannot think without writing." - Niklas Luhmann +#+end_quote + +1. Write fleeting notes +2. Convert to permanent notes +3. Add to the slip box with links +4. Review connections + +* Implementation + +| Analog | Digital | +|--------|---------| +| Index cards | Org-roam | +| Physical box | Obsidian | +| Manual links | Wiki links | + +* Why It Works + +The method leverages [[file:spaced-repetition.org][Spaced Repetition]] through organic review and builds on [[file:knowledge-management.org][Knowledge Management]] principles. + +* Further Reading + +See cite:ahrens2017 for the definitive guide. diff --git a/example-notes/daily/2024-01-20.org b/example-notes/daily/2024-01-20.org new file mode 100644 index 0000000..6115b12 --- /dev/null +++ b/example-notes/daily/2024-01-20.org @@ -0,0 +1,25 @@ +#+title: 2024-01-20 +#+date: 2024-01-20 +#+filetags: :daily:journal: + +* Tasks + +- [X] Review [[file:../concepts/zettelkasten.org][Zettelkasten]] notes +- [X] Update [[file:../projects/website.org][Website Project]] status +- [ ] Read more of cite:ahrens2017 + +* Notes + +Realized that [[file:../concepts/atomic-notes.org][Atomic Notes]] principle applies beyond note-taking: +- Commit messages +- Documentation sections +- Email paragraphs + +* Ideas + +Could [[file:../concepts/spaced-repetition.org][Spaced Repetition]] work for code review? Flag old code for periodic review. + +* References + +- Discussed [[file:../concepts/knowledge-management.org][Knowledge Management]] with colleague +- Found new source on [[file:../concepts/linking.org][Linking]] theory diff --git a/example-notes/index.org b/example-notes/index.org new file mode 100644 index 0000000..394b046 --- /dev/null +++ b/example-notes/index.org @@ -0,0 +1,17 @@ +#+title: Welcome +#+date: 2024-01-15 +#+filetags: :home: + +This is a digital garden built from org-mode notes. + +* Getting Started + +Explore the different sections: + +- [[file:concepts/knowledge-management.org][Knowledge Management]] - Core concepts +- [[file:projects/website.org][Website Project]] - An example project +- [[file:references/index.org][References]] - Books and papers + +* Recent Updates + +Check out [[file:concepts/zettelkasten.org][Zettelkasten]] for note-taking methodology. diff --git a/example-notes/projects/learning-system.org b/example-notes/projects/learning-system.org new file mode 100644 index 0000000..538e1a9 --- /dev/null +++ b/example-notes/projects/learning-system.org @@ -0,0 +1,34 @@ +#+title: Learning System +#+date: 2024-01-18 +#+filetags: :project:planning: + +A personal system for continuous learning. + +* Components + +1. *Capture* - Quick notes, highlights, ideas +2. *Process* - Convert to [[file:../concepts/atomic-notes.org][Atomic Notes]] +3. *Connect* - Add [[file:../concepts/linking.org][Links]] to existing notes +4. *Review* - [[file:../concepts/spaced-repetition.org][Spaced Repetition]] for retention + +* Daily Practice + +| Time | Activity | +|------|----------| +| Morning | Review queue | +| Anytime | Capture ideas | +| Evening | Process inbox | + +* Tools + +- Org-mode for capture +- Org-roam for [[file:../concepts/zettelkasten.org][Zettelkasten]] +- Org-drill for review + +* Inspiration + +Based on [[file:../concepts/knowledge-management.org][Knowledge Management]] research and cite:forte2022 Building a Second Brain methodology. + +* Status + +Currently implementing the capture phase. See [[file:website.org][Website Project]] for publishing component. diff --git a/example-notes/projects/website.org b/example-notes/projects/website.org new file mode 100644 index 0000000..a431f78 --- /dev/null +++ b/example-notes/projects/website.org @@ -0,0 +1,40 @@ +#+title: Website Project +#+date: 2024-01-20 +#+filetags: :project:active: + +Building a digital garden from org-mode notes. + +* Overview + +Transform [[file:../concepts/knowledge-management.org][Knowledge Management]] practices into a public website. + +* Goals + +- [X] Convert org files to markdown +- [X] Preserve [[file:../concepts/linking.org][Linking]] between notes +- [ ] Add search functionality +- [ ] Deploy to hosting + +* Technical Stack + +| Component | Tool | +|-----------|------| +| Source | Org-mode | +| Converter | org-to-quartz | +| Generator | Quartz | +| Hosting | TBD | + +* Architecture + +#+begin_src text + org files --> org-to-quartz --> markdown --> Quartz --> HTML +#+end_src + +* Related + +- [[file:../concepts/zettelkasten.org][Zettelkasten]] - The methodology behind the content +- [[file:../references/index.org][References]] - Sources cited throughout + +* Notes + +Using [[file:../concepts/atomic-notes.org][Atomic Notes]] makes each page focused and linkable. diff --git a/example-notes/references/ahrens2017.org b/example-notes/references/ahrens2017.org new file mode 100644 index 0000000..1c7d226 --- /dev/null +++ b/example-notes/references/ahrens2017.org @@ -0,0 +1,32 @@ +#+title: How to Take Smart Notes +#+date: 2024-01-05 +#+filetags: :reference:book: + +* Citation + +Ahrens, S. (2017). /How to Take Smart Notes/. CreateSpace. + +* Summary + +A guide to the [[file:../concepts/zettelkasten.org][Zettelkasten]] method based on Niklas Luhmann's practice. + +* Key Ideas + +1. Writing is thinking +2. One idea per note ([[file:../concepts/atomic-notes.org][Atomic Notes]]) +3. Links over folders ([[file:../concepts/linking.org][Linking]]) +4. Bottom-up organization + +* Quotes + +#+begin_quote +"The slip-box is designed to present you with ideas you have already forgotten." +#+end_quote + +#+begin_quote +"Every intellectual endeavour starts from an already existing preconception." +#+end_quote + +* Application + +This book directly influenced the [[file:../projects/learning-system.org][Learning System]] project. diff --git a/example-notes/references/index.org b/example-notes/references/index.org new file mode 100644 index 0000000..a12523d --- /dev/null +++ b/example-notes/references/index.org @@ -0,0 +1,31 @@ +#+title: References +#+date: 2024-01-08 +#+filetags: :reference:index: + +Collection of books, papers, and resources. + +* Books + +** Knowledge Management +- cite:ahrens2017 - How to Take Smart Notes +- cite:forte2022 - Building a Second Brain + +** Software & Writing +- cite:martin2008 - Clean Code +- cite:weinberger2007 - Everything Is Miscellaneous + +* Papers + +- cite:ebbinghaus1885 - Memory: A Contribution to Experimental Psychology +- cite:pimsleur1967 - A Memory Schedule + +* Web Resources + +- cite:bernerslee1999 - Weaving the Web + +* How References Are Used + +References connect to concepts: +- [[file:../concepts/zettelkasten.org][Zettelkasten]] cites cite:ahrens2017 +- [[file:../concepts/spaced-repetition.org][Spaced Repetition]] cites cite:ebbinghaus1885 +- [[file:../concepts/linking.org][Linking]] cites cite:bernerslee1999 diff --git a/flake.nix b/flake.nix index e936df7..e7f0bb8 100644 --- a/flake.nix +++ b/flake.nix @@ -44,35 +44,267 @@ }; }; - # Script to serve quartz with converted notes + # Quartz package - builds the Quartz static site generator + quartz = pkgs.buildNpmPackage { + pname = "quartz"; + version = "4.5.2"; + + src = pkgs.fetchFromGitHub { + owner = "jackyzha0"; + repo = "quartz"; + rev = "v4.5.2"; + hash = "sha256-A6ePeNmcsbtKVnm7hVFOyjyc7gRYvXuG0XXQ6fvTLEw="; + }; + + npmDepsHash = "sha256-xxK9qy04m1olekOJIyYJHfdkYFzpjsgcfyFPuKsHpKE="; + + # Quartz doesn't have a build step in the traditional sense + # It's a CLI tool that builds sites at runtime + dontNpmBuild = true; + + # Install the quartz CLI + installPhase = '' + runHook preInstall + + mkdir -p $out/lib/quartz + cp -r . $out/lib/quartz + + mkdir -p $out/bin + cat > $out/bin/quartz <<'WRAPPER' + #!/usr/bin/env bash + # Quartz CLI wrapper - runs quartz from its library directory + # or from current directory if it contains a quartz config + QUARTZ_LIB="$out/lib/quartz" + + if [[ -f "./quartz.config.ts" ]]; then + # Run from current directory (user's project) + exec ${pkgs.nodejs}/bin/node "$QUARTZ_LIB/quartz/bootstrap-cli.mjs" "$@" + else + # Run from quartz lib directory + cd "$QUARTZ_LIB" + exec ${pkgs.nodejs}/bin/node quartz/bootstrap-cli.mjs "$@" + fi + WRAPPER + # Replace $out with actual path + substituteInPlace $out/bin/quartz --replace-quiet '$out' "$out" + chmod +x $out/bin/quartz + + runHook postInstall + ''; + + meta = { + description = "A fast, batteries-included static-site generator for digital gardens"; + homepage = "https://quartz.jzhao.xyz"; + mainProgram = "quartz"; + }; + }; + + # Default quartz config that works in Nix sandbox (no network fetches) + defaultQuartzConfig = pkgs.writeText "quartz.config.ts" '' + import { QuartzConfig } from "./quartz/cfg" + import * as Plugin from "./quartz/plugins" + + const config: QuartzConfig = { + configuration: { + pageTitle: "Quartz Notes", + pageTitleSuffix: "", + enableSPA: true, + enablePopovers: true, + analytics: null, + locale: "en-US", + baseUrl: "localhost", + ignorePatterns: ["private", "templates", ".obsidian"], + defaultDateType: "modified", + theme: { + fontOrigin: "local", + cdnCaching: false, + typography: { + header: "sans-serif", + body: "sans-serif", + code: "monospace", + }, + colors: { + lightMode: { + light: "#faf8f8", + lightgray: "#e5e5e5", + gray: "#b8b8b8", + darkgray: "#4e4e4e", + dark: "#2b2b2b", + secondary: "#284b63", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#fff23688", + }, + darkMode: { + light: "#161618", + lightgray: "#393639", + gray: "#646464", + darkgray: "#d4d4d4", + dark: "#ebebec", + secondary: "#7b97aa", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#b3aa0288", + }, + }, + }, + }, + plugins: { + transformers: [ + Plugin.FrontMatter(), + Plugin.CreatedModifiedDate({ + priority: ["frontmatter", "git", "filesystem"], + }), + Plugin.SyntaxHighlighting({ + theme: { + light: "github-light", + dark: "github-dark", + }, + keepBackground: false, + }), + Plugin.OxHugoFlavouredMarkdown(), + Plugin.GitHubFlavoredMarkdown(), + Plugin.TableOfContents(), + Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), + Plugin.Description(), + Plugin.Latex({ renderEngine: "katex" }), + ], + filters: [Plugin.RemoveDrafts()], + emitters: [ + Plugin.AliasRedirects(), + Plugin.ComponentResources(), + Plugin.ContentPage(), + Plugin.FolderPage(), + Plugin.TagPage(), + Plugin.ContentIndex({ + enableSiteMap: true, + enableRSS: true, + }), + Plugin.Assets(), + Plugin.Static(), + Plugin.Favicon(), + Plugin.NotFoundPage(), + // CustomOgImages disabled - requires network access for fonts + ], + }, + } + + export default config + ''; + + # Function to build Quartz pages from a content directory + # Usage: buildQuartzPages { contentDir = ./my-org-notes; } + # contentDir: directory containing org files or markdown files + # quartzConfig: optional path to quartz.config.ts (uses sandbox-friendly default if not provided) + # quartzLayout: optional path to quartz.layout.ts + buildQuartzPages = { + contentDir, + quartzConfig ? null, + quartzLayout ? null, + name ? "quartz-pages" + }: + pkgs.stdenv.mkDerivation { + inherit name; + + # Don't use src, we copy everything in buildPhase + dontUnpack = true; + + nativeBuildInputs = [ + pkgs.nodejs + org-to-quartz + ]; + + buildPhase = '' + runHook preBuild + + # Set up a writable quartz directory + cp -r ${quartz}/lib/quartz/* . + chmod -R u+w . + + # Remove default content + rm -rf content + mkdir -p content + + # Convert org files to markdown, or copy markdown directly + if ls ${contentDir}/*.org >/dev/null 2>&1; then + echo "Converting org notes from ${contentDir}..." + org-to-quartz ${contentDir} content + else + echo "Copying content from ${contentDir}..." + cp -r ${contentDir}/* content/ + fi + + # Apply quartz config (use sandbox-friendly default if not provided) + ${if quartzConfig != null then '' + echo "Using custom quartz.config.ts..." + cp ${quartzConfig} quartz.config.ts + '' else '' + echo "Using default sandbox-friendly quartz.config.ts..." + cp ${defaultQuartzConfig} quartz.config.ts + ''} + + ${pkgs.lib.optionalString (quartzLayout != null) '' + echo "Using custom quartz.layout.ts..." + cp ${quartzLayout} quartz.layout.ts + ''} + + # Build the static site + export HOME=$(mktemp -d) + echo "Building Quartz site..." + node quartz/bootstrap-cli.mjs build + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mv public $out + + runHook postInstall + ''; + }; + + # Script to serve quartz with converted notes (for development) quartz-serve = pkgs.writeShellScriptBin "quartz-serve" '' set -e NOTES_DIR="''${1:-.}" PORT="''${2:-8080}" WORK_DIR=$(mktemp -d) - echo "Cloning Quartz..." - ${pkgs.git}/bin/git clone --depth 1 https://github.com/jackyzha0/quartz.git "$WORK_DIR/quartz" 2>/dev/null - - echo "Installing dependencies..." - cd "$WORK_DIR/quartz" - ${pkgs.nodejs}/bin/npm install --silent + echo "Setting up Quartz..." + cp -r ${quartz}/lib/quartz/* "$WORK_DIR/" + chmod -R u+w "$WORK_DIR" echo "Converting org notes from $NOTES_DIR..." - ${org-to-quartz}/bin/org-to-quartz "$NOTES_DIR" "$WORK_DIR/quartz/content" -v + rm -rf "$WORK_DIR/content" + mkdir -p "$WORK_DIR/content" + ${org-to-quartz}/bin/org-to-quartz "$NOTES_DIR" "$WORK_DIR/content" -v - # Enable OxHugo plugin - ${pkgs.gnused}/bin/sed -i 's/Plugin.GitHubFlavoredMarkdown()/Plugin.OxHugoFlavouredMarkdown(),\n Plugin.GitHubFlavoredMarkdown()/' quartz.config.ts + cd "$WORK_DIR" echo "" echo "Starting Quartz on http://localhost:$PORT" - ${pkgs.nodejs}/bin/npx quartz build --serve --port "$PORT" + ${pkgs.nodejs}/bin/node quartz/bootstrap-cli.mjs build --serve --port "$PORT" ''; + # Example: build pages from example-notes directory + example-pages = buildQuartzPages { + contentDir = ./example-notes; + name = "example-quartz-pages"; + }; + in { packages = { default = org-to-quartz; org-to-quartz = org-to-quartz; + quartz = quartz; + example-pages = example-pages; + }; + + # Export the buildQuartzPages function for use in other flakes + lib = { + inherit buildQuartzPages; }; devShells.default = pkgs.mkShell { diff --git a/src/org_to_quartz/main.py b/src/org_to_quartz/main.py index a7d0f12..b5682ed 100644 --- a/src/org_to_quartz/main.py +++ b/src/org_to_quartz/main.py @@ -11,18 +11,26 @@ from .citations import CitationResolver def find_org_files(input_dir: Path) -> list[Path]: - """Find all .org files in directory (non-recursive).""" - return list(input_dir.glob("*.org")) + """Find all .org files in directory (recursive).""" + return list(input_dir.rglob("*.org")) def convert_file( org_path: Path, + input_dir: Path, output_dir: Path, citation_resolver: CitationResolver | None, verbose: bool = False, ) -> Path | None: """Convert a single org file to Quartz markdown. + Args: + org_path: Path to the org file + input_dir: Root input directory (for computing relative paths) + output_dir: Root output directory + citation_resolver: Optional citation resolver + verbose: Enable verbose output + Returns: Path to created note directory, or None on error """ @@ -33,8 +41,21 @@ def convert_file( if verbose: print(f" Parsed: {doc.title or org_path.stem}") + # Compute relative path from input dir to preserve directory structure + relative_path = org_path.relative_to(input_dir) + + # Special case: root index.org becomes content/index.md directly + if relative_path.stem == "index" and relative_path.parent == Path("."): + note_dir = output_dir + output_path = output_dir / "index.md" + else: + # Replace .org with directory containing index.md + # e.g., concepts/zettelkasten.org -> concepts/zettelkasten/index.md + note_relative_dir = relative_path.parent / relative_path.stem + note_dir = output_dir / note_relative_dir + output_path = note_dir / "index.md" + # Create output directory for this note - note_dir = output_dir / doc.slug note_dir.mkdir(parents=True, exist_ok=True) # Copy images first (before conversion, to get path mapping) @@ -50,7 +71,6 @@ def convert_file( md_content = update_image_paths(md_content, image_mapping) # Write output - output_path = note_dir / "index.md" output_path.write_text(md_content, encoding="utf-8") return note_dir @@ -123,7 +143,7 @@ def main() -> int: for org_path in org_files: print(f"Converting: {org_path.name}") - result = convert_file(org_path, args.output_dir, citation_resolver, args.verbose) + result = convert_file(org_path, args.input_dir, args.output_dir, citation_resolver, args.verbose) if result: success_count += 1 if args.verbose: diff --git a/src/org_to_quartz/markdown_writer.py b/src/org_to_quartz/markdown_writer.py index 26ae6e4..f74fca0 100644 --- a/src/org_to_quartz/markdown_writer.py +++ b/src/org_to_quartz/markdown_writer.py @@ -57,7 +57,8 @@ def process_wikilinks(content: str) -> str: Pandoc converts org links to markdown links: - [[roam:Title]] -> [roam:Title](roam:Title) -> [[Title]] - [[id:uuid][Desc]] -> [Desc](id:uuid) -> [[Desc]] - - [[file:x.org][Desc]] -> [Desc](file:x.org) -> [[Desc]] + - [[file:x.org][Desc]] -> [Desc](x) (relative link) + - [[file:../dir/x.org][Desc]] -> [Desc](../dir/x) (relative link) """ # [roam:Title](roam:Title) -> [[Title]] content = re.sub(r"\[roam:([^\]]+)\]\(roam:[^)]+\)", r"[[\1]]", content) @@ -65,8 +66,21 @@ def process_wikilinks(content: str) -> str: # [Description](id:uuid) -> [[Description]] content = re.sub(r"\[([^\]]+)\]\(id:[a-f0-9-]+\)", r"[[\1]]", content) - # [Description](file:something.org) -> [[Description]] - content = re.sub(r"\[([^\]]+)\]\(file:[^)]+\.org\)", r"[[\1]]", content) + # [Description](file:path/to/something.org) -> [Description](/path/to/something) + # Convert org file links to proper Quartz paths + def convert_file_link(match): + description = match.group(1) + path = match.group(2) + # Remove file: prefix and .org suffix, convert to Quartz path + # ../concepts/foo.org -> ../concepts/foo + clean_path = re.sub(r"\.org$", "", path) + return f"[{description}]({clean_path})" + + content = re.sub(r"\[([^\]]+)\]\(file:([^)]+\.org)\)", convert_file_link, content) + + # Also handle relative paths without file: prefix (pandoc sometimes drops it) + # [Description](../something.org) -> [Description](../something) + content = re.sub(r"\[([^\]]+)\]\(([^):]+\.org)\)", convert_file_link, content) return content