Compare commits
3 Commits
dependabot
...
ec1426241c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec1426241c | ||
|
|
692f23bc36 | ||
|
|
86eaaae945 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -9,3 +9,7 @@ tsconfig.tsbuildinfo
|
||||
private/
|
||||
.replit
|
||||
replit.nix
|
||||
erl_crash.dump
|
||||
# content/ is generated by the export script; only keep the placeholder
|
||||
content/*
|
||||
!content/.gitkeep
|
||||
|
||||
282
AGENTS.md
Normal file
282
AGENTS.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# AGENTS.md - Coding Agent Instructions
|
||||
|
||||
This document provides essential information for AI coding agents working in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Quartz** is a static site generator for publishing digital gardens and notes as websites.
|
||||
Built with TypeScript, Preact, and unified/remark/rehype for markdown processing.
|
||||
|
||||
| Stack | Technology |
|
||||
| ------------- | ----------------------------------------- |
|
||||
| Language | TypeScript 5.x (strict mode) |
|
||||
| Runtime | Node.js >=22 (v22.16.0 pinned) |
|
||||
| Package Mgr | npm >=10.9.2 |
|
||||
| Module System | ES Modules (`"type": "module"`) |
|
||||
| UI Framework | Preact 10.x (JSX with `react-jsx` pragma) |
|
||||
| Build Tool | esbuild |
|
||||
| Styling | SCSS via esbuild-sass-plugin |
|
||||
|
||||
## Environment
|
||||
|
||||
This is a Nix project. Use the provided `flake.nix` to enter a dev shell with Node.js 22 and npm:
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
All `npm` commands below must be run inside the dev shell.
|
||||
|
||||
## Build, Lint, and Test Commands
|
||||
|
||||
```bash
|
||||
# Type check and format check (CI validation)
|
||||
npm run check
|
||||
|
||||
# Auto-format code with Prettier
|
||||
npm run format
|
||||
|
||||
# Run all tests
|
||||
npm run test
|
||||
|
||||
# Run a single test file
|
||||
npx tsx --test quartz/util/path.test.ts
|
||||
|
||||
# Run tests matching a pattern (use --test-name-pattern)
|
||||
npx tsx --test --test-name-pattern="typeguards" quartz/util/path.test.ts
|
||||
|
||||
# Build the static site
|
||||
npx quartz build
|
||||
|
||||
# Build and serve with hot reload
|
||||
npx quartz build --serve
|
||||
|
||||
# Profile build performance
|
||||
npm run profile
|
||||
```
|
||||
|
||||
### Test Files Location
|
||||
|
||||
Tests use Node.js native test runner via `tsx`. Test files follow the `*.test.ts` pattern:
|
||||
|
||||
- `quartz/util/path.test.ts`
|
||||
- `quartz/util/fileTrie.test.ts`
|
||||
- `quartz/components/scripts/search.test.ts`
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Prettier Configuration (`.prettierrc`)
|
||||
|
||||
```json
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"trailingComma": "all",
|
||||
"quoteProps": "as-needed"
|
||||
}
|
||||
```
|
||||
|
||||
**No ESLint** - only Prettier for formatting. Run `npm run format` before committing.
|
||||
|
||||
### TypeScript Configuration
|
||||
|
||||
- **Strict mode enabled** (`strict: true`)
|
||||
- `noUnusedLocals: true` - no unused variables
|
||||
- `noUnusedParameters: true` - no unused function parameters
|
||||
- JSX configured for Preact (`jsxImportSource: "preact"`)
|
||||
|
||||
### Import Conventions
|
||||
|
||||
```typescript
|
||||
// 1. External packages first
|
||||
import { PluggableList } from "unified"
|
||||
import { visit } from "unist-util-visit"
|
||||
|
||||
// 2. Internal utilities/types (relative paths)
|
||||
import { QuartzTransformerPlugin } from "../types"
|
||||
import { FilePath, slugifyFilePath } from "../../util/path"
|
||||
import { i18n } from "../../i18n"
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
| Element | Convention | Example |
|
||||
| ---------------- | ------------ | ----------------------------------- |
|
||||
| Files (utils) | camelCase | `path.ts`, `fileTrie.ts` |
|
||||
| Files (comps) | PascalCase | `TableOfContents.tsx`, `Search.tsx` |
|
||||
| Types/Interfaces | PascalCase | `QuartzComponent`, `FullSlug` |
|
||||
| Type Guards | `is*` prefix | `isFilePath()`, `isFullSlug()` |
|
||||
| Constants | UPPER_CASE | `QUARTZ`, `UPSTREAM_NAME` |
|
||||
| Options types | `Options` | `interface Options { ... }` |
|
||||
|
||||
### Branded Types Pattern
|
||||
|
||||
This codebase uses branded types for type-safe path handling:
|
||||
|
||||
```typescript
|
||||
type SlugLike<T> = string & { __brand: T }
|
||||
export type FilePath = SlugLike<"filepath">
|
||||
export type FullSlug = SlugLike<"full">
|
||||
export type SimpleSlug = SlugLike<"simple">
|
||||
|
||||
// Always validate with type guards before using
|
||||
export function isFilePath(s: string): s is FilePath { ... }
|
||||
```
|
||||
|
||||
### Component Pattern (Preact)
|
||||
|
||||
Components use a factory function pattern with attached static properties:
|
||||
|
||||
```typescript
|
||||
export default ((userOpts?: Partial<Options>) => {
|
||||
const opts: Options = { ...defaultOptions, ...userOpts }
|
||||
|
||||
const ComponentName: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => {
|
||||
return <div class={classNames(displayClass, "component-name")}>...</div>
|
||||
}
|
||||
|
||||
ComponentName.css = style // SCSS styles
|
||||
ComponentName.afterDOMLoaded = script // Client-side JS
|
||||
return ComponentName
|
||||
}) satisfies QuartzComponentConstructor
|
||||
```
|
||||
|
||||
### Plugin Pattern
|
||||
|
||||
Three plugin types: transformers, filters, and emitters.
|
||||
|
||||
```typescript
|
||||
export const PluginName: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
return {
|
||||
name: "PluginName",
|
||||
markdownPlugins(ctx) { return [...] },
|
||||
htmlPlugins(ctx) { return [...] },
|
||||
externalResources(ctx) { return { js: [], css: [] } },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Pattern
|
||||
|
||||
Use Node.js native test runner with `assert`:
|
||||
|
||||
```typescript
|
||||
import test, { describe, beforeEach } from "node:test"
|
||||
import assert from "node:assert"
|
||||
|
||||
describe("FeatureName", () => {
|
||||
test("should do something", () => {
|
||||
assert.strictEqual(actual, expected)
|
||||
assert.deepStrictEqual(actualObj, expectedObj)
|
||||
assert(condition) // truthy assertion
|
||||
assert(!condition) // falsy assertion
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use `try/catch` for critical operations (file I/O, parsing)
|
||||
- Custom `trace` utility for error reporting with stack traces
|
||||
- `process.exit(1)` for fatal errors
|
||||
- `console.warn()` for non-fatal issues
|
||||
|
||||
### Async Patterns
|
||||
|
||||
- Prefer `async/await` over raw promises
|
||||
- Use async generators (`async *emit()`) for streaming file output
|
||||
- Use `async-mutex` for concurrent build protection
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
quartz/
|
||||
├── bootstrap-cli.mjs # CLI entry point
|
||||
├── build.ts # Build orchestration
|
||||
├── cfg.ts # Configuration types
|
||||
├── components/ # Preact UI components
|
||||
│ ├── *.tsx # Components
|
||||
│ ├── scripts/ # Client-side scripts (*.inline.ts)
|
||||
│ └── styles/ # Component SCSS
|
||||
├── plugins/
|
||||
│ ├── transformers/ # Markdown AST transformers
|
||||
│ ├── filters/ # Content filters
|
||||
│ ├── emitters/ # Output generators
|
||||
│ └── types.ts # Plugin type definitions
|
||||
├── processors/ # Build pipeline (parse/filter/emit)
|
||||
├── util/ # Utility functions
|
||||
└── i18n/ # Internationalization (30+ locales)
|
||||
```
|
||||
|
||||
## Branch Workflow
|
||||
|
||||
This is a fork of [jackyzha0/quartz](https://github.com/jackyzha0/quartz) with org-roam customizations.
|
||||
|
||||
| Branch | Purpose |
|
||||
| ----------- | ------------------------------------------------ |
|
||||
| `main` | Clean mirror of upstream quartz — no custom code |
|
||||
| `org-roam` | Default branch — all customizations live here |
|
||||
| `feature/*` | Short-lived branches off `org-roam` |
|
||||
|
||||
### Pulling Upstream Updates
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git fetch upstream
|
||||
git merge upstream/main
|
||||
git checkout org-roam
|
||||
git merge main
|
||||
# Resolve conflicts if any, then commit
|
||||
```
|
||||
|
||||
### Working on Features
|
||||
|
||||
```bash
|
||||
git checkout org-roam
|
||||
git checkout -b feature/my-feature
|
||||
# ... work ...
|
||||
git checkout org-roam
|
||||
git merge feature/my-feature
|
||||
git branch -d feature/my-feature
|
||||
```
|
||||
|
||||
**Merge direction:** `upstream → main → org-roam → feature/*`
|
||||
|
||||
## Org-Roam Workflow
|
||||
|
||||
Notes live in a **separate directory** outside this repo. The export script
|
||||
converts them to Markdown via ox-hugo, then Quartz builds the site.
|
||||
|
||||
### Tooling
|
||||
|
||||
The dev shell (`nix develop`) provides:
|
||||
|
||||
- `nodejs_22` — Quartz build
|
||||
- `elixir` — runs the export script
|
||||
- `emacs` + `ox-hugo` — performs the org → markdown conversion
|
||||
|
||||
### Export and build
|
||||
|
||||
```bash
|
||||
# Export only (wipes content/, exports all .org files)
|
||||
NOTES_DIR=/path/to/notes npm run export
|
||||
|
||||
# Export then build the site
|
||||
NOTES_DIR=/path/to/notes npm run build:notes
|
||||
|
||||
# Positional arg also works
|
||||
elixir scripts/export.exs /path/to/notes
|
||||
```
|
||||
|
||||
The export script (`scripts/export.exs`) runs Emacs in batch mode, calling
|
||||
`org-hugo-export-to-md` per file (per-file mode, not per-subtree). It uses
|
||||
YAML frontmatter (ox-hugo default). `content/` is wiped before each export.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Client-side scripts**: Use `.inline.ts` suffix, bundled via esbuild
|
||||
- **Isomorphic code**: `quartz/util/path.ts` must not use Node.js APIs
|
||||
- **Incremental builds**: Plugins can implement `partialEmit` for efficiency
|
||||
- **Markdown flavors**: Supports Obsidian (`ofm.ts`) and Roam (`roam.ts`) syntax
|
||||
64
README.md
64
README.md
@@ -1,13 +1,71 @@
|
||||
# Quartz v4
|
||||
# Quartz v4 — org-roam edition
|
||||
|
||||
> “[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important.” — Richard Hamming
|
||||
> "[One] who works with the door open gets all kinds of interruptions, but [they] also occasionally gets clues as to what the world is and what might be important." — Richard Hamming
|
||||
|
||||
Quartz is a set of tools that helps you publish your [digital garden](https://jzhao.xyz/posts/networked-thought) and notes as a website for free.
|
||||
|
||||
🔗 Read the documentation and get started: https://quartz.jzhao.xyz/
|
||||
This fork adds first-class support for [org-roam](https://www.orgroam.com/) notes via [ox-hugo](https://ox-hugo.scripter.co/).
|
||||
|
||||
🔗 Upstream documentation: https://quartz.jzhao.xyz/
|
||||
|
||||
[Join the Discord Community](https://discord.gg/cRFFHYye7t)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
This project uses Nix. Enter the development shell, which provides Node.js 22, Elixir, and Emacs with ox-hugo:
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
All commands below must be run inside this shell.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Building from org-roam notes
|
||||
|
||||
Your org-roam notes live in a separate directory. Point `NOTES_DIR` at it:
|
||||
|
||||
```bash
|
||||
# Export notes to content/ and build the site
|
||||
NOTES_DIR=/path/to/notes npm run build:notes
|
||||
|
||||
# Export, build, and serve with hot reload
|
||||
NOTES_DIR=/path/to/notes npm run serve:notes
|
||||
|
||||
# Export only (wipes content/ and re-exports all .org files)
|
||||
NOTES_DIR=/path/to/notes npm run export
|
||||
```
|
||||
|
||||
The export script mirrors the subdirectory structure of your notes into `content/`
|
||||
and generates a fallback `index.md` if your notes don't include one.
|
||||
|
||||
### Building without org-roam notes
|
||||
|
||||
If you manage `content/` directly with Markdown files:
|
||||
|
||||
```bash
|
||||
# Build the site
|
||||
npx quartz build
|
||||
|
||||
# Build and serve with hot reload
|
||||
npx quartz build --serve
|
||||
```
|
||||
|
||||
The site is generated in `public/`. When serving, visit http://localhost:8080.
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
npm run check # type check + format check
|
||||
npm run format # auto-format with Prettier
|
||||
npm run test # run tests
|
||||
```
|
||||
|
||||
## Sponsors
|
||||
|
||||
<p align="center">
|
||||
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771008912,
|
||||
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
30
flake.nix
Normal file
30
flake.nix
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
description = "Quartz org-roam dev shell";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
pkgs.nodejs_22
|
||||
pkgs.elixir
|
||||
((pkgs.emacsPackagesFor pkgs.emacs-nox).emacsWithPackages (epkgs: [ epkgs.ox-hugo ]))
|
||||
pkgs.mcp-nixos
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "Node $(node --version) / npm $(npm --version)"
|
||||
elixir --version 2>/dev/null | head -1 || true
|
||||
echo "Emacs $(emacs --version | head -1)"
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
16
notes/bus/emt-madrid.org
Normal file
16
notes/bus/emt-madrid.org
Normal file
@@ -0,0 +1,16 @@
|
||||
:PROPERTIES:
|
||||
:ID: emt-madrid
|
||||
:END:
|
||||
#+title: EMT Madrid (urban bus)
|
||||
|
||||
Empresa Municipal de Transportes (EMT) operates the urban bus network
|
||||
within the municipality of Madrid — around 200 lines.
|
||||
|
||||
* Notable lines
|
||||
- *Line 27* — connects Embajadores with Barrio de la Concepción, one of the
|
||||
oldest routes in the network.
|
||||
- *Line 34* — Argüelles to Carabanchel, crossing the city centre via Gran Vía.
|
||||
- *Búho (owl) lines* — night buses running from Cibeles from midnight to 6 am.
|
||||
|
||||
* See also
|
||||
- [[id:madrid-transport][Madrid Public Transport]]
|
||||
17
notes/madrid-transport.org
Normal file
17
notes/madrid-transport.org
Normal file
@@ -0,0 +1,17 @@
|
||||
:PROPERTIES:
|
||||
:ID: madrid-transport
|
||||
:END:
|
||||
#+title: Madrid Public Transport
|
||||
|
||||
Madrid has one of the most extensive public transport networks in Europe,
|
||||
operated primarily by [[id:crtm][Consorcio Regional de Transportes de Madrid]] (CRTM).
|
||||
|
||||
* Modes
|
||||
- [[id:metro-madrid][Metro de Madrid]] — 13 lines, ~300 km of track
|
||||
- [[id:emt-madrid][EMT Bus]] — urban buses within the city
|
||||
- Cercanías — suburban rail run by Renfe
|
||||
- Interurbano — regional buses to the wider Community of Madrid
|
||||
|
||||
* Ticketing
|
||||
A single [[https://www.crtm.es][tarjeta transporte]] (transport card) works across all modes.
|
||||
The Multi card covers zones A–C and is topped up at any metro station.
|
||||
18
notes/metro/metro-madrid.org
Normal file
18
notes/metro/metro-madrid.org
Normal file
@@ -0,0 +1,18 @@
|
||||
:PROPERTIES:
|
||||
:ID: metro-madrid
|
||||
:END:
|
||||
#+title: Metro de Madrid
|
||||
|
||||
The Madrid Metro is the main rapid transit network in the city, opened in 1919.
|
||||
It is the second oldest metro in the Iberian Peninsula after Barcelona.
|
||||
|
||||
* Key Lines
|
||||
| Line | Name | Colour | Terminals |
|
||||
|------+-----------------+--------+------------------------------|
|
||||
| L1 | Pinar de Chamartín–Valdecarros | Blue | Pinar de Chamartín / Valdecarros |
|
||||
| L6 | Circular | Grey | Circular (loop) |
|
||||
| L10 | — | Dark blue | Hospital Infanta Sofía / Tres Olivos |
|
||||
|
||||
* See also
|
||||
- [[id:madrid-transport][Madrid Public Transport]]
|
||||
- [[id:sol-interchange][Sol interchange]]
|
||||
12
notes/metro/sol-interchange.org
Normal file
12
notes/metro/sol-interchange.org
Normal file
@@ -0,0 +1,12 @@
|
||||
:PROPERTIES:
|
||||
:ID: sol-interchange
|
||||
:END:
|
||||
#+title: Sol (interchange)
|
||||
|
||||
Sol is the busiest interchange station in the Madrid Metro, sitting beneath
|
||||
Puerta del Sol in the city centre.
|
||||
|
||||
Lines serving Sol: [[id:metro-madrid][L1]], L2, L3.
|
||||
|
||||
It also connects to the Cercanías hub underneath, making it the de-facto
|
||||
zero point of Madrid's public transport.
|
||||
22
notes/roads/crtm.org
Normal file
22
notes/roads/crtm.org
Normal file
@@ -0,0 +1,22 @@
|
||||
:PROPERTIES:
|
||||
:ID: crtm
|
||||
:END:
|
||||
#+title: CRTM — Consorcio Regional de Transportes de Madrid
|
||||
|
||||
The CRTM is the regional authority that coordinates public transport across
|
||||
the Community of Madrid. It does not operate services directly but sets
|
||||
fares, zones, and integration policy.
|
||||
|
||||
* Fare zones
|
||||
| Zone | Coverage |
|
||||
|-------+-----------------------------|
|
||||
| A | Municipality of Madrid |
|
||||
| B1 | Inner ring municipalities |
|
||||
| B2 | Outer ring municipalities |
|
||||
| B3 | Further suburban area |
|
||||
| C1–C2 | Commuter belt |
|
||||
|
||||
* Related
|
||||
- [[id:madrid-transport][Madrid Public Transport]]
|
||||
- [[id:metro-madrid][Metro de Madrid]]
|
||||
- [[id:emt-madrid][EMT Madrid]]
|
||||
19
notes/roads/m30.org
Normal file
19
notes/roads/m30.org
Normal file
@@ -0,0 +1,19 @@
|
||||
:PROPERTIES:
|
||||
:ID: m30
|
||||
:END:
|
||||
#+title: M-30
|
||||
|
||||
The M-30 is Madrid's innermost ring road, circling the city centre at a
|
||||
radius of roughly 3–5 km from Puerta del Sol.
|
||||
|
||||
It runs mostly underground through the Madrid Río tunnel section along the
|
||||
Manzanares river, built during the 2004–2007 renovation that reclaimed the
|
||||
riverbank as a public park.
|
||||
|
||||
* Key junctions
|
||||
- Nudo Norte — connects to A-1 (Burgos) and A-6 (La Coruña)
|
||||
- Nudo Sur — connects to A-4 (Cádiz) and A-42 (Toledo)
|
||||
|
||||
* See also
|
||||
- [[id:crtm][CRTM]]
|
||||
- [[id:madrid-transport][Madrid Public Transport]]
|
||||
10
opencode.json
Normal file
10
opencode.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"nixos": {
|
||||
"type": "local",
|
||||
"command": ["mcp-nixos"],
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,10 @@
|
||||
"check": "tsc --noEmit && npx prettier . --check",
|
||||
"format": "npx prettier . --write",
|
||||
"test": "tsx --test",
|
||||
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
|
||||
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1",
|
||||
"export": "elixir scripts/export.exs",
|
||||
"build:notes": "elixir scripts/export.exs && npx quartz build",
|
||||
"serve:notes": "elixir scripts/export.exs && npx quartz build --serve"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=10.9.2",
|
||||
|
||||
@@ -55,7 +55,7 @@ const config: QuartzConfig = {
|
||||
},
|
||||
plugins: {
|
||||
transformers: [
|
||||
Plugin.FrontMatter(),
|
||||
Plugin.FrontMatter({ delimiters: "+++", language: "toml" }),
|
||||
Plugin.CreatedModifiedDate({
|
||||
priority: ["frontmatter", "git", "filesystem"],
|
||||
}),
|
||||
@@ -66,7 +66,11 @@ const config: QuartzConfig = {
|
||||
},
|
||||
keepBackground: false,
|
||||
}),
|
||||
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
||||
// OxHugoFlavouredMarkdown must come before GitHubFlavoredMarkdown.
|
||||
// Note: not compatible with ObsidianFlavoredMarkdown — use one or the other.
|
||||
// If ox-hugo exports TOML frontmatter, change FrontMatter to:
|
||||
// Plugin.FrontMatter({ delims: "+++", language: "toml" })
|
||||
Plugin.OxHugoFlavouredMarkdown(),
|
||||
Plugin.GitHubFlavoredMarkdown(),
|
||||
Plugin.TableOfContents(),
|
||||
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||
|
||||
136
scripts/export.exs
Normal file
136
scripts/export.exs
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env elixir
|
||||
# Export org-roam notes (per-file) to content/ via ox-hugo.
|
||||
#
|
||||
# Usage:
|
||||
# NOTES_DIR=~/notes elixir scripts/export.exs
|
||||
# elixir scripts/export.exs /path/to/notes
|
||||
#
|
||||
# The positional argument takes precedence over the NOTES_DIR env var.
|
||||
|
||||
notes_dir =
|
||||
case System.argv() do
|
||||
[dir | _] -> dir
|
||||
[] ->
|
||||
System.get_env("NOTES_DIR") ||
|
||||
(IO.puts(:stderr, "Usage: NOTES_DIR=/path/to/notes elixir scripts/export.exs"); System.halt(1))
|
||||
end
|
||||
|
||||
notes_dir = Path.expand(notes_dir)
|
||||
repo_root = __DIR__ |> Path.join("..") |> Path.expand()
|
||||
content_dir = Path.join(repo_root, "content")
|
||||
|
||||
unless File.dir?(notes_dir) do
|
||||
IO.puts(:stderr, "Error: notes directory does not exist: #{notes_dir}")
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
# Wipe content/, preserving .gitkeep
|
||||
IO.puts("==> Wiping #{content_dir}")
|
||||
|
||||
content_dir
|
||||
|> File.ls!()
|
||||
|> Enum.reject(&(&1 == ".gitkeep"))
|
||||
|> Enum.each(fn entry ->
|
||||
Path.join(content_dir, entry) |> File.rm_rf!()
|
||||
end)
|
||||
|
||||
# Collect all .org files
|
||||
IO.puts("==> Exporting org files from #{notes_dir}")
|
||||
|
||||
org_files =
|
||||
Path.join(notes_dir, "**/*.org")
|
||||
|> Path.wildcard()
|
||||
|
||||
if org_files == [] do
|
||||
IO.puts("No .org files found in #{notes_dir}")
|
||||
System.halt(0)
|
||||
end
|
||||
|
||||
# Export each file via emacs --batch
|
||||
results =
|
||||
Enum.map(org_files, fn orgfile ->
|
||||
IO.puts(" exporting: #{orgfile}")
|
||||
|
||||
# Mirror the notes subdirectory structure under content/
|
||||
section =
|
||||
orgfile
|
||||
|> Path.dirname()
|
||||
|> Path.relative_to(notes_dir)
|
||||
|
||||
{output, exit_code} =
|
||||
System.cmd(
|
||||
"emacs",
|
||||
[
|
||||
"--batch",
|
||||
"--eval", "(require 'ox-hugo)",
|
||||
"--eval", ~s[(setq org-hugo-base-dir "#{repo_root}")],
|
||||
"--eval", ~s[(setq org-hugo-default-section-directory "#{section}")],
|
||||
"--visit", orgfile,
|
||||
"--funcall", "org-hugo-export-to-md"
|
||||
],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
# Filter noisy emacs startup lines, same as the shell script
|
||||
filtered =
|
||||
output
|
||||
|> String.split("\n")
|
||||
|> Enum.reject(&String.match?(&1, ~r/^Loading|^ad-handle|^For information/))
|
||||
|> Enum.join("\n")
|
||||
|
||||
if filtered != "", do: IO.puts(filtered)
|
||||
|
||||
{orgfile, exit_code}
|
||||
end)
|
||||
|
||||
failures = Enum.filter(results, fn {_, code} -> code != 0 end)
|
||||
|
||||
if failures != [] do
|
||||
IO.puts(:stderr, "\nFailed to export #{length(failures)} file(s):")
|
||||
Enum.each(failures, fn {f, code} -> IO.puts(:stderr, " [exit #{code}] #{f}") end)
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
md_count =
|
||||
Path.join(content_dir, "**/*.md")
|
||||
|> Path.wildcard()
|
||||
|> length()
|
||||
|
||||
# Generate a default index.md if none was exported
|
||||
index_path = Path.join(content_dir, "index.md")
|
||||
|
||||
unless File.exists?(index_path) do
|
||||
IO.puts("==> Generating default index.md")
|
||||
|
||||
pages =
|
||||
Path.join(content_dir, "**/*.md")
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(fn path ->
|
||||
slug = Path.relative_to(path, content_dir) |> Path.rootname()
|
||||
|
||||
title =
|
||||
path
|
||||
|> File.read!()
|
||||
|> then(fn content ->
|
||||
case Regex.run(~r/^title\s*=\s*"(.+)"/m, content) do
|
||||
[_, t] -> t
|
||||
_ -> slug
|
||||
end
|
||||
end)
|
||||
|
||||
{slug, title}
|
||||
end)
|
||||
|> Enum.sort_by(fn {_, title} -> title end)
|
||||
|> Enum.map(fn {slug, title} -> "- [#{title}](#{slug})" end)
|
||||
|> Enum.join("\n")
|
||||
|
||||
File.write!(index_path, """
|
||||
---
|
||||
title: Index
|
||||
---
|
||||
|
||||
#{pages}
|
||||
""")
|
||||
end
|
||||
|
||||
IO.puts("==> Done. #{md_count} markdown files in #{content_dir}")
|
||||
Reference in New Issue
Block a user