diff --git a/SPEC.md b/SPEC.md index 439edad..64ec47b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -72,15 +72,15 @@ References to definitions can be: #### Semicolons -- **Records** do NOT have semicolons after the closing brace `}` -- All other definitions require semicolons: - - `use` statements end with `;` - - `token` definitions end with `;` - - `inline type` definitions end with `;` - - `def type` definitions end with `;` - - `query` definitions end with `;` - - `procedure` definitions end with `;` - - `subscription` definitions end with `;` +All definitions require semicolons: +- `record` definitions end with `};` +- `use` statements end with `;` +- `token` definitions end with `;` +- `inline type` definitions end with `;` +- `def type` definitions end with `;` +- `query` definitions end with `;` +- `procedure` definitions end with `;` +- `subscription` definitions end with `;` #### Commas diff --git a/website/content/docs/_index.md b/website/content/docs/_index.md index 3a03f97..98cf817 100644 --- a/website/content/docs/_index.md +++ b/website/content/docs/_index.md @@ -3,6 +3,7 @@ title = "Documentation" description = "Complete guide to MLF" sort_by = "weight" template = "section.html" +redirect_to = "/docs/getting-started/" +++ MLF (Matt's Lexicon Format) is a human-friendly DSL for writing ATProto Lexicons. This documentation will help you learn the language, use the CLI tools, and integrate MLF into your projects. diff --git a/website/content/docs/language-guide/01-your-first-lexicon.md b/website/content/docs/language-guide/01-your-first-lexicon.md new file mode 100644 index 0000000..7cb774a --- /dev/null +++ b/website/content/docs/language-guide/01-your-first-lexicon.md @@ -0,0 +1,131 @@ ++++ +title = "Your First Lexicon" +weight = 1 ++++ + +Welcome to MLF! Let's create your first lexicon by defining a simple record. + +## A Basic Record + +Here's a complete MLF file that defines a user profile: + +```mlf +/// A user profile +record profile { + /// The user's display name + name: string, + /// The user's email address + email: string, + /// When the account was created + createdAt: Datetime, +} +``` + +This defines a `profile` record with three fields: `name`, `email`, and `createdAt`. + +## File Naming and Namespaces + +The file path determines the lexicon namespace. If you save this as: + +``` +com/example/forum/profile.mlf +``` + +Then the namespace will be `com.example.forum.profile`, and the full identifier for this record is `com.example.forum.profile`. + +The namespace comes from the file path, not from any declaration in the file. + +## What Gets Generated + +When you compile this MLF file, it generates a JSON lexicon: + +```json +{ + "lexicon": 1, + "id": "com.example.forum.profile", + "defs": { + "main": { + "type": "record", + "description": "A user profile", + "key": "tid", + "record": { + "type": "object", + "required": ["name", "email", "createdAt"], + "properties": { + "name": { + "type": "string", + "description": "The user's display name" + }, + "email": { + "type": "string", + "description": "The user's email address" + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "When the account was created" + } + } + } + } + } +} +``` + +The MLF syntax is much cleaner and easier to read! + +## Comments + +MLF supports three types of comments: + +**Documentation comments** (`///`) appear in the generated lexicon: +```mlf +/// This comment appears in generated docs +record example { + /// This field comment also appears + field: string, +} +``` + +**Regular comments** (`//`) are for internal notes only: +```mlf +// This is a note to yourself, won't appear in output +record example { + field: string, // Inline comments work too +} +``` + +**Hash comments** (`#`) at the start of a file are ignored (useful for shebangs): +```mlf +#!/usr/bin/env mlf +# This line is ignored + +record example { + field: string, +} +``` + +## Complete Example + +Here's a minimal, complete lexicon for a forum post: + +**File: `com/example/forum/post.mlf`** +```mlf +/// A forum post +record post { + /// Post title + title: string, + /// Post content + body: string, + /// Post author's DID + author: Did, + /// When the post was published + publishedAt: Datetime, +} +``` + +This creates the lexicon `com.example.forum.post` with a single record definition. + +## What's Next? + +Now that you understand the basics, let's learn about fields in more detail. diff --git a/website/content/docs/language-guide/02-fields.md b/website/content/docs/language-guide/02-fields.md new file mode 100644 index 0000000..ea7f1f0 --- /dev/null +++ b/website/content/docs/language-guide/02-fields.md @@ -0,0 +1,198 @@ ++++ +title = "Fields" +weight = 2 ++++ + +Fields are the building blocks of records. Let's explore the different types of fields you can define. + +## Required vs Optional Fields + +By default, all fields are required. Use `?` to make a field optional: + +```mlf +record user { + name: string, // Required - must be provided + bio?: string, // Optional - can be omitted + email: string, // Required + website?: string, // Optional +} +``` + +## Primitive Types + +MLF supports several primitive types: + +**Strings:** +```mlf +record example { + name: string, +} +``` + +**Integers** (64-bit signed): +```mlf +record example { + count: integer, + age: integer, +} +``` + +**Numbers** (double-precision floats): +```mlf +record example { + price: number, + rating: number, +} +``` + +**Booleans:** +```mlf +record example { + isActive: boolean, + verified: boolean, +} +``` + +**Binary data:** +```mlf +record example { + data: bytes, // Raw byte array + image: blob, // Binary with metadata (MIME type, size) +} +``` + +**Unknown** (for forward compatibility): +```mlf +record example { + metadata: unknown, // Can be any value +} +``` + +**Null:** +```mlf +record example { + nothing: null, // Always null (rarely used) +} +``` + +## Special Format Types + +MLF provides built-in types for common formats: + +```mlf +record post { + author: Did, // Decentralized Identifier (did:*) + uri: AtUri, // AT Protocol URI (at://...) + timestamp: Datetime, // ISO 8601 datetime + website: Uri, // Generic URI + contentHash: Cid, // Content Identifier + handle: Handle, // Handle (domain name) + language: Language, // BCP 47 language code +} +``` + +These are actually inline type aliases defined in the prelude, but you can use them as if they were primitive types. + +## Objects + +Define inline object types with curly braces: + +```mlf +record post { + author: { + did: Did, + handle: Handle, + name: string, + }, +} +``` + +Objects can be nested: + +```mlf +record profile { + location: { + city: string, + coordinates: { + lat: number, + lng: number, + }, + }, +} +``` + +## Arrays + +Add `[]` after any type to make it an array: + +```mlf +record post { + tags: string[], // Array of strings + images: Uri[], // Array of URIs + counts: integer[], // Array of integers +} +``` + +Arrays of objects: + +```mlf +record post { + authors: { + did: Did, + role: string, + }[], +} +``` + +Nested arrays: + +```mlf +record matrix { + grid: integer[][], // Array of arrays +} +``` + +## Complete Example + +Here's a complete record showing all field types: + +**File: `com/example/forum/post.mlf`** +```mlf +/// A forum post +record post { + /// Post text content + text: string, + + /// Post author + author: Did, + + /// When the post was created + createdAt: Datetime, + + /// Optional reply count + replyCount?: integer, + + /// Whether the post is pinned + isPinned: boolean, + + /// Optional geographic location + location?: { + name: string, + lat: number, + lng: number, + }, + + /// Tags on this post + tags: string[], + + /// Optional embedded images + images?: Uri[], + + /// Arbitrary metadata + metadata: unknown, +} +``` + +## What's Next? + +Now that you understand fields, let's learn how to add validation rules with constraints. diff --git a/website/content/docs/language-guide/03-constraints.md b/website/content/docs/language-guide/03-constraints.md new file mode 100644 index 0000000..e8e13ad --- /dev/null +++ b/website/content/docs/language-guide/03-constraints.md @@ -0,0 +1,256 @@ ++++ +title = "Constraints" +weight = 3 ++++ + +Constraints add validation rules to your fields, ensuring data meets specific requirements. + +## String Constraints + +Strings support several validation options: + +**Length constraints** (in bytes): +```mlf +title: string constrained { + minLength: 1, + maxLength: 200, +} +``` + +**Grapheme constraints** (user-perceived characters): +```mlf +text: string constrained { + minGraphemes: 1, + maxGraphemes: 500, +} +``` + +Use `maxGraphemes` for user-visible content (handles emojis correctly), and `maxLength` for technical limits. + +**Enum** (closed set - only these values allowed): +```mlf +status: string constrained { + enum: ["draft", "published", "archived"], +} +``` + +**Known values** (open set - these values are documented, but others are allowed): +```mlf +postType: string constrained { + knownValues: ["text", "image", "video"], +} +``` + +**Default value:** +```mlf +visibility: string constrained { + enum: ["public", "private"], + default: "public", +} +``` + +**Format validation:** +```mlf +email: string constrained { + format: "email", +} +``` + +**All string constraints together:** +```mlf +username: string constrained { + minLength: 3, + maxLength: 20, + minGraphemes: 3, + maxGraphemes: 20, +} +``` + +## Integer Constraints + +Integers can have numeric ranges and specific values: + +**Range constraints:** +```mlf +age: integer constrained { + minimum: 0, + maximum: 150, +} +``` + +**Enum values:** +```mlf +priority: integer constrained { + enum: [1, 2, 3, 4, 5], +} +``` + +**Default value:** +```mlf +count: integer constrained { + minimum: 0, + default: 0, +} +``` + +## Number Constraints + +Numbers (floats) work the same as integers: + +```mlf +rating: number constrained { + minimum: 0.0, + maximum: 5.0, +} + +price: number constrained { + minimum: 0.0, + default: 0.0, +} +``` + +## Boolean Constraints + +Booleans only support default values: + +```mlf +isActive: boolean constrained { + default: true, +} +``` + +## Array Constraints + +Arrays can be constrained by length: + +```mlf +tags: string[] constrained { + minLength: 1, + maxLength: 10, +} + +images: Uri[] constrained { + maxLength: 4, +} +``` + +## Blob Constraints + +Blobs support MIME type and size constraints: + +```mlf +avatar: blob constrained { + accept: ["image/png", "image/jpeg", "image/webp"], + maxSize: 1000000, // 1MB in bytes +} + +video: blob constrained { + accept: ["video/mp4"], + maxSize: 50000000, // 50MB +} +``` + +## Constraint Refinement + +You can apply constraints multiple times, but they must always become **more restrictive**: + +```mlf +record post { + // First constraint: max 500 characters + text: string constrained { + maxGraphemes: 500, + }, +} + +record shortPost { + // Further constrain to max 100 characters - valid! + text: string constrained { + maxGraphemes: 500, + } constrained { + maxGraphemes: 100, + }, +} +``` + +**Refinement rules:** +- `minimum` can only increase +- `maximum` can only decrease +- `minLength`/`minGraphemes` can only increase +- `maxLength`/`maxGraphemes` can only decrease +- `enum` can only restrict to a subset +- `format` cannot change once set + +**Invalid refinement:** +```mlf +// ERROR: Can't increase maximum +text: string constrained { + maxLength: 100, +} constrained { + maxLength: 200, // Invalid! Going from 100 to 200 +} +``` + +## Complete Example + +Here's a complete record demonstrating various constraints: + +**File: `com/example/forum/thread.mlf`** +```mlf +/// A forum thread +record thread { + /// Thread title (1-200 characters) + title: string constrained { + minGraphemes: 1, + maxGraphemes: 200, + }, + + /// Thread content + body: string constrained { + maxGraphemes: 5000, + }, + + /// View count (must be non-negative) + views: integer constrained { + minimum: 0, + }, + + /// Reply count + replies: integer constrained { + minimum: 0, + default: 0, + }, + + /// Thread status + status: string constrained { + enum: ["open", "closed", "pinned"], + default: "open", + }, + + /// Thread images (1-10 images) + images: Uri[] constrained { + minLength: 1, + maxLength: 10, + }, + + /// Optional thread thumbnail + thumbnail?: blob constrained { + accept: ["image/png", "image/jpeg"], + maxSize: 500000, + }, + + /// Average rating (0-5 stars) + rating?: number constrained { + minimum: 0.0, + maximum: 5.0, + }, + + /// Whether thread is featured + featured: boolean constrained { + default: false, + }, +} +``` + +## What's Next? + +Now that you can validate your data, let's learn how to create reusable type definitions. diff --git a/website/content/docs/language-guide/04-custom-types.md b/website/content/docs/language-guide/04-custom-types.md new file mode 100644 index 0000000..6ded98d --- /dev/null +++ b/website/content/docs/language-guide/04-custom-types.md @@ -0,0 +1,197 @@ ++++ +title = "Custom Types" +weight = 4 ++++ + +As you build lexicons, you'll find yourself repeating the same constraints and object shapes. Custom types help you avoid duplication. + +## Inline Types + +Inline types are like macros or type aliases - they expand at the point of use and never appear in the generated lexicon. + +**Without inline types:** +```mlf +record user { + name: string constrained { + minGraphemes: 1, + maxGraphemes: 100, + }, + displayName: string constrained { + minGraphemes: 1, + maxGraphemes: 100, + }, +} +``` + +**With inline types:** +```mlf +inline type ShortText = string constrained { + minGraphemes: 1, + maxGraphemes: 100, +}; + +record user { + name: ShortText, + displayName: ShortText, +} +``` + +When compiled, `ShortText` is replaced with the full constraint definition. It's purely for convenience. + +**More examples:** +```mlf +inline type PositiveInt = integer constrained { + minimum: 0, +}; + +inline type EmailAddress = string constrained { + format: "email", + maxLength: 254, +}; + +inline type UserId = Did; // Simple alias + +record account { + id: UserId, + email: EmailAddress, + loginCount: PositiveInt, +} +``` + +Inline types can define objects too: + +```mlf +inline type Coordinates = { + lat: number, + lng: number, +}; + +record location { + coords: Coordinates, +} +``` + +## Def Types + +When you want a type to be **shared and referenced by name** in the generated lexicon, use `def type`: + +```mlf +def type author = { + did: Did, + handle: Handle, + displayName?: string, +}; + +record post { + author: author, +} + +record comment { + author: author, +} +``` + +In the generated lexicon, `author` appears as a named definition that both `post` and `comment` reference. + +**Key difference:** +- `inline type` - expands inline, doesn't appear in output +- `def type` - becomes a named definition, referenced by name + +## When to Use Each + +**Use inline types for:** +- Type aliases (`inline type UserId = Did;`) +- Reusable constraint patterns +- Types that should be transparent in the output +- Simple wrappers + +**Use def types for:** +- Complex objects used multiple times +- Types that form part of your API contract +- Types you want to reference from other files +- Types that should have their own documentation + +## Example Comparison + +**Inline type (expands everywhere):** +```mlf +inline type ShortString = string constrained { + maxGraphemes: 100, +}; + +record post { + title: ShortString, // Expands to: string constrained { maxGraphemes: 100 } +} +``` + +**Def type (referenced by name):** +```mlf +def type postRef = { + uri: AtUri, + cid: Cid, +}; + +record reply { + replyTo: postRef, // References: #postRef in lexicon +} +``` + +## Complete Example + +Here's a complete file showing both types: + +**File: `com/example/forum/thread.mlf`** +```mlf +// Inline types for common patterns +inline type ShortText = string constrained { + minGraphemes: 1, + maxGraphemes: 200, +}; + +inline type LongText = string constrained { + maxGraphemes: 50000, +}; + +// Def type for shared object +def type author = { + did: Did, + handle: Handle, + displayName?: ShortText, +}; + +/// A forum thread +record thread { + /// Thread title + title: ShortText, + + /// Thread body + body: LongText, + + /// Thread author + author: author, + + /// When created + createdAt: Datetime, +} + +/// A reply to a thread +record reply { + /// Reply text + text: LongText, + + /// Reply author (reuses author type) + author: author, + + /// When created + createdAt: Datetime, +} +``` + +In the generated lexicon: +- `ShortText` and `LongText` don't appear - they're expanded inline +- `author` appears as a named definition in the `defs` block +- Both `thread` and `reply` reference `#author` + +## What's Next? + +Now that you can create reusable types, let's learn about unions for accepting multiple types. diff --git a/website/content/docs/language-guide/05-unions.md b/website/content/docs/language-guide/05-unions.md new file mode 100644 index 0000000..2fbd6e1 --- /dev/null +++ b/website/content/docs/language-guide/05-unions.md @@ -0,0 +1,168 @@ ++++ +title = "Unions" +weight = 5 ++++ + +Unions allow a field to accept multiple types. MLF supports both closed unions (fixed set of types) and open unions (allowing unknown types). + +## Closed Unions + +Use the pipe operator `|` to create a union of types: + +```mlf +def type textPost = { + text: string, +}; + +def type imagePost = { + image: Uri, + caption?: string, +}; + +def type videoPost = { + video: Uri, + duration: integer, +}; + +record post { + content: textPost | imagePost | videoPost, +} +``` + +The `content` field must be one of these three types. No other types are accepted. + +## Open Unions + +Add `| _` to allow unknown types for forward compatibility: + +```mlf +record post { + content: textPost | imagePost | _, +} +``` + +Now the system knows about `textPost` and `imagePost`, but will also accept unknown types it hasn't seen before. This is useful when you expect the union to grow in the future. + +## Why Use Open Unions? + +Open unions help with forward compatibility: + +```mlf +// Version 1: Only text and images +record post { + content: textPost | imagePost | _, +} + +// Version 2: Add video support +// Old clients still work because of the `_` +def type videoPost = { + video: Uri, +}; + +record post { + content: textPost | imagePost | videoPost | _, +} +``` + +Old clients that don't know about `videoPost` can still handle the lexicon because of the open union. + +## Unions with Inline Objects + +You don't need to define types separately: + +```mlf +record embed { + content: { + text: string, + } | { + image: Uri, + } | { + link: Uri, + title: string, + }, +} +``` + +Though defining types separately is often cleaner. + +## Unions in Arrays + +Unions work in arrays: + +```mlf +def type mention = { + did: Did, + start: integer, + end: integer, +}; + +def type link = { + uri: Uri, + start: integer, + end: integer, +}; + +def type tag = { + name: string, + start: integer, + end: integer, +}; + +record post { + text: string, + facets: (mention | link | tag)[], +} +``` + +The `facets` array can contain any mix of mentions, links, and tags. + +## Complete Example + +Here's a complete forum post system with unions: + +**File: `com/example/forum/post.mlf`** +```mlf +/// Text content +def type textContent = { + text: string constrained { + maxGraphemes: 2000, + }, +}; + +/// Image content +def type imageContent = { + url: Uri, + width: integer, + height: integer, + alt?: string, +}; + +/// File attachment +def type fileContent = { + url: Uri, + filename: string, + size: integer, + mimeType: string, +}; + +/// A forum post with different content types +record post { + /// Post author + author: Did, + + /// Post content (text, image, or file) + content: textContent | imageContent | fileContent | _, + + /// When the post was created + createdAt: Datetime, + + /// Optional reply reference + replyTo?: AtUri, +} +``` + +The `| _` at the end means future content types can be added without breaking old clients. + +## What's Next? + +Now that you understand unions, let's learn about tokens for named constants. diff --git a/website/content/docs/language-guide/06-tokens.md b/website/content/docs/language-guide/06-tokens.md new file mode 100644 index 0000000..d907759 --- /dev/null +++ b/website/content/docs/language-guide/06-tokens.md @@ -0,0 +1,245 @@ ++++ +title = "Tokens" +weight = 6 ++++ + +Tokens are named constants that can be used in enums, known values, defaults, and unions. They make your lexicons more maintainable and self-documenting. + +## Defining Tokens + +Tokens are simple named values with documentation: + +```mlf +/// Open state +token open; + +/// Closed state +token closed; + +record issue { + state: string constrained { + enum: [open, closed], + }, +} +``` + +Notice we reference tokens without quotes: `[open, closed]`, not `["open", "closed"]`. + +## Tokens in Constraints + +Tokens work in enum and knownValues constraints: + +```mlf +/// Public visibility +token public; + +/// Private visibility +token private; + +/// Unlisted visibility +token unlisted; + +record post { + visibility: string constrained { + enum: [public, private, unlisted], + default: public, + }, +} +``` + +## Tokens in Unions + +Tokens can be used directly in unions: + +```mlf +/// Success status +token success; + +/// Error status +token error; + +/// Pending status +token pending; + +record result { + status: success | error | pending, +} +``` + +This creates a union where the field must be one of these three token values. + +## How Tokens Become Strings + +**Important:** In the generated lexicon, tokens are converted to **fully qualified NSID string literals**. + +If you define this in `com/example/forum/post.mlf`: + +```mlf +token draft; +token published; + +record post { + status: string constrained { + enum: [draft, published], + }, +} +``` + +The generated lexicon will have: + +```json +{ + "status": { + "type": "string", + "enum": ["com.example.forum.post#draft", "com.example.forum.post#published"] + } +} +``` + +The tokens become fully qualified: `com.example.forum.post#draft` and `com.example.forum.post#published`. + +## Why Use Tokens? + +**Without tokens:** +```mlf +record post { + state: string constrained { + knownValues: ["draft", "published", "archived"], + }, +} +``` + +- No documentation for individual values +- Easy to typo +- Hard to reuse across files + +**With tokens:** +```mlf +/// Draft state - not yet published +token draft; + +/// Published state - visible to all +token published; + +/// Archived state - no longer active +token archived; + +record post { + state: string constrained { + knownValues: [draft, published, archived], + }, +} +``` + +- Each value is documented +- No typos (references are checked) +- Can be reused across definitions +- More maintainable + +## Reusing Tokens + +Define tokens once and use them everywhere: + +```mlf +token active; +token inactive; +token suspended; + +record user { + status: string constrained { + enum: [active, inactive, suspended], + }, +} + +record account { + status: string constrained { + enum: [active, inactive], + }, +} +``` + +## Tokens vs String Literals + +You can mix tokens and string literals: + +```mlf +token published; +token archived; + +record post { + status: string constrained { + knownValues: [published, archived, "draft"], // Mix of token and literal + }, +} +``` + +But using tokens consistently is cleaner. + +## Complete Example + +Here's a complete forum thread system using tokens: + +**File: `com/example/forum/thread.mlf`** +```mlf +/// Thread is open for replies +token open; + +/// Thread is closed +token closed; + +/// Thread is pinned +token pinned; + +/// Thread is locked +token locked; + +/// High priority +token high; + +/// Medium priority +token medium; + +/// Low priority +token low; + +/// A thread in a forum +record thread { + /// Thread title + title: string constrained { + minGraphemes: 1, + maxGraphemes: 200, + }, + + /// Thread content + body?: string constrained { + maxGraphemes: 5000, + }, + + /// Thread status + status: string constrained { + enum: [open, closed, pinned, locked], + default: open, + }, + + /// Thread priority + priority: string constrained { + enum: [high, medium, low], + default: medium, + }, + + /// Thread author + author: Did, + + /// When thread was created + createdAt: Datetime, +} +``` + +In the generated lexicon: +- `open` becomes `com.example.forum.thread#open` +- `closed` becomes `com.example.forum.thread#closed` +- And so on... + +## What's Next? + +Now that you understand data types, let's learn about XRPC operations: queries, procedures, and subscriptions. diff --git a/website/content/docs/language-guide/07-xrpc.md b/website/content/docs/language-guide/07-xrpc.md new file mode 100644 index 0000000..2835b80 --- /dev/null +++ b/website/content/docs/language-guide/07-xrpc.md @@ -0,0 +1,345 @@ ++++ +title = "XRPC" +weight = 7 ++++ + +So far we've defined data structures with records. Now let's define operations using XRPC (Cross-organizational RPC). + +## XRPC Structure + +All XRPC definitions follow this pattern: + +``` + (): +``` + +Where: +- **keyword** - `query`, `procedure`, or `subscription` +- **name** - The operation name +- **input** - Parameters in parentheses +- **output** - Return type after `:` + +## Queries + +Queries are **read-only operations** that use HTTP GET. They retrieve data without modifying state. + +**Basic query:** +```mlf +/// Get a user profile +query getProfile( + actor: Did +):{ + did: Did, + handle: Handle, + displayName?: string, +}; +``` + +**Query with optional parameters:** +```mlf +/// Search for posts +query searchPosts( + q: string, + limit?: integer constrained { + minimum: 1, + maximum: 100, + default: 25, + }, + cursor?: string +):{ + posts: post[], + cursor?: string, +}; +``` + +**Query returning a record:** +```mlf +record profile { + did: Did, + handle: Handle, + displayName?: string, +} + +query getProfile( + actor: Did +):profile; +``` + +## Procedures + +Procedures are **write operations** that use HTTP POST. They create, update, or delete data. + +**Basic procedure:** +```mlf +/// Create a new post +procedure createPost( + text: string constrained { + minGraphemes: 1, + maxGraphemes: 500, + } +):{ + uri: AtUri, + cid: Cid, +}; +``` + +**Procedure with multiple parameters:** +```mlf +/// Update a user profile +procedure updateProfile( + displayName?: string, + bio?: string, + avatar?: blob +):{ + success: boolean, +}; +``` + +**Delete procedure:** +```mlf +/// Delete a post +procedure deletePost( + uri: AtUri +):{ + success: boolean, +}; +``` + +## Subscriptions + +Subscriptions are **real-time event streams** over WebSocket. They push updates to clients as events occur. + +**Basic subscription:** +```mlf +/// Subscribe to new posts +subscription subscribePosts():post; +``` + +**Subscription with parameters:** +```mlf +/// Subscribe to posts from specific users +subscription subscribePosts( + authors?: Did[] +):post; +``` + +**Subscription with multiple message types:** +```mlf +def type postCreated = { + post: post, +}; + +def type postDeleted = { + uri: AtUri, +}; + +def type postUpdated = { + post: post, +}; + +/// Subscribe to post events +subscription subscribePostEvents():postCreated | postDeleted | postUpdated; +``` + +**Resumable subscription with cursor:** +```mlf +/// Subscribe to repository events +subscription subscribeRepos( + cursor?: integer +):commit | identity | tombstone; +``` + +The `cursor` parameter lets clients resume from where they left off. + +## Differences Between Operations + +| Feature | Query | Procedure | Subscription | +|---------|-------|-----------|--------------| +| HTTP Method | GET | POST | WebSocket | +| Purpose | Read data | Write data | Real-time events | +| Idempotent | Yes | Usually no | N/A | +| Errors | `| error` | `| error` | Error frames | + +## Error Handling + +Queries and procedures can specify errors using `| error`: + +```mlf +query getPost( + uri: AtUri +):post | error { + /// Post not found + NotFound, + /// No permission to view + Forbidden, +}; +``` + +**Procedure with errors:** +```mlf +procedure createPost( + text: string +):{ + uri: AtUri, + cid: Cid, +} | error { + /// Text exceeds maximum length + TextTooLong, + /// User is rate limited + RateLimited, + /// User not authenticated + Unauthorized, +}; +``` + +Each error should have a doc comment explaining when it occurs. + +**Note:** Subscriptions don't use `| error` - errors are sent as special message frames over the WebSocket connection. + +## Parameters + +Parameters can have constraints just like record fields: + +```mlf +query searchPosts( + /// Search query (1-200 characters) + q: string constrained { + minLength: 1, + maxLength: 200, + }, + + /// Results per page + limit?: integer constrained { + minimum: 1, + maximum: 100, + default: 25, + } +):{ + posts: post[], +} +``` + +## Return Types + +Operations can return: + +**Inline objects:** +```mlf +query getStats():{ + posts: integer, + followers: integer, +}; +``` + +**Named records:** +```mlf +query getProfile(did: Did):profile; +``` + +**Unions:** +```mlf +query getPost(uri: AtUri):post | deleted; +``` + +## Complete Example + +Here's a complete API for a forum: + +**File: `com/example/forum/post.mlf`** +```mlf +/// A forum post +record post { + /// Post title + title: string constrained { + minGraphemes: 1, + maxGraphemes: 200, + }, + + /// Post content + body: string constrained { + maxGraphemes: 50000, + }, + + /// Post author + author: Did, + + /// When published + publishedAt: Datetime, +} + +/// Get a single post +query getPost( + /// Post URI + uri: AtUri +):post | error { + /// Post not found + NotFound, + /// Post is private + Forbidden, +} + +/// List posts by author +query listPosts( + /// Author DID + author: Did, + + /// Results per page + limit?: integer constrained { + minimum: 1, + maximum: 100, + default: 25, + }, + + /// Pagination cursor + cursor?: string +):{ + posts: post[], + cursor?: string, +}; + +/// Create a new post +procedure createPost( + /// Post title + title: string constrained { + minGraphemes: 1, + maxGraphemes: 200, + }, + + /// Post body + body: string constrained { + maxGraphemes: 50000, + } +):{ + uri: AtUri, + cid: Cid, + post: post, +} | error { + /// User not authenticated + Unauthorized, + /// Title or body invalid + InvalidInput, +}; + +/// Delete a post +procedure deletePost( + /// Post URI to delete + uri: AtUri +):{ + success: boolean, +} | error { + /// Post not found + NotFound, + /// User doesn't own this post + Forbidden, +}; + +/// Subscribe to new posts +subscription subscribePosts( + /// Optional author filter + author?: Did +):post; +``` + +## What's Next? + +Now that you can define complete APIs, let's learn how to import definitions from other files. diff --git a/website/content/docs/language-guide/08-imports.md b/website/content/docs/language-guide/08-imports.md new file mode 100644 index 0000000..5c7a421 --- /dev/null +++ b/website/content/docs/language-guide/08-imports.md @@ -0,0 +1,226 @@ ++++ +title = "Imports" +weight = 8 ++++ + +As your schemas grow, you'll want to split them across multiple files and reuse definitions. The `use` statement lets you import definitions from other files. + +## Basic Import + +Import a definition from another file: + +```mlf +use com.example.forum.profile; + +record post { + author: profile, +} +``` + +This imports the `profile` record from `com/example/forum/profile.mlf`. + +## How Imports Work + +The namespace matches the file path: + +| File Path | Namespace | Import Statement | +|-----------|-----------|------------------| +| `com/example/forum/user.mlf` | `com.example.forum.user` | `use com.example.forum.user;` | +| `com/example/forum/post.mlf` | `com.example.forum.post` | `use com.example.forum.post;` | + +## What Can Be Imported + +You can import **data-shaped definitions**: + +- ✅ **Records** - `record user { ... }` +- ✅ **Def types** - `def type author = { ... }` +- ✅ **Tokens** - `token public;` + +You **cannot** import XRPC operations: + +- ❌ **Queries** - `query getUser(...)` +- ❌ **Procedures** - `procedure createUser(...)` +- ❌ **Subscriptions** - `subscription subscribeUsers(...)` + +**Note:** You also cannot import inline types - they're file-local only. + +## Using Imported Definitions + +Once imported, reference the definition by its name: + +```mlf +use com.example.forum.author; +use com.example.forum.postRef; + +record comment { + text: string, + author: author, + replyTo: postRef, +} +``` + +## Multiple Imports + +Import multiple definitions with separate `use` statements: + +```mlf +use com.example.forum.author; +use com.example.forum.timestamp; +use com.example.forum.location; + +record post { + author: author, + createdAt: timestamp, + location?: location, +} +``` + +## Organizing Files + +Common organization patterns: + +**By feature:** +``` +com/ + example/ + forum/ + user.mlf + post.mlf + comment.mlf +``` + +**Shared types:** +``` +com/ + example/ + forum/ + author.mlf + postRef.mlf + post.mlf + comment.mlf +``` + +## Avoiding Circular Dependencies + +Don't create circular imports: + +```mlf +// user.mlf +use com.example.forum.post; + +record user { + recentPost?: post, // References post +} +``` + +```mlf +// post.mlf +use com.example.forum.user; + +record post { + author: user, // References user - CIRCULAR! +} +``` + +**Solution:** Use a shared reference type: + +```mlf +// userRef.mlf +def type userRef = { + did: Did, + handle: Handle, +}; +``` + +```mlf +// post.mlf +use com.example.forum.userRef; + +record post { + author: userRef, // No circular dependency +} +``` + +## Complete Example + +Here's a well-organized multi-file lexicon: + +**File: `com/example/forum/author.mlf`** +```mlf +/// Basic author information +def type author = { + did: Did, + handle: Handle, + displayName?: string, +}; +``` + +**File: `com/example/forum/postRef.mlf`** +```mlf +/// Reference to a post +def type postRef = { + uri: AtUri, + cid: Cid, +}; +``` + +**File: `com/example/forum/post.mlf`** +```mlf +use com.example.forum.author; +use com.example.forum.postRef; + +/// A forum post +record post { + /// Post text + text: string constrained { + minGraphemes: 1, + maxGraphemes: 500, + }, + + /// Post author + author: author, + + /// Optional reply reference + replyTo?: postRef, + + /// When created + createdAt: Datetime, +} + +/// Get a post by URI +query getPost( + uri: AtUri +):post | error { + NotFound, +}; +``` + +**File: `com/example/forum/comment.mlf`** +```mlf +use com.example.forum.author; +use com.example.forum.postRef; + +/// A comment on a post +record comment { + /// Comment text + text: string constrained { + minGraphemes: 1, + maxGraphemes: 500, + }, + + /// Comment author + author: author, + + /// Post being commented on + post: postRef, + + /// When created + createdAt: Datetime, +} +``` + +Both `post.mlf` and `comment.mlf` import and reuse the same `author` and `postRef` types. + +## What's Next? + +Finally, let's learn about the prelude - built-in types available in every file. diff --git a/website/content/docs/language-guide/09-prelude.md b/website/content/docs/language-guide/09-prelude.md new file mode 100644 index 0000000..9d5daa4 --- /dev/null +++ b/website/content/docs/language-guide/09-prelude.md @@ -0,0 +1,196 @@ ++++ +title = "Prelude" +weight = 9 ++++ + +The prelude is a set of definitions automatically available in every MLF file. You don't need to import them - they're always there. + +## String Format Types + +The prelude provides inline type aliases for common string formats: + +```mlf +// These are defined in the prelude - you can use them anywhere +record example { + id: Did, // Decentralized Identifier (did:*) + uri: AtUri, // AT Protocol URI (at://...) + timestamp: Datetime, // ISO 8601 datetime + website: Uri, // Generic URI + hash: Cid, // Content Identifier + username: Handle, // Handle (domain name) + lang: Language, // BCP 47 language code +} +``` + +## All Prelude Types + +Here are all the format types in the prelude: + +| Type | Description | Example | +|------|-------------|---------| +| `Did` | Decentralized Identifier | `did:plc:abc123...` | +| `AtUri` | AT Protocol URI | `at://did:plc:abc/app.bsky.feed.post/123` | +| `AtIdentifier` | DID or Handle | `did:plc:abc` or `alice.com` | +| `Handle` | Domain name handle | `alice.com` | +| `Datetime` | ISO 8601 datetime | `2024-01-15T10:30:00Z` | +| `Uri` | Generic URI | `https://example.com` | +| `Cid` | Content Identifier (IPFS) | `bafyrei...` | +| `Nsid` | Namespaced Identifier | `com.example.post` | +| `Tid` | Timestamp Identifier | `3l2p5g7...` | +| `RecordKey` | Record key in a repo | `3l2p5g7...` | +| `Language` | BCP 47 language tag | `en`, `en-US`, `ja` | + +## How They Work + +These types are implemented as inline types with format constraints: + +```mlf +// Simplified version of what's in the prelude +inline type Did = string constrained { + format: "did", +}; + +inline type Datetime = string constrained { + format: "datetime", +}; + +inline type Uri = string constrained { + format: "uri", +}; +``` + +When you use `Datetime` in your record, it expands to `string` with `format: "datetime"`. + +## Future: ATProto Types + +In the future, the prelude will also include all `com.atproto.*` definitions: + +```mlf +// Eventually, these will be in the prelude +record myPost { + // Reference standard ATProto types without importing + repo: com.atproto.sync.repo, + commit: com.atproto.sync.commit, +} +``` + +This will make it easier to reference standard ATProto types without manual imports. + +## Using Prelude Types + +You can use prelude types anywhere: + +**In records:** +```mlf +record post { + uri: AtUri, + author: Did, + createdAt: Datetime, +} +``` + +**In queries:** +```mlf +query getPost( + uri: AtUri +):post; +``` + +**In constraints:** +```mlf +record event { + participants: Did[] constrained { + maxLength: 100, + }, +} +``` + +**In custom types:** +```mlf +def type reference = { + uri: AtUri, + cid: Cid, +}; +``` + +## Complete Example + +Here's a complete lexicon using various prelude types: + +**File: `com/example/forum/profile.mlf`** +```mlf +/// A user profile +record profile { + /// User's DID + did: Did, + + /// User's handle + handle: Handle, + + /// Display name + displayName?: string constrained { + maxGraphemes: 64, + }, + + /// Profile description + description?: string constrained { + maxGraphemes: 256, + }, + + /// Avatar image URI + avatar?: Uri, + + /// Website link + website?: Uri, + + /// Account creation date + createdAt: Datetime, + + /// Preferred language + language?: Language, +} + +/// Get a profile by DID or handle +query getProfile( + /// User identifier (DID or handle) + actor: AtIdentifier +):profile | error { + /// Profile not found + NotFound, +}; + +/// Update your profile +procedure updateProfile( + /// New display name + displayName?: string, + + /// New description + description?: string, + + /// New avatar URI + avatar?: Uri, + + /// New website URI + website?: Uri +):{ + uri: AtUri, + cid: Cid, + profile: profile, +} | error { + /// User not authenticated + Unauthorized, +}; +``` + +All the format types (`Did`, `Handle`, `Uri`, `Datetime`, `Language`, `AtIdentifier`, `AtUri`, `Cid`) are from the prelude - no imports needed. + +## Summary + +The prelude provides: +- ✅ String format types for common patterns +- ✅ No imports needed - always available +- ✅ Eventually will include all `com.atproto.*` types + +You've now learned all the core features of MLF! You can define records, add constraints, create custom types, use unions and tokens, define XRPC operations, import from other files, and use prelude types. + +Check out the [Playground](/playground/) to experiment with MLF, or read the [CLI documentation](/docs/cli/) to learn how to compile your lexicons. diff --git a/website/content/docs/language-guide/_index.md b/website/content/docs/language-guide/_index.md new file mode 100644 index 0000000..b546826 --- /dev/null +++ b/website/content/docs/language-guide/_index.md @@ -0,0 +1,11 @@ ++++ +title = "Language Guide" +description = "A comprehensive guide to the MLF language" +weight = 2 +sort_by = "weight" +template = "section.html" ++++ + +This guide will teach you MLF from the ground up, starting with simple examples and gradually introducing more advanced features. + +Each section builds on the previous ones, so we recommend reading them in order if you're new to MLF. diff --git a/website/content/docs/syntax.md b/website/content/docs/syntax.md deleted file mode 100644 index 89b1211..0000000 --- a/website/content/docs/syntax.md +++ /dev/null @@ -1,593 +0,0 @@ -+++ -title = "Language Syntax" -description = "Complete reference for MLF syntax and features" -weight = 2 -+++ - -## File Structure - -### File Extension -- `.mlf` - MLF source files - -### Shebang (Optional) -```mlf -#!/usr/bin/env mlf -``` - -### File Naming Convention -The file path determines the lexicon NSID. Files should follow the lexicon NSID structure: -- `com.example.forum.thread.mlf` → Lexicon NSID: `com.example.forum.thread` -- `com.example.user.profile.mlf` → Lexicon NSID: `com.example.user.profile` - -The lexicon NSID is derived solely from the filename, not from any internal declarations. - -## Basic Structure - -Every MLF file can contain: - -- Use statements (imports) -- Type definitions (record, inline type, def type, token, query, procedure, subscription) - -## Syntax Rules - -### Semicolons - -- **Records** do NOT have semicolons after the closing brace `}` -- All other definitions require semicolons: - - `use` statements end with `;` - - `token` definitions end with `;` - - `inline type` definitions end with `;` - - `def type` definitions end with `;` - - `query` definitions end with `;` - - `procedure` definitions end with `;` - - `subscription` definitions end with `;` - -### Commas - -Commas are **required** between items, with **trailing commas allowed**: - -**Record fields:** -```mlf -record example { - field1: string, - field2: integer, // trailing comma allowed -} -``` - -**Constraints:** -```mlf -title: string constrained { - maxLength: 200, - minLength: 1, // trailing comma allowed -} -``` - -**Error definitions:** -```mlf -query getThread(): thread | error { - NotFound, - BadRequest, // trailing comma allowed -} -``` - -## Primitive Types - -- `null` - Null value -- `boolean` - True/false -- `integer` - 64-bit integer -- `number` - Double-precision float -- `string` - UTF-8 string -- `bytes` - Byte array -- `blob` - Binary large object with metadata -- `unknown` - Any value (for forward compatibility) - -## Special String Formats - -These are defined in the prelude and available everywhere: - -- `Did` - Decentralized Identifier (did:*) -- `AtUri` - AT-URI (at://...) -- `AtIdentifier` - Either a DID or Handle -- `Handle` - Handle identifier (domain name) -- `Datetime` - ISO 8601 datetime -- `Uri` - Generic URI -- `Cid` - Content Identifier -- `Nsid` - Namespaced Identifier -- `Tid` - Timestamp Identifier -- `RecordKey` - Record key -- `Language` - BCP 47 language code - -## Records - -Records define structured data types stored in repositories: - -```mlf -/// A forum thread -record thread { - /// Thread title - title: string constrained { - maxLength: 200 - minLength: 1 - } - /// Thread body - body?: string // Optional field - /// Thread creation timestamp - createdAt: Datetime -} -``` - -## Type Definitions - -MLF supports two kinds of type definitions: - -### Inline Types - -Expanded at the point of use, never appear in generated lexicon defs: - -```mlf -inline type AtIdentifier = string constrained { - format "at-identifier" -}; -``` - -### Def Types - -Become named definitions in the lexicon's defs block: - -```mlf -def type replyRef = { - root: AtUri - parent: AtUri -}; - -record thread { - reply?: replyRef -} -``` - -Use `inline type` for type aliases that should be expanded inline (like primitive type wrappers). Use `def type` for types that should be referenced by name in the generated lexicon. - -## Tokens - -Tokens are named constants used in enums and unions: - -```mlf -/// Open state -token open; - -/// Closed state -token closed; - -record issue { - state: string constrained { - knownValues: [open, closed] - default: "open" - } -} -``` - -Tokens must have doc comments describing their purpose. - -## Constrained Types - -Add validation constraints to types: - -```mlf -title: string constrained { - maxLength: 200 - minLength: 1 -} - -age: integer constrained { - minimum: 0 - maximum: 150 -} - -status: string constrained { - enum: ["draft", "published", "archived"] -} -``` - -### String Constraints - -- `maxLength` / `minLength` - Length in bytes -- `maxGraphemes` / `minGraphemes` - Length in grapheme clusters -- `format` - Format validation (datetime, uri, did, handle, etc.) -- `enum` - Allowed values (closed set) - accepts string literals or token references -- `knownValues` - Known values (extensible set) - accepts string literals or token references -- `default` - Default value - -**enum, knownValues, and default** can use either literals or references: -```mlf -// String literals -status: string constrained { - knownValues: ["open", "closed", "pending"] - default: "open" -} - -// References to named items (tokens, aliases, records, etc.) -token open; -token closed; - -status: string constrained { - knownValues: [open, closed] // References tokens defined above - default: open // References the token -} -``` - -### Integer Constraints - -- `minimum` / `maximum` - Min/max values -- `enum` - Allowed values -- `default` - Default value - -### Array Constraints - -```mlf -tags: string[] constrained { - minLength: 1 - maxLength: 10 -} -``` - -### Blob Constraints - -```mlf -avatar: blob constrained { - accept: ["image/png", "image/jpeg"] - maxSize: 1000000 // bytes -} -``` - -### Boolean Constraints - -```mlf -field: boolean constrained { - default: false -} -``` - -### Constraint Refinement - -Constraints can only make types **more restrictive**, never less restrictive: - -```mlf -def type shortString = string constrained { - maxLength: 100 -}; - -record post { - // Valid: 50 is more restrictive than 100 - title: shortString constrained { - maxLength: 50 - } -} -``` - -**Refinement rules:** -- Numeric bounds: `minimum` can only increase, `maximum` can only decrease -- Length bounds: `minLength`/`minGraphemes` can only increase, `maxLength`/`maxGraphemes` can only decrease -- Enums: Can only restrict to a subset -- Format: Cannot change once specified - -## Arrays - -```mlf -tags: string[] - -items: string[] constrained { - minLength: 1, - maxLength: 10, -} -``` - -## Unions - -Use the pipe operator `|`: - -```mlf -// Closed union (only these types) -content: text | image | video - -// Union of tokens -state: open | closed | pending -``` - -Open unions (allowing unknown types) use `_`: - -```mlf -// Open union (can include unknown types) -content: text | image | _ -``` - -## Objects - -Inline object types: - -```mlf -metadata: { - version: integer - timestamp: Datetime -} -``` - -## Queries - -Queries are read-only HTTP endpoints (GET): - -```mlf -/// Get a user profile -query getProfile( - /// The actor's DID or handle - actor: AtIdentifier -): profile; -``` - -With errors: - -```mlf -query getThread( - uri: AtUri -): thread | error { - /// Thread not found - NotFound - /// Invalid request - BadRequest -}; -``` - -## Procedures - -Procedures are write operations (POST): - -```mlf -/// Create a new thread -procedure createThread( - title: string - body: string -): { - uri: AtUri - cid: Cid -} | error { - /// Title too long - TitleTooLong -}; -``` - -## Subscriptions - -Subscriptions are WebSocket-based event streams: - -```mlf -/// Subscribe to repository events -subscription subscribeRepos( - /// Optional cursor for resuming - cursor?: integer -): commit | identity | handle; -``` - -Message types must be defined as def types or records: - -```mlf -/// Commit message -def type commit = { - seq: integer - repo: Did - commit: Cid - time: Datetime -}; - -/// Identity message -def type identity = { - did: Did - handle: Handle -}; -``` - -## Comments - -### Documentation Comments - -Use `///` for documentation (appears in generated docs/code): - -```mlf -/// A forum thread -record thread { - /// Thread title - title: string -} -``` - -### Regular Comments - -Use `//` for comments that won't appear in output: - -```mlf -// This is a regular comment -record example { - field: string // inline comment -} -``` - -## Annotations - -Annotations use `@` and provide metadata for external tooling: - -### Simple Annotation -```mlf -@deprecated -record oldRecord { - field: string -} -``` - -### Positional Arguments -```mlf -@since(1, 2, 0) -@doc("https://example.com/docs") -record example { - field: string -} -``` - -### Named Arguments -```mlf -@validate(min: 0, max: 100, strict: true) -@table(name: "threads", indexes: "did,createdAt") -record thread { - @indexed - did: Did - - @sensitive(pii: true) - title: string -} -``` - -Annotations can be placed on records, inline types, def types, tokens, queries, procedures, subscriptions, and fields. - -## Imports - -Import definitions from other lexicons: - -```mlf -// Single import -use com.example.user.profile; - -// Multiple imports -use com.example.forum.{thread, reply}; - -// Alias import -use com.example.user as User; - -// Wildcard import -use com.example.forum.*; - -// Import with alias -use com.example.forum.{thread as ForumThread}; -``` - -After importing, use the short name: - -```mlf -use com.example.user.profile; - -record thread { - author: profile // Instead of com.example.user.profile -} -``` - -## References - -Reference local or external definitions: - -```mlf -// Local reference (same file) -record thread { - author: author // References 'def type author' in same file -} - -// Cross-file reference -record thread { - profile: com.example.user.profile // References com/example/user/profile.mlf -} -``` - -All references use dotted notation. - -## Optional Fields - -Use `?` to mark fields as optional: - -```mlf -record thread { - title: string // Required - body?: string // Optional - tags?: string[] // Optional array -} -``` - -## Raw Identifiers - -Use backticks to escape reserved keywords when you need to use them as identifiers: - -```mlf -def type `record` = { - `record`: com.atproto.repo.strongRef - `error`: string -}; -``` - -This is useful when working with existing schemas that use MLF keywords as field or type names. - -## Format Strings - -Available format strings for constrained strings: - -- `datetime` - ISO 8601 datetime -- `uri` - URI (RFC 3986) -- `at-uri` - AT-URI (ATProto) -- `did` - Decentralized Identifier -- `handle` - ATProto handle -- `nsid` - Namespaced Identifier -- `cid` - Content Identifier -- `at-identifier` - DID or handle -- `language` - BCP 47 language tag -- `tid` - Timestamp ID -- `record-key` - Record key - -## Complete Example - -```mlf -#!/usr/bin/env mlf - -use com.example.user.profile; - -/// Open state -token open; - -/// Closed state -token closed; - -/// A forum thread -record thread { - /// Thread title - title: string constrained { - minGraphemes: 1 - maxGraphemes: 200 - } - /// Thread body (markdown) - body?: string constrained { - maxGraphemes: 10000 - } - /// Thread state - state: string constrained { - knownValues: [open, closed] - default: "open" - } - /// Author profile - author: profile - /// Creation timestamp - createdAt: Datetime -} - -/// Get a thread by URI -query getThread( - /// Thread AT-URI - uri: AtUri -): thread | error { - /// Thread not found - NotFound -}; - -/// Create a new thread -procedure createThread( - title: string - body?: string -): { - uri: AtUri - cid: Cid -} | error { - /// Title too long - TitleTooLong -}; -``` diff --git a/website/sass/style.scss b/website/sass/style.scss index afb8dff..45ab039 100644 --- a/website/sass/style.scss +++ b/website/sass/style.scss @@ -823,6 +823,32 @@ footer { font-weight: 500; } +.doc-nav .nav-section { + margin-top: 1rem; +} + +.doc-nav .nav-section-title { + display: block; + padding: 0.5rem 0.75rem; + font-weight: 600; + color: var(--text); + margin-bottom: 0.25rem; +} + +.doc-nav .nav-section ul { + margin-top: 0.25rem; + padding-left: 1rem; +} + +.doc-nav .nav-section ul li { + margin-bottom: 0.25rem; +} + +.doc-nav .nav-section ul a { + font-size: 0.875rem; + padding: 0.375rem 0.75rem; +} + .doc-main { min-width: 0; } diff --git a/website/templates/page.html b/website/templates/page.html index 21d1ddb..67cf44a 100644 --- a/website/templates/page.html +++ b/website/templates/page.html @@ -19,6 +19,21 @@ {% endfor %} + {% for subsection in docs_section.subsections %} + {% set sub = get_section(path=subsection) %} + + {% endfor %}