diff --git a/skills/json-canvas/SKILL.md b/skills/json-canvas/SKILL.md index 0ddd0e4..8fb2c9d 100644 --- a/skills/json-canvas/SKILL.md +++ b/skills/json-canvas/SKILL.md @@ -5,15 +5,9 @@ description: Create and edit JSON Canvas files (.canvas) with nodes, edges, grou # JSON Canvas Skill -This skill enables skills-compatible agents to create and edit valid JSON Canvas files (`.canvas`) used in Obsidian and other applications. - -## Overview - -JSON Canvas is an open file format for infinite canvas data. Canvas files use the `.canvas` extension and contain valid JSON following the [JSON Canvas Spec 1.0](https://jsoncanvas.org/spec/1.0/). - ## File Structure -A canvas file contains two top-level arrays: +A canvas file (`.canvas`) contains two top-level arrays following the [JSON Canvas Spec 1.0](https://jsoncanvas.org/spec/1.0/): ```json { @@ -25,37 +19,64 @@ A canvas file contains two top-level arrays: - `nodes` (optional): Array of node objects - `edges` (optional): Array of edge objects connecting nodes +## Common Workflows + +### 1. Create a New Canvas + +1. Create a `.canvas` file with the base structure `{"nodes": [], "edges": []}` +2. Generate unique 16-character hex IDs for each node (e.g., `"6f0ad84f44ce9c17"`) +3. Add nodes with required fields: `id`, `type`, `x`, `y`, `width`, `height` +4. Add edges referencing valid node IDs via `fromNode` and `toNode` +5. **Validate**: Parse the JSON to confirm it is valid. Verify all `fromNode`/`toNode` values exist in the nodes array + +### 2. Add a Node to an Existing Canvas + +1. Read and parse the existing `.canvas` file +2. Generate a unique ID that does not collide with existing node or edge IDs +3. Choose position (`x`, `y`) that avoids overlapping existing nodes (leave 50-100px spacing) +4. Append the new node object to the `nodes` array +5. Optionally add edges connecting the new node to existing nodes +6. **Validate**: Confirm all IDs are unique and all edge references resolve to existing nodes + +### 3. Connect Two Nodes + +1. Identify the source and target node IDs +2. Generate a unique edge ID +3. Set `fromNode` and `toNode` to the source and target IDs +4. Optionally set `fromSide`/`toSide` (top, right, bottom, left) for anchor points +5. Optionally set `label` for descriptive text on the edge +6. Append the edge to the `edges` array +7. **Validate**: Confirm both `fromNode` and `toNode` reference existing node IDs + +### 4. Edit an Existing Canvas + +1. Read and parse the `.canvas` file as JSON +2. Locate the target node or edge by `id` +3. Modify the desired attributes (text, position, color, etc.) +4. Write the updated JSON back to the file +5. **Validate**: Re-check all ID uniqueness and edge reference integrity after editing + ## Nodes -Nodes are objects placed on the canvas. There are four node types: -- `text` - Text content with Markdown -- `file` - Reference to files/attachments -- `link` - External URL -- `group` - Visual container for other nodes - -### Z-Index Ordering - -Nodes are ordered by z-index in the array: -- First node = bottom layer (displayed below others) -- Last node = top layer (displayed above others) +Nodes are objects placed on the canvas. Array order determines z-index: first node = bottom layer, last node = top layer. ### Generic Node Attributes -All nodes share these attributes: - | Attribute | Required | Type | Description | |-----------|----------|------|-------------| -| `id` | Yes | string | Unique identifier for the node | -| `type` | Yes | string | Node type: `text`, `file`, `link`, or `group` | +| `id` | Yes | string | Unique 16-char hex identifier | +| `type` | Yes | string | `text`, `file`, `link`, or `group` | | `x` | Yes | integer | X position in pixels | | `y` | Yes | integer | Y position in pixels | | `width` | Yes | integer | Width in pixels | | `height` | Yes | integer | Height in pixels | -| `color` | No | canvasColor | Node color (see Color section) | +| `color` | No | canvasColor | Preset `"1"`-`"6"` or hex (e.g., `"#FF0000"`) | ### Text Nodes -Text nodes contain Markdown content. +| Attribute | Required | Type | Description | +|-----------|----------|------|-------------| +| `text` | Yes | string | Plain text with Markdown syntax | ```json { @@ -69,27 +90,14 @@ Text nodes contain Markdown content. } ``` -#### Newline Escaping (Common Pitfall) - -In JSON, newline characters inside strings **must** be represented as `\n`. Do **not** use the literal sequence `\\n` in a `.canvas` file—Obsidian will render it as the characters `\` and `n` instead of a line break. - -Examples: - -```json -{ "type": "text", "text": "Line 1\nLine 2" } -``` - -```json -{ "type": "text", "text": "Line 1\\nLine 2" } -``` - -| Attribute | Required | Type | Description | -|-----------|----------|------|-------------| -| `text` | Yes | string | Plain text with Markdown syntax | +**Newline pitfall**: Use `\n` for line breaks in JSON strings. Do **not** use the literal `\\n` -- Obsidian renders that as the characters `\` and `n`. ### File Nodes -File nodes reference files or attachments (images, videos, PDFs, notes, etc.). +| Attribute | Required | Type | Description | +|-----------|----------|------|-------------| +| `file` | Yes | string | Path to file within the system | +| `subpath` | No | string | Link to heading or block (starts with `#`) | ```json { @@ -103,27 +111,11 @@ File nodes reference files or attachments (images, videos, PDFs, notes, etc.). } ``` -```json -{ - "id": "b2c3d4e5f6789012", - "type": "file", - "x": 500, - "y": 400, - "width": 400, - "height": 300, - "file": "Notes/Project Overview.md", - "subpath": "#Implementation" -} -``` +### Link Nodes | Attribute | Required | Type | Description | |-----------|----------|------|-------------| -| `file` | Yes | string | Path to file within the system | -| `subpath` | No | string | Link to heading or block (starts with `#`) | - -### Link Nodes - -Link nodes display external URLs. +| `url` | Yes | string | External URL | ```json { @@ -137,13 +129,15 @@ Link nodes display external URLs. } ``` -| Attribute | Required | Type | Description | -|-----------|----------|------|-------------| -| `url` | Yes | string | External URL | - ### Group Nodes -Group nodes are visual containers for organizing other nodes. +Groups are visual containers for organizing other nodes. Position child nodes inside the group's bounds. + +| Attribute | Required | Type | Description | +|-----------|----------|------|-------------| +| `label` | No | string | Text label for the group | +| `background` | No | string | Path to background image | +| `backgroundStyle` | No | string | `cover`, `ratio`, or `repeat` | ```json { @@ -158,107 +152,37 @@ Group nodes are visual containers for organizing other nodes. } ``` -```json -{ - "id": "e5f67890123456ab", - "type": "group", - "x": 0, - "y": 700, - "width": 800, - "height": 500, - "label": "Resources", - "background": "Attachments/background.png", - "backgroundStyle": "cover" -} -``` - -| Attribute | Required | Type | Description | -|-----------|----------|------|-------------| -| `label` | No | string | Text label for the group | -| `background` | No | string | Path to background image | -| `backgroundStyle` | No | string | Background rendering style | - -#### Background Styles - -| Value | Description | -|-------|-------------| -| `cover` | Fills entire width and height of node | -| `ratio` | Maintains aspect ratio of background image | -| `repeat` | Repeats image as pattern in both directions | - ## Edges -Edges are lines connecting nodes. +Edges connect nodes via `fromNode` and `toNode` IDs. -```json -{ - "id": "f67890123456789a", - "fromNode": "6f0ad84f44ce9c17", - "toNode": "a1b2c3d4e5f67890" -} -``` +| Attribute | Required | Type | Default | Description | +|-----------|----------|------|---------|-------------| +| `id` | Yes | string | - | Unique identifier | +| `fromNode` | Yes | string | - | Source node ID | +| `fromSide` | No | string | - | `top`, `right`, `bottom`, or `left` | +| `fromEnd` | No | string | `none` | `none` or `arrow` | +| `toNode` | Yes | string | - | Target node ID | +| `toSide` | No | string | - | `top`, `right`, `bottom`, or `left` | +| `toEnd` | No | string | `arrow` | `none` or `arrow` | +| `color` | No | canvasColor | - | Line color | +| `label` | No | string | - | Text label | ```json { "id": "0123456789abcdef", "fromNode": "6f0ad84f44ce9c17", "fromSide": "right", - "fromEnd": "none", - "toNode": "b2c3d4e5f6789012", + "toNode": "a1b2c3d4e5f67890", "toSide": "left", "toEnd": "arrow", - "color": "1", "label": "leads to" } ``` -| Attribute | Required | Type | Default | Description | -|-----------|----------|------|---------|-------------| -| `id` | Yes | string | - | Unique identifier for the edge | -| `fromNode` | Yes | string | - | Node ID where connection starts | -| `fromSide` | No | string | - | Side where edge starts | -| `fromEnd` | No | string | `none` | Shape at edge start | -| `toNode` | Yes | string | - | Node ID where connection ends | -| `toSide` | No | string | - | Side where edge ends | -| `toEnd` | No | string | `arrow` | Shape at edge end | -| `color` | No | canvasColor | - | Line color | -| `label` | No | string | - | Text label for the edge | - -### Side Values - -| Value | Description | -|-------|-------------| -| `top` | Top edge of node | -| `right` | Right edge of node | -| `bottom` | Bottom edge of node | -| `left` | Left edge of node | - -### End Shapes - -| Value | Description | -|-------|-------------| -| `none` | No endpoint shape | -| `arrow` | Arrow endpoint | - ## Colors -The `canvasColor` type can be specified in two ways: - -### Hex Colors - -```json -{ - "color": "#FF0000" -} -``` - -### Preset Colors - -```json -{ - "color": "1" -} -``` +The `canvasColor` type accepts either a hex string or a preset number: | Preset | Color | |--------|-------| @@ -269,360 +193,23 @@ The `canvasColor` type can be specified in two ways: | `"5"` | Cyan | | `"6"` | Purple | -Note: Specific color values for presets are intentionally undefined, allowing applications to use their own brand colors. - -## Complete Examples - -### Simple Canvas with Text and Connections - -```json -{ - "nodes": [ - { - "id": "8a9b0c1d2e3f4a5b", - "type": "text", - "x": 0, - "y": 0, - "width": 300, - "height": 150, - "text": "# Main Idea\n\nThis is the central concept." - }, - { - "id": "1a2b3c4d5e6f7a8b", - "type": "text", - "x": 400, - "y": -100, - "width": 250, - "height": 100, - "text": "## Supporting Point A\n\nDetails here." - }, - { - "id": "2b3c4d5e6f7a8b9c", - "type": "text", - "x": 400, - "y": 100, - "width": 250, - "height": 100, - "text": "## Supporting Point B\n\nMore details." - } - ], - "edges": [ - { - "id": "3c4d5e6f7a8b9c0d", - "fromNode": "8a9b0c1d2e3f4a5b", - "fromSide": "right", - "toNode": "1a2b3c4d5e6f7a8b", - "toSide": "left" - }, - { - "id": "4d5e6f7a8b9c0d1e", - "fromNode": "8a9b0c1d2e3f4a5b", - "fromSide": "right", - "toNode": "2b3c4d5e6f7a8b9c", - "toSide": "left" - } - ] -} -``` - -### Project Board with Groups - -```json -{ - "nodes": [ - { - "id": "5e6f7a8b9c0d1e2f", - "type": "group", - "x": 0, - "y": 0, - "width": 300, - "height": 500, - "label": "To Do", - "color": "1" - }, - { - "id": "6f7a8b9c0d1e2f3a", - "type": "group", - "x": 350, - "y": 0, - "width": 300, - "height": 500, - "label": "In Progress", - "color": "3" - }, - { - "id": "7a8b9c0d1e2f3a4b", - "type": "group", - "x": 700, - "y": 0, - "width": 300, - "height": 500, - "label": "Done", - "color": "4" - }, - { - "id": "8b9c0d1e2f3a4b5c", - "type": "text", - "x": 20, - "y": 50, - "width": 260, - "height": 80, - "text": "## Task 1\n\nImplement feature X" - }, - { - "id": "9c0d1e2f3a4b5c6d", - "type": "text", - "x": 370, - "y": 50, - "width": 260, - "height": 80, - "text": "## Task 2\n\nReview PR #123", - "color": "2" - }, - { - "id": "0d1e2f3a4b5c6d7e", - "type": "text", - "x": 720, - "y": 50, - "width": 260, - "height": 80, - "text": "## Task 3\n\n~~Setup CI/CD~~" - } - ], - "edges": [] -} -``` - -### Research Canvas with Files and Links - -```json -{ - "nodes": [ - { - "id": "1e2f3a4b5c6d7e8f", - "type": "text", - "x": 300, - "y": 200, - "width": 400, - "height": 200, - "text": "# Research Topic\n\n## Key Questions\n\n- How does X affect Y?\n- What are the implications?", - "color": "5" - }, - { - "id": "2f3a4b5c6d7e8f9a", - "type": "file", - "x": 0, - "y": 0, - "width": 250, - "height": 150, - "file": "Literature/Paper A.pdf" - }, - { - "id": "3a4b5c6d7e8f9a0b", - "type": "file", - "x": 0, - "y": 200, - "width": 250, - "height": 150, - "file": "Notes/Meeting Notes.md", - "subpath": "#Key Insights" - }, - { - "id": "4b5c6d7e8f9a0b1c", - "type": "link", - "x": 0, - "y": 400, - "width": 250, - "height": 100, - "url": "https://example.com/research" - }, - { - "id": "5c6d7e8f9a0b1c2d", - "type": "file", - "x": 750, - "y": 150, - "width": 300, - "height": 250, - "file": "Attachments/diagram.png" - } - ], - "edges": [ - { - "id": "6d7e8f9a0b1c2d3e", - "fromNode": "2f3a4b5c6d7e8f9a", - "fromSide": "right", - "toNode": "1e2f3a4b5c6d7e8f", - "toSide": "left", - "label": "supports" - }, - { - "id": "7e8f9a0b1c2d3e4f", - "fromNode": "3a4b5c6d7e8f9a0b", - "fromSide": "right", - "toNode": "1e2f3a4b5c6d7e8f", - "toSide": "left", - "label": "informs" - }, - { - "id": "8f9a0b1c2d3e4f5a", - "fromNode": "4b5c6d7e8f9a0b1c", - "fromSide": "right", - "toNode": "1e2f3a4b5c6d7e8f", - "toSide": "left", - "toEnd": "arrow", - "color": "6" - }, - { - "id": "9a0b1c2d3e4f5a6b", - "fromNode": "1e2f3a4b5c6d7e8f", - "fromSide": "right", - "toNode": "5c6d7e8f9a0b1c2d", - "toSide": "left", - "label": "visualized by" - } - ] -} -``` - -### Flowchart - -```json -{ - "nodes": [ - { - "id": "a0b1c2d3e4f5a6b7", - "type": "text", - "x": 200, - "y": 0, - "width": 150, - "height": 60, - "text": "**Start**", - "color": "4" - }, - { - "id": "b1c2d3e4f5a6b7c8", - "type": "text", - "x": 200, - "y": 100, - "width": 150, - "height": 60, - "text": "Step 1:\nGather data" - }, - { - "id": "c2d3e4f5a6b7c8d9", - "type": "text", - "x": 200, - "y": 200, - "width": 150, - "height": 80, - "text": "**Decision**\n\nIs data valid?", - "color": "3" - }, - { - "id": "d3e4f5a6b7c8d9e0", - "type": "text", - "x": 400, - "y": 200, - "width": 150, - "height": 60, - "text": "Process data" - }, - { - "id": "e4f5a6b7c8d9e0f1", - "type": "text", - "x": 0, - "y": 200, - "width": 150, - "height": 60, - "text": "Request new data", - "color": "1" - }, - { - "id": "f5a6b7c8d9e0f1a2", - "type": "text", - "x": 400, - "y": 320, - "width": 150, - "height": 60, - "text": "**End**", - "color": "4" - } - ], - "edges": [ - { - "id": "a6b7c8d9e0f1a2b3", - "fromNode": "a0b1c2d3e4f5a6b7", - "fromSide": "bottom", - "toNode": "b1c2d3e4f5a6b7c8", - "toSide": "top" - }, - { - "id": "b7c8d9e0f1a2b3c4", - "fromNode": "b1c2d3e4f5a6b7c8", - "fromSide": "bottom", - "toNode": "c2d3e4f5a6b7c8d9", - "toSide": "top" - }, - { - "id": "c8d9e0f1a2b3c4d5", - "fromNode": "c2d3e4f5a6b7c8d9", - "fromSide": "right", - "toNode": "d3e4f5a6b7c8d9e0", - "toSide": "left", - "label": "Yes", - "color": "4" - }, - { - "id": "d9e0f1a2b3c4d5e6", - "fromNode": "c2d3e4f5a6b7c8d9", - "fromSide": "left", - "toNode": "e4f5a6b7c8d9e0f1", - "toSide": "right", - "label": "No", - "color": "1" - }, - { - "id": "e0f1a2b3c4d5e6f7", - "fromNode": "e4f5a6b7c8d9e0f1", - "fromSide": "top", - "fromEnd": "none", - "toNode": "b1c2d3e4f5a6b7c8", - "toSide": "left", - "toEnd": "arrow" - }, - { - "id": "f1a2b3c4d5e6f7a8", - "fromNode": "d3e4f5a6b7c8d9e0", - "fromSide": "bottom", - "toNode": "f5a6b7c8d9e0f1a2", - "toSide": "top" - } - ] -} -``` +Preset color values are intentionally undefined -- applications use their own brand colors. ## ID Generation -Node and edge IDs must be unique strings. Obsidian generates 16-character hexadecimal IDs: +Generate 16-character lowercase hexadecimal strings (64-bit random value): -```json -"id": "6f0ad84f44ce9c17" -"id": "a3b2c1d0e9f8g7h6" -"id": "1234567890abcdef" ``` - -This format is a 16-character lowercase hex string (64-bit random value). +"6f0ad84f44ce9c17" +"a3b2c1d0e9f8a7b6" +``` ## Layout Guidelines -### Positioning - - Coordinates can be negative (canvas extends infinitely) -- `x` increases to the right -- `y` increases downward -- Position refers to top-left corner of node - -### Recommended Sizes +- `x` increases right, `y` increases down; position is the top-left corner +- Space nodes 50-100px apart; leave 20-50px padding inside groups +- Align to grid (multiples of 10 or 20) for cleaner layouts | Node Type | Suggested Width | Suggested Height | |-----------|-----------------|------------------| @@ -631,24 +218,25 @@ This format is a 16-character lowercase hex string (64-bit random value). | Large text | 400-600 | 300-500 | | File preview | 300-500 | 200-400 | | Link preview | 250-400 | 100-200 | -| Group | Varies | Varies | -### Spacing +## Validation Checklist -- Leave 20-50px padding inside groups -- Space nodes 50-100px apart for readability -- Align nodes to grid (multiples of 10 or 20) for cleaner layouts +After creating or editing a canvas file, verify: -## Validation Rules +1. All `id` values are unique across both nodes and edges +2. Every `fromNode` and `toNode` references an existing node ID +3. Required fields are present for each node type (`text` for text nodes, `file` for file nodes, `url` for link nodes) +4. `type` is one of: `text`, `file`, `link`, `group` +5. `fromSide`/`toSide` values are one of: `top`, `right`, `bottom`, `left` +6. `fromEnd`/`toEnd` values are one of: `none`, `arrow` +7. Color presets are `"1"` through `"6"` or valid hex (e.g., `"#FF0000"`) +8. JSON is valid and parseable -1. All `id` values must be unique across nodes and edges -2. `fromNode` and `toNode` must reference existing node IDs -3. Required fields must be present for each node type -4. `type` must be one of: `text`, `file`, `link`, `group` -5. `backgroundStyle` must be one of: `cover`, `ratio`, `repeat` -6. `fromSide`, `toSide` must be one of: `top`, `right`, `bottom`, `left` -7. `fromEnd`, `toEnd` must be one of: `none`, `arrow` -8. Color presets must be `"1"` through `"6"` or valid hex color +If validation fails, check for duplicate IDs, dangling edge references, or malformed JSON strings (especially unescaped newlines in text content). + +## Complete Examples + +See [references/EXAMPLES.md](references/EXAMPLES.md) for full canvas examples including mind maps, project boards, research canvases, and flowcharts. ## References diff --git a/skills/json-canvas/references/EXAMPLES.md b/skills/json-canvas/references/EXAMPLES.md new file mode 100644 index 0000000..c94f996 --- /dev/null +++ b/skills/json-canvas/references/EXAMPLES.md @@ -0,0 +1,329 @@ +# JSON Canvas Complete Examples + +## Simple Canvas with Text and Connections + +```json +{ + "nodes": [ + { + "id": "8a9b0c1d2e3f4a5b", + "type": "text", + "x": 0, + "y": 0, + "width": 300, + "height": 150, + "text": "# Main Idea\n\nThis is the central concept." + }, + { + "id": "1a2b3c4d5e6f7a8b", + "type": "text", + "x": 400, + "y": -100, + "width": 250, + "height": 100, + "text": "## Supporting Point A\n\nDetails here." + }, + { + "id": "2b3c4d5e6f7a8b9c", + "type": "text", + "x": 400, + "y": 100, + "width": 250, + "height": 100, + "text": "## Supporting Point B\n\nMore details." + } + ], + "edges": [ + { + "id": "3c4d5e6f7a8b9c0d", + "fromNode": "8a9b0c1d2e3f4a5b", + "fromSide": "right", + "toNode": "1a2b3c4d5e6f7a8b", + "toSide": "left" + }, + { + "id": "4d5e6f7a8b9c0d1e", + "fromNode": "8a9b0c1d2e3f4a5b", + "fromSide": "right", + "toNode": "2b3c4d5e6f7a8b9c", + "toSide": "left" + } + ] +} +``` + +## Project Board with Groups + +```json +{ + "nodes": [ + { + "id": "5e6f7a8b9c0d1e2f", + "type": "group", + "x": 0, + "y": 0, + "width": 300, + "height": 500, + "label": "To Do", + "color": "1" + }, + { + "id": "6f7a8b9c0d1e2f3a", + "type": "group", + "x": 350, + "y": 0, + "width": 300, + "height": 500, + "label": "In Progress", + "color": "3" + }, + { + "id": "7a8b9c0d1e2f3a4b", + "type": "group", + "x": 700, + "y": 0, + "width": 300, + "height": 500, + "label": "Done", + "color": "4" + }, + { + "id": "8b9c0d1e2f3a4b5c", + "type": "text", + "x": 20, + "y": 50, + "width": 260, + "height": 80, + "text": "## Task 1\n\nImplement feature X" + }, + { + "id": "9c0d1e2f3a4b5c6d", + "type": "text", + "x": 370, + "y": 50, + "width": 260, + "height": 80, + "text": "## Task 2\n\nReview PR #123", + "color": "2" + }, + { + "id": "0d1e2f3a4b5c6d7e", + "type": "text", + "x": 720, + "y": 50, + "width": 260, + "height": 80, + "text": "## Task 3\n\n~~Setup CI/CD~~" + } + ], + "edges": [] +} +``` + +## Research Canvas with Files and Links + +```json +{ + "nodes": [ + { + "id": "1e2f3a4b5c6d7e8f", + "type": "text", + "x": 300, + "y": 200, + "width": 400, + "height": 200, + "text": "# Research Topic\n\n## Key Questions\n\n- How does X affect Y?\n- What are the implications?", + "color": "5" + }, + { + "id": "2f3a4b5c6d7e8f9a", + "type": "file", + "x": 0, + "y": 0, + "width": 250, + "height": 150, + "file": "Literature/Paper A.pdf" + }, + { + "id": "3a4b5c6d7e8f9a0b", + "type": "file", + "x": 0, + "y": 200, + "width": 250, + "height": 150, + "file": "Notes/Meeting Notes.md", + "subpath": "#Key Insights" + }, + { + "id": "4b5c6d7e8f9a0b1c", + "type": "link", + "x": 0, + "y": 400, + "width": 250, + "height": 100, + "url": "https://example.com/research" + }, + { + "id": "5c6d7e8f9a0b1c2d", + "type": "file", + "x": 750, + "y": 150, + "width": 300, + "height": 250, + "file": "Attachments/diagram.png" + } + ], + "edges": [ + { + "id": "6d7e8f9a0b1c2d3e", + "fromNode": "2f3a4b5c6d7e8f9a", + "fromSide": "right", + "toNode": "1e2f3a4b5c6d7e8f", + "toSide": "left", + "label": "supports" + }, + { + "id": "7e8f9a0b1c2d3e4f", + "fromNode": "3a4b5c6d7e8f9a0b", + "fromSide": "right", + "toNode": "1e2f3a4b5c6d7e8f", + "toSide": "left", + "label": "informs" + }, + { + "id": "8f9a0b1c2d3e4f5a", + "fromNode": "4b5c6d7e8f9a0b1c", + "fromSide": "right", + "toNode": "1e2f3a4b5c6d7e8f", + "toSide": "left", + "toEnd": "arrow", + "color": "6" + }, + { + "id": "9a0b1c2d3e4f5a6b", + "fromNode": "1e2f3a4b5c6d7e8f", + "fromSide": "right", + "toNode": "5c6d7e8f9a0b1c2d", + "toSide": "left", + "label": "visualized by" + } + ] +} +``` + +## Flowchart + +```json +{ + "nodes": [ + { + "id": "a0b1c2d3e4f5a6b7", + "type": "text", + "x": 200, + "y": 0, + "width": 150, + "height": 60, + "text": "**Start**", + "color": "4" + }, + { + "id": "b1c2d3e4f5a6b7c8", + "type": "text", + "x": 200, + "y": 100, + "width": 150, + "height": 60, + "text": "Step 1:\nGather data" + }, + { + "id": "c2d3e4f5a6b7c8d9", + "type": "text", + "x": 200, + "y": 200, + "width": 150, + "height": 80, + "text": "**Decision**\n\nIs data valid?", + "color": "3" + }, + { + "id": "d3e4f5a6b7c8d9e0", + "type": "text", + "x": 400, + "y": 200, + "width": 150, + "height": 60, + "text": "Process data" + }, + { + "id": "e4f5a6b7c8d9e0f1", + "type": "text", + "x": 0, + "y": 200, + "width": 150, + "height": 60, + "text": "Request new data", + "color": "1" + }, + { + "id": "f5a6b7c8d9e0f1a2", + "type": "text", + "x": 400, + "y": 320, + "width": 150, + "height": 60, + "text": "**End**", + "color": "4" + } + ], + "edges": [ + { + "id": "a6b7c8d9e0f1a2b3", + "fromNode": "a0b1c2d3e4f5a6b7", + "fromSide": "bottom", + "toNode": "b1c2d3e4f5a6b7c8", + "toSide": "top" + }, + { + "id": "b7c8d9e0f1a2b3c4", + "fromNode": "b1c2d3e4f5a6b7c8", + "fromSide": "bottom", + "toNode": "c2d3e4f5a6b7c8d9", + "toSide": "top" + }, + { + "id": "c8d9e0f1a2b3c4d5", + "fromNode": "c2d3e4f5a6b7c8d9", + "fromSide": "right", + "toNode": "d3e4f5a6b7c8d9e0", + "toSide": "left", + "label": "Yes", + "color": "4" + }, + { + "id": "d9e0f1a2b3c4d5e6", + "fromNode": "c2d3e4f5a6b7c8d9", + "fromSide": "left", + "toNode": "e4f5a6b7c8d9e0f1", + "toSide": "right", + "label": "No", + "color": "1" + }, + { + "id": "e0f1a2b3c4d5e6f7", + "fromNode": "e4f5a6b7c8d9e0f1", + "fromSide": "top", + "fromEnd": "none", + "toNode": "b1c2d3e4f5a6b7c8", + "toSide": "left", + "toEnd": "arrow" + }, + { + "id": "f1a2b3c4d5e6f7a8", + "fromNode": "d3e4f5a6b7c8d9e0", + "fromSide": "bottom", + "toNode": "f5a6b7c8d9e0f1a2", + "toSide": "top" + } + ] +} +``` diff --git a/skills/obsidian-bases/SKILL.md b/skills/obsidian-bases/SKILL.md index 4b27f7e..7e84aa4 100644 --- a/skills/obsidian-bases/SKILL.md +++ b/skills/obsidian-bases/SKILL.md @@ -5,17 +5,18 @@ description: Create and edit Obsidian Bases (.base files) with views, filters, f # Obsidian Bases Skill -This skill enables skills-compatible agents to create and edit valid Obsidian Bases (`.base` files) including views, filters, formulas, and all related configurations. +## Workflow -## Overview +1. **Create the file**: Create a `.base` file in the vault with valid YAML content +2. **Define scope**: Add `filters` to select which notes appear (by tag, folder, property, or date) +3. **Add formulas** (optional): Define computed properties in the `formulas` section +4. **Configure views**: Add one or more views (`table`, `cards`, `list`, or `map`) with `order` specifying which properties to display +5. **Validate**: Verify the file is valid YAML with no syntax errors. Check that all referenced properties and formulas exist. Common issues: unquoted strings containing special YAML characters, mismatched quotes in formula expressions, referencing `formula.X` without defining `X` in `formulas` +6. **Test in Obsidian**: Open the `.base` file in Obsidian to confirm the view renders correctly. If it shows a YAML error, check quoting rules below -Obsidian Bases are YAML-based files that define dynamic views of notes in an Obsidian vault. A Base file can contain multiple views, global filters, formulas, property configurations, and custom summaries. +## Schema -## File Format - -Base files use the `.base` extension and contain valid YAML. They can also be embedded in Markdown code blocks. - -## Complete Schema +Base files use the `.base` extension and contain valid YAML. ```yaml # Global filters apply to ALL views in the base @@ -171,71 +172,33 @@ formulas: days_until_due: 'if(due_date, (date(due_date) - today()).days, "")' ``` -## Functions Reference +## Key Functions -### Global Functions +Most commonly used functions. For the complete reference of all types (Date, String, Number, List, File, Link, Object, RegExp), see [FUNCTIONS_REFERENCE.md](references/FUNCTIONS_REFERENCE.md). | Function | Signature | Description | |----------|-----------|-------------| -| `date()` | `date(string): date` | Parse string to date. Format: `YYYY-MM-DD HH:mm:ss` | -| `duration()` | `duration(string): duration` | Parse duration string | +| `date()` | `date(string): date` | Parse string to date (`YYYY-MM-DD HH:mm:ss`) | | `now()` | `now(): date` | Current date and time | | `today()` | `today(): date` | Current date (time = 00:00:00) | | `if()` | `if(condition, trueResult, falseResult?)` | Conditional | -| `min()` | `min(n1, n2, ...): number` | Smallest number | -| `max()` | `max(n1, n2, ...): number` | Largest number | -| `number()` | `number(any): number` | Convert to number | -| `link()` | `link(path, display?): Link` | Create a link | -| `list()` | `list(element): List` | Wrap in list if not already | +| `duration()` | `duration(string): duration` | Parse duration string | | `file()` | `file(path): file` | Get file object | -| `image()` | `image(path): image` | Create image for rendering | -| `icon()` | `icon(name): icon` | Lucide icon by name | -| `html()` | `html(string): html` | Render as HTML | -| `escapeHTML()` | `escapeHTML(string): string` | Escape HTML characters | - -### Any Type Functions - -| Function | Signature | Description | -|----------|-----------|-------------| -| `isTruthy()` | `any.isTruthy(): boolean` | Coerce to boolean | -| `isType()` | `any.isType(type): boolean` | Check type | -| `toString()` | `any.toString(): string` | Convert to string | - -### Date Functions & Fields - -**Fields:** `date.year`, `date.month`, `date.day`, `date.hour`, `date.minute`, `date.second`, `date.millisecond` - -| Function | Signature | Description | -|----------|-----------|-------------| -| `date()` | `date.date(): date` | Remove time portion | -| `format()` | `date.format(string): string` | Format with Moment.js pattern | -| `time()` | `date.time(): string` | Get time as string | -| `relative()` | `date.relative(): string` | Human-readable relative time | -| `isEmpty()` | `date.isEmpty(): boolean` | Always false for dates | +| `link()` | `link(path, display?): Link` | Create a link | ### Duration Type -When subtracting two dates, the result is a **Duration** type (not a number). Duration has its own properties and methods. +When subtracting two dates, the result is a **Duration** type (not a number). -**Duration Fields:** -| Field | Type | Description | -|-------|------|-------------| -| `duration.days` | Number | Total days in duration | -| `duration.hours` | Number | Total hours in duration | -| `duration.minutes` | Number | Total minutes in duration | -| `duration.seconds` | Number | Total seconds in duration | -| `duration.milliseconds` | Number | Total milliseconds in duration | +**Duration Fields:** `duration.days`, `duration.hours`, `duration.minutes`, `duration.seconds`, `duration.milliseconds` -**IMPORTANT:** Duration does NOT support `.round()`, `.floor()`, `.ceil()` directly. You must access a numeric field first (like `.days`), then apply number functions. +**IMPORTANT:** Duration does NOT support `.round()`, `.floor()`, `.ceil()` directly. Access a numeric field first (like `.days`), then apply number functions. ```yaml # CORRECT: Calculate days between dates "(date(due_date) - today()).days" # Returns number of days "(now() - file.ctime).days" # Days since created - -# CORRECT: Round the numeric result if needed "(date(due_date) - today()).days.round(0)" # Rounded days -"(now() - file.ctime).hours.round(0)" # Rounded hours # WRONG - will cause error: # "((date(due) - today()) / 86400000).round(0)" # Duration doesn't support division then round @@ -246,105 +209,12 @@ When subtracting two dates, the result is a **Duration** type (not a number). Du ```yaml # Duration units: y/year/years, M/month/months, d/day/days, # w/week/weeks, h/hour/hours, m/minute/minutes, s/second/seconds - -# Add/subtract durations -"date + \"1M\"" # Add 1 month -"date - \"2h\"" # Subtract 2 hours "now() + \"1 day\"" # Tomorrow "today() + \"7d\"" # A week from today - -# Subtract dates returns Duration type -"now() - file.ctime" # Returns Duration -"(now() - file.ctime).days" # Get days as number -"(now() - file.ctime).hours" # Get hours as number - -# Complex duration arithmetic -"now() + (duration('1d') * 2)" +"now() - file.ctime" # Returns Duration +"(now() - file.ctime).days" # Get days as number ``` -### String Functions - -**Field:** `string.length` - -| Function | Signature | Description | -|----------|-----------|-------------| -| `contains()` | `string.contains(value): boolean` | Check substring | -| `containsAll()` | `string.containsAll(...values): boolean` | All substrings present | -| `containsAny()` | `string.containsAny(...values): boolean` | Any substring present | -| `startsWith()` | `string.startsWith(query): boolean` | Starts with query | -| `endsWith()` | `string.endsWith(query): boolean` | Ends with query | -| `isEmpty()` | `string.isEmpty(): boolean` | Empty or not present | -| `lower()` | `string.lower(): string` | To lowercase | -| `title()` | `string.title(): string` | To Title Case | -| `trim()` | `string.trim(): string` | Remove whitespace | -| `replace()` | `string.replace(pattern, replacement): string` | Replace pattern | -| `repeat()` | `string.repeat(count): string` | Repeat string | -| `reverse()` | `string.reverse(): string` | Reverse string | -| `slice()` | `string.slice(start, end?): string` | Substring | -| `split()` | `string.split(separator, n?): list` | Split to list | - -### Number Functions - -| Function | Signature | Description | -|----------|-----------|-------------| -| `abs()` | `number.abs(): number` | Absolute value | -| `ceil()` | `number.ceil(): number` | Round up | -| `floor()` | `number.floor(): number` | Round down | -| `round()` | `number.round(digits?): number` | Round to digits | -| `toFixed()` | `number.toFixed(precision): string` | Fixed-point notation | -| `isEmpty()` | `number.isEmpty(): boolean` | Not present | - -### List Functions - -**Field:** `list.length` - -| Function | Signature | Description | -|----------|-----------|-------------| -| `contains()` | `list.contains(value): boolean` | Element exists | -| `containsAll()` | `list.containsAll(...values): boolean` | All elements exist | -| `containsAny()` | `list.containsAny(...values): boolean` | Any element exists | -| `filter()` | `list.filter(expression): list` | Filter by condition (uses `value`, `index`) | -| `map()` | `list.map(expression): list` | Transform elements (uses `value`, `index`) | -| `reduce()` | `list.reduce(expression, initial): any` | Reduce to single value (uses `value`, `index`, `acc`) | -| `flat()` | `list.flat(): list` | Flatten nested lists | -| `join()` | `list.join(separator): string` | Join to string | -| `reverse()` | `list.reverse(): list` | Reverse order | -| `slice()` | `list.slice(start, end?): list` | Sublist | -| `sort()` | `list.sort(): list` | Sort ascending | -| `unique()` | `list.unique(): list` | Remove duplicates | -| `isEmpty()` | `list.isEmpty(): boolean` | No elements | - -### File Functions - -| Function | Signature | Description | -|----------|-----------|-------------| -| `asLink()` | `file.asLink(display?): Link` | Convert to link | -| `hasLink()` | `file.hasLink(otherFile): boolean` | Has link to file | -| `hasTag()` | `file.hasTag(...tags): boolean` | Has any of the tags | -| `hasProperty()` | `file.hasProperty(name): boolean` | Has property | -| `inFolder()` | `file.inFolder(folder): boolean` | In folder or subfolder | - -### Link Functions - -| Function | Signature | Description | -|----------|-----------|-------------| -| `asFile()` | `link.asFile(): file` | Get file object | -| `linksTo()` | `link.linksTo(file): boolean` | Links to file | - -### Object Functions - -| Function | Signature | Description | -|----------|-----------|-------------| -| `isEmpty()` | `object.isEmpty(): boolean` | No properties | -| `keys()` | `object.keys(): list` | List of keys | -| `values()` | `object.values(): list` | List of values | - -### Regular Expression Functions - -| Function | Signature | Description | -|----------|-----------|-------------| -| `matches()` | `regexp.matches(string): boolean` | Test if matches | - ## View Types ### Table View @@ -512,48 +382,6 @@ views: - formula.reading_time ``` -### Project Notes Base - -```yaml -filters: - and: - - file.inFolder("Projects") - - 'file.ext == "md"' - -formulas: - last_updated: 'file.mtime.relative()' - link_count: 'file.links.length' - -summaries: - avgLinks: 'values.filter(value.isType("number")).mean().round(1)' - -properties: - formula.last_updated: - displayName: "Updated" - formula.link_count: - displayName: "Links" - -views: - - type: table - name: "All Projects" - order: - - file.name - - status - - formula.last_updated - - formula.link_count - summaries: - formula.link_count: avgLinks - groupBy: - property: status - direction: ASC - - - type: list - name: "Quick List" - order: - - file.name - - status -``` - ### Daily Notes Index ```yaml @@ -600,47 +428,64 @@ Embed in Markdown files: - Use double quotes for simple strings: `"My View Name"` - Escape nested quotes properly in complex expressions -## Common Patterns +## Troubleshooting + +### YAML Syntax Errors + +**Unquoted special characters**: Strings containing `:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` `` must be quoted. -### Filter by Tag ```yaml -filters: - and: - - file.hasTag("project") +# WRONG - colon in unquoted string +displayName: Status: Active + +# CORRECT +displayName: "Status: Active" ``` -### Filter by Folder +**Mismatched quotes in formulas**: When a formula contains double quotes, wrap the entire formula in single quotes. + ```yaml -filters: - and: - - file.inFolder("Notes") +# WRONG - double quotes inside double quotes +formulas: + label: "if(done, "Yes", "No")" + +# CORRECT - single quotes wrapping double quotes +formulas: + label: 'if(done, "Yes", "No")' ``` -### Filter by Date Range +### Common Formula Errors + +**Duration math without field access**: Subtracting dates returns a Duration, not a number. Always access `.days`, `.hours`, etc. + ```yaml -filters: - and: - - 'file.mtime > now() - "7d"' +# WRONG - Duration is not a number +"(now() - file.ctime).round(0)" + +# CORRECT - access .days first, then round +"(now() - file.ctime).days.round(0)" ``` -### Filter by Property Value +**Missing null checks**: Properties may not exist on all notes. Use `if()` to guard. + ```yaml -filters: - and: - - 'status == "active"' - - 'priority >= 3' +# WRONG - crashes if due_date is empty +"(date(due_date) - today()).days" + +# CORRECT - guard with if() +'if(due_date, (date(due_date) - today()).days, "")' ``` -### Combine Multiple Conditions +**Referencing undefined formulas**: Ensure every `formula.X` in `order` or `properties` has a matching entry in `formulas`. + ```yaml -filters: - or: - - and: - - file.hasTag("important") - - 'status != "done"' - - and: - - 'priority == 1' - - 'due != ""' +# This will fail silently if 'total' is not defined in formulas +order: + - formula.total + +# Fix: define it +formulas: + total: "price * quantity" ``` ## References @@ -649,3 +494,4 @@ filters: - [Functions](https://help.obsidian.md/bases/functions) - [Views](https://help.obsidian.md/bases/views) - [Formulas](https://help.obsidian.md/formulas) +- [Complete Functions Reference](references/FUNCTIONS_REFERENCE.md) diff --git a/skills/obsidian-bases/references/FUNCTIONS_REFERENCE.md b/skills/obsidian-bases/references/FUNCTIONS_REFERENCE.md new file mode 100644 index 0000000..047888d --- /dev/null +++ b/skills/obsidian-bases/references/FUNCTIONS_REFERENCE.md @@ -0,0 +1,173 @@ +# Functions Reference + +## Global Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `date()` | `date(string): date` | Parse string to date. Format: `YYYY-MM-DD HH:mm:ss` | +| `duration()` | `duration(string): duration` | Parse duration string | +| `now()` | `now(): date` | Current date and time | +| `today()` | `today(): date` | Current date (time = 00:00:00) | +| `if()` | `if(condition, trueResult, falseResult?)` | Conditional | +| `min()` | `min(n1, n2, ...): number` | Smallest number | +| `max()` | `max(n1, n2, ...): number` | Largest number | +| `number()` | `number(any): number` | Convert to number | +| `link()` | `link(path, display?): Link` | Create a link | +| `list()` | `list(element): List` | Wrap in list if not already | +| `file()` | `file(path): file` | Get file object | +| `image()` | `image(path): image` | Create image for rendering | +| `icon()` | `icon(name): icon` | Lucide icon by name | +| `html()` | `html(string): html` | Render as HTML | +| `escapeHTML()` | `escapeHTML(string): string` | Escape HTML characters | + +## Any Type Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `isTruthy()` | `any.isTruthy(): boolean` | Coerce to boolean | +| `isType()` | `any.isType(type): boolean` | Check type | +| `toString()` | `any.toString(): string` | Convert to string | + +## Date Functions & Fields + +**Fields:** `date.year`, `date.month`, `date.day`, `date.hour`, `date.minute`, `date.second`, `date.millisecond` + +| Function | Signature | Description | +|----------|-----------|-------------| +| `date()` | `date.date(): date` | Remove time portion | +| `format()` | `date.format(string): string` | Format with Moment.js pattern | +| `time()` | `date.time(): string` | Get time as string | +| `relative()` | `date.relative(): string` | Human-readable relative time | +| `isEmpty()` | `date.isEmpty(): boolean` | Always false for dates | + +## Duration Type + +When subtracting two dates, the result is a **Duration** type (not a number). Duration has its own properties and methods. + +**Duration Fields:** +| Field | Type | Description | +|-------|------|-------------| +| `duration.days` | Number | Total days in duration | +| `duration.hours` | Number | Total hours in duration | +| `duration.minutes` | Number | Total minutes in duration | +| `duration.seconds` | Number | Total seconds in duration | +| `duration.milliseconds` | Number | Total milliseconds in duration | + +**IMPORTANT:** Duration does NOT support `.round()`, `.floor()`, `.ceil()` directly. You must access a numeric field first (like `.days`), then apply number functions. + +```yaml +# CORRECT: Calculate days between dates +"(date(due_date) - today()).days" # Returns number of days +"(now() - file.ctime).days" # Days since created + +# CORRECT: Round the numeric result if needed +"(date(due_date) - today()).days.round(0)" # Rounded days +"(now() - file.ctime).hours.round(0)" # Rounded hours + +# WRONG - will cause error: +# "((date(due) - today()) / 86400000).round(0)" # Duration doesn't support division then round +``` + +## Date Arithmetic + +```yaml +# Duration units: y/year/years, M/month/months, d/day/days, +# w/week/weeks, h/hour/hours, m/minute/minutes, s/second/seconds + +# Add/subtract durations +"date + \"1M\"" # Add 1 month +"date - \"2h\"" # Subtract 2 hours +"now() + \"1 day\"" # Tomorrow +"today() + \"7d\"" # A week from today + +# Subtract dates returns Duration type +"now() - file.ctime" # Returns Duration +"(now() - file.ctime).days" # Get days as number +"(now() - file.ctime).hours" # Get hours as number + +# Complex duration arithmetic +"now() + (duration('1d') * 2)" +``` + +## String Functions + +**Field:** `string.length` + +| Function | Signature | Description | +|----------|-----------|-------------| +| `contains()` | `string.contains(value): boolean` | Check substring | +| `containsAll()` | `string.containsAll(...values): boolean` | All substrings present | +| `containsAny()` | `string.containsAny(...values): boolean` | Any substring present | +| `startsWith()` | `string.startsWith(query): boolean` | Starts with query | +| `endsWith()` | `string.endsWith(query): boolean` | Ends with query | +| `isEmpty()` | `string.isEmpty(): boolean` | Empty or not present | +| `lower()` | `string.lower(): string` | To lowercase | +| `title()` | `string.title(): string` | To Title Case | +| `trim()` | `string.trim(): string` | Remove whitespace | +| `replace()` | `string.replace(pattern, replacement): string` | Replace pattern | +| `repeat()` | `string.repeat(count): string` | Repeat string | +| `reverse()` | `string.reverse(): string` | Reverse string | +| `slice()` | `string.slice(start, end?): string` | Substring | +| `split()` | `string.split(separator, n?): list` | Split to list | + +## Number Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `abs()` | `number.abs(): number` | Absolute value | +| `ceil()` | `number.ceil(): number` | Round up | +| `floor()` | `number.floor(): number` | Round down | +| `round()` | `number.round(digits?): number` | Round to digits | +| `toFixed()` | `number.toFixed(precision): string` | Fixed-point notation | +| `isEmpty()` | `number.isEmpty(): boolean` | Not present | + +## List Functions + +**Field:** `list.length` + +| Function | Signature | Description | +|----------|-----------|-------------| +| `contains()` | `list.contains(value): boolean` | Element exists | +| `containsAll()` | `list.containsAll(...values): boolean` | All elements exist | +| `containsAny()` | `list.containsAny(...values): boolean` | Any element exists | +| `filter()` | `list.filter(expression): list` | Filter by condition (uses `value`, `index`) | +| `map()` | `list.map(expression): list` | Transform elements (uses `value`, `index`) | +| `reduce()` | `list.reduce(expression, initial): any` | Reduce to single value (uses `value`, `index`, `acc`) | +| `flat()` | `list.flat(): list` | Flatten nested lists | +| `join()` | `list.join(separator): string` | Join to string | +| `reverse()` | `list.reverse(): list` | Reverse order | +| `slice()` | `list.slice(start, end?): list` | Sublist | +| `sort()` | `list.sort(): list` | Sort ascending | +| `unique()` | `list.unique(): list` | Remove duplicates | +| `isEmpty()` | `list.isEmpty(): boolean` | No elements | + +## File Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `asLink()` | `file.asLink(display?): Link` | Convert to link | +| `hasLink()` | `file.hasLink(otherFile): boolean` | Has link to file | +| `hasTag()` | `file.hasTag(...tags): boolean` | Has any of the tags | +| `hasProperty()` | `file.hasProperty(name): boolean` | Has property | +| `inFolder()` | `file.inFolder(folder): boolean` | In folder or subfolder | + +## Link Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `asFile()` | `link.asFile(): file` | Get file object | +| `linksTo()` | `link.linksTo(file): boolean` | Links to file | + +## Object Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `isEmpty()` | `object.isEmpty(): boolean` | No properties | +| `keys()` | `object.keys(): list` | List of keys | +| `values()` | `object.values(): list` | List of values | + +## Regular Expression Functions + +| Function | Signature | Description | +|----------|-----------|-------------| +| `matches()` | `regexp.matches(string): boolean` | Test if matches | diff --git a/skills/obsidian-cli/SKILL.md b/skills/obsidian-cli/SKILL.md index 4cd0035..0046c45 100644 --- a/skills/obsidian-cli/SKILL.md +++ b/skills/obsidian-cli/SKILL.md @@ -61,11 +61,29 @@ Use `--copy` on any command to copy output to clipboard. Use `silent` to prevent ## Plugin development -Reload a plugin after code changes — essential for the develop/test cycle: +### Develop/test cycle -```bash -obsidian plugin:reload id=my-plugin -``` +After making code changes to a plugin or theme, follow this workflow: + +1. **Reload** the plugin to pick up changes: + ```bash + obsidian plugin:reload id=my-plugin + ``` +2. **Check for errors** — if errors appear, fix and repeat from step 1: + ```bash + obsidian dev:errors + ``` +3. **Verify visually** with a screenshot or DOM inspection: + ```bash + obsidian dev:screenshot path=screenshot.png + obsidian dev:dom selector=".workspace-leaf" text + ``` +4. **Check console output** for warnings or unexpected logs: + ```bash + obsidian dev:console level=error + ``` + +### Additional developer commands Run JavaScript in the app context: @@ -73,24 +91,9 @@ Run JavaScript in the app context: obsidian eval code="app.vault.getFiles().length" ``` -Check for errors and console output: +Inspect CSS values: ```bash -obsidian dev:errors -obsidian dev:console -obsidian dev:console level=error -``` - -Take a screenshot for visual testing: - -```bash -obsidian dev:screenshot path=screenshot.png -``` - -Inspect DOM and CSS: - -```bash -obsidian dev:dom selector=".workspace-leaf" text obsidian dev:css selector=".workspace-leaf" prop=background-color ``` diff --git a/skills/obsidian-markdown/SKILL.md b/skills/obsidian-markdown/SKILL.md index 2fb45c5..bca51a4 100644 --- a/skills/obsidian-markdown/SKILL.md +++ b/skills/obsidian-markdown/SKILL.md @@ -5,390 +5,130 @@ description: Create and edit Obsidian Flavored Markdown with wikilinks, embeds, # Obsidian Flavored Markdown Skill -This skill enables skills-compatible agents to create and edit valid Obsidian Flavored Markdown, including all Obsidian-specific syntax extensions. +Create and edit valid Obsidian Flavored Markdown. Obsidian extends CommonMark and GFM with wikilinks, embeds, callouts, properties, comments, and other syntax. This skill covers only Obsidian-specific extensions -- standard Markdown (headings, bold, italic, lists, quotes, code blocks, tables) is assumed knowledge. -## Overview +## Workflow: Creating an Obsidian Note -Obsidian uses a combination of Markdown flavors: -- [CommonMark](https://commonmark.org/) -- [GitHub Flavored Markdown](https://github.github.com/gfm/) -- [LaTeX](https://www.latex-project.org/) for math -- Obsidian-specific extensions (wikilinks, callouts, embeds, etc.) +1. **Add frontmatter** with properties (title, tags, aliases) at the top of the file. See [PROPERTIES.md](references/PROPERTIES.md) for all property types. +2. **Write content** using standard Markdown for structure, plus Obsidian-specific syntax below. +3. **Link related notes** using wikilinks (`[[Note]]`) for internal vault connections, or standard Markdown links for external URLs. +4. **Embed content** from other notes, images, or PDFs using the `![[embed]]` syntax. See [EMBEDS.md](references/EMBEDS.md) for all embed types. +5. **Add callouts** for highlighted information using `> [!type]` syntax. See [CALLOUTS.md](references/CALLOUTS.md) for all callout types. +6. **Verify** the note renders correctly in Obsidian's reading view. -## Basic Formatting - -### Paragraphs and Line Breaks - -```markdown -This is a paragraph. - -This is another paragraph (blank line between creates separate paragraphs). - -For a line break within a paragraph, add two spaces at the end -or use Shift+Enter. -``` - -### Headings - -```markdown -# Heading 1 -## Heading 2 -### Heading 3 -#### Heading 4 -##### Heading 5 -###### Heading 6 -``` - -### Text Formatting - -| Style | Syntax | Example | Output | -|-------|--------|---------|--------| -| Bold | `**text**` or `__text__` | `**Bold**` | **Bold** | -| Italic | `*text*` or `_text_` | `*Italic*` | *Italic* | -| Bold + Italic | `***text***` | `***Both***` | ***Both*** | -| Strikethrough | `~~text~~` | `~~Striked~~` | ~~Striked~~ | -| Highlight | `==text==` | `==Highlighted==` | ==Highlighted== | -| Inline code | `` `code` `` | `` `code` `` | `code` | - -### Escaping Formatting - -Use backslash to escape special characters: -```markdown -\*This won't be italic\* -\#This won't be a heading -1\. This won't be a list item -``` - -Common characters to escape: `\*`, `\_`, `\#`, `` \` ``, `\|`, `\~` +> When choosing between wikilinks and Markdown links: use `[[wikilinks]]` for notes within the vault (Obsidian tracks renames automatically) and `[text](url)` for external URLs only. ## Internal Links (Wikilinks) -### Basic Links - ```markdown -[[Note Name]] -[[Note Name.md]] -[[Note Name|Display Text]] +[[Note Name]] Link to note +[[Note Name|Display Text]] Custom display text +[[Note Name#Heading]] Link to heading +[[Note Name#^block-id]] Link to block +[[#Heading in same note]] Same-note heading link ``` -### Link to Headings +Define a block ID by appending `^block-id` to any paragraph: ```markdown -[[Note Name#Heading]] -[[Note Name#Heading|Custom Text]] -[[#Heading in same note]] -[[##Search all headings in vault]] +This paragraph can be linked to. ^my-block-id ``` -### Link to Blocks +For lists and quotes, place the block ID on a separate line after the block: ```markdown -[[Note Name#^block-id]] -[[Note Name#^block-id|Custom Text]] -``` - -Define a block ID by adding `^block-id` at the end of a paragraph: -```markdown -This is a paragraph that can be linked to. ^my-block-id -``` - -For lists and quotes, add the block ID on a separate line: -```markdown -> This is a quote -> With multiple lines +> A quote block ^quote-id ``` -### Search Links - -```markdown -[[##heading]] Search for headings containing "heading" -[[^^block]] Search for blocks containing "block" -``` - -## Markdown-Style Links - -```markdown -[Display Text](Note%20Name.md) -[Display Text](Note%20Name.md#Heading) -[Display Text](https://example.com) -[Note](obsidian://open?vault=VaultName&file=Note.md) -``` - -Note: Spaces must be URL-encoded as `%20` in Markdown links. - ## Embeds -### Embed Notes +Prefix any wikilink with `!` to embed its content inline: ```markdown -![[Note Name]] -![[Note Name#Heading]] -![[Note Name#^block-id]] +![[Note Name]] Embed full note +![[Note Name#Heading]] Embed section +![[image.png]] Embed image +![[image.png|300]] Embed image with width +![[document.pdf#page=3]] Embed PDF page ``` -### Embed Images - -```markdown -![[image.png]] -![[image.png|640x480]] Width x Height -![[image.png|300]] Width only (maintains aspect ratio) -``` - -### External Images - -```markdown -![Alt text](https://example.com/image.png) -![Alt text|300](https://example.com/image.png) -``` - -### Embed Audio - -```markdown -![[audio.mp3]] -![[audio.ogg]] -``` - -### Embed PDF - -```markdown -![[document.pdf]] -![[document.pdf#page=3]] -![[document.pdf#height=400]] -``` - -### Embed Lists - -```markdown -![[Note#^list-id]] -``` - -Where the list has been defined with a block ID: -```markdown -- Item 1 -- Item 2 -- Item 3 - -^list-id -``` - -### Embed Search Results - -````markdown -```query -tag:#project status:done -``` -```` +See [EMBEDS.md](references/EMBEDS.md) for audio, video, search embeds, and external images. ## Callouts -### Basic Callout - ```markdown > [!note] -> This is a note callout. +> Basic callout. -> [!info] Custom Title -> This callout has a custom title. +> [!warning] Custom Title +> Callout with a custom title. -> [!tip] Title Only -``` - -### Foldable Callouts - -```markdown > [!faq]- Collapsed by default -> This content is hidden until expanded. - -> [!faq]+ Expanded by default -> This content is visible but can be collapsed. +> Foldable callout (- collapsed, + expanded). ``` -### Nested Callouts +Common types: `note`, `tip`, `warning`, `info`, `example`, `quote`, `bug`, `danger`, `success`, `failure`, `question`, `abstract`, `todo`. + +See [CALLOUTS.md](references/CALLOUTS.md) for the full list with aliases, nesting, and custom CSS callouts. + +## Properties (Frontmatter) + +```yaml +--- +title: My Note +date: 2024-01-15 +tags: + - project + - active +aliases: + - Alternative Name +cssclasses: + - custom-class +--- +``` + +Default properties: `tags` (searchable labels), `aliases` (alternative note names for link suggestions), `cssclasses` (CSS classes for styling). + +See [PROPERTIES.md](references/PROPERTIES.md) for all property types, tag syntax rules, and advanced usage. + +## Tags ```markdown -> [!question] Outer callout -> > [!note] Inner callout -> > Nested content +#tag Inline tag +#nested/tag Nested tag with hierarchy ``` -### Supported Callout Types +Tags can contain letters, numbers (not first character), underscores, hyphens, and forward slashes. Tags can also be defined in frontmatter under the `tags` property. -| Type | Aliases | Description | -|------|---------|-------------| -| `note` | - | Blue, pencil icon | -| `abstract` | `summary`, `tldr` | Teal, clipboard icon | -| `info` | - | Blue, info icon | -| `todo` | - | Blue, checkbox icon | -| `tip` | `hint`, `important` | Cyan, flame icon | -| `success` | `check`, `done` | Green, checkmark icon | -| `question` | `help`, `faq` | Yellow, question mark | -| `warning` | `caution`, `attention` | Orange, warning icon | -| `failure` | `fail`, `missing` | Red, X icon | -| `danger` | `error` | Red, zap icon | -| `bug` | - | Red, bug icon | -| `example` | - | Purple, list icon | -| `quote` | `cite` | Gray, quote icon | - -### Custom Callouts (CSS) - -```css -.callout[data-callout="custom-type"] { - --callout-color: 255, 0, 0; - --callout-icon: lucide-alert-circle; -} -``` - -## Lists - -### Unordered Lists +## Comments ```markdown -- Item 1 -- Item 2 - - Nested item - - Another nested -- Item 3 +This is visible %%but this is hidden%% text. -* Also works with asterisks -+ Or plus signs +%% +This entire block is hidden in reading view. +%% ``` -### Ordered Lists +## Obsidian-Specific Formatting ```markdown -1. First item -2. Second item - 1. Nested numbered - 2. Another nested -3. Third item - -1) Alternative syntax -2) With parentheses -``` - -### Task Lists - -```markdown -- [ ] Incomplete task -- [x] Completed task -- [ ] Task with sub-tasks - - [ ] Subtask 1 - - [x] Subtask 2 -``` - -## Quotes - -```markdown -> This is a blockquote. -> It can span multiple lines. -> -> And include multiple paragraphs. -> -> > Nested quotes work too. -``` - -## Code - -### Inline Code - -```markdown -Use `backticks` for inline code. -Use double backticks for ``code with a ` backtick inside``. -``` - -### Code Blocks - -````markdown -``` -Plain code block -``` - -```javascript -// Syntax highlighted code block -function hello() { - console.log("Hello, world!"); -} -``` - -```python -# Python example -def greet(name): - print(f"Hello, {name}!") -``` -```` - -### Nesting Code Blocks - -Use more backticks or tildes for the outer block: - -`````markdown -````markdown -Here's how to create a code block: -```js -console.log("Hello") -``` -```` -````` - -## Tables - -```markdown -| Header 1 | Header 2 | Header 3 | -|----------|----------|----------| -| Cell 1 | Cell 2 | Cell 3 | -| Cell 4 | Cell 5 | Cell 6 | -``` - -### Alignment - -```markdown -| Left | Center | Right | -|:---------|:--------:|---------:| -| Left | Center | Right | -``` - -### Using Pipes in Tables - -Escape pipes with backslash: -```markdown -| Column 1 | Column 2 | -|----------|----------| -| [[Link\|Display]] | ![[Image\|100]] | +==Highlighted text== Highlight syntax ``` ## Math (LaTeX) -### Inline Math - ```markdown -This is inline math: $e^{i\pi} + 1 = 0$ -``` +Inline: $e^{i\pi} + 1 = 0$ -### Block Math - -```markdown +Block: $$ -\begin{vmatrix} -a & b \\ -c & d -\end{vmatrix} = ad - bc +\frac{a}{b} = c $$ ``` -### Common Math Syntax - -```markdown -$x^2$ Superscript -$x_i$ Subscript -$\frac{a}{b}$ Fraction -$\sqrt{x}$ Square root -$\sum_{i=1}^{n}$ Summation -$\int_a^b$ Integral -$\alpha, \beta$ Greek letters -``` - ## Diagrams (Mermaid) ````markdown @@ -397,147 +137,19 @@ graph TD A[Start] --> B{Decision} B -->|Yes| C[Do this] B -->|No| D[Do that] - C --> E[End] - D --> E ``` ```` -### Sequence Diagrams - -````markdown -```mermaid -sequenceDiagram - Alice->>Bob: Hello Bob - Bob-->>Alice: Hi Alice -``` -```` - -### Linking in Diagrams - -````markdown -```mermaid -graph TD - A[Biology] - B[Chemistry] - A --> B - class A,B internal-link; -``` -```` +To link Mermaid nodes to Obsidian notes, add `class NodeName internal-link;`. ## Footnotes ```markdown -This sentence has a footnote[^1]. +Text with a footnote[^1]. -[^1]: This is the footnote content. +[^1]: Footnote content. -You can also use named footnotes[^note]. - -[^note]: Named footnotes still appear as numbers. - -Inline footnotes are also supported.^[This is an inline footnote.] -``` - -## Comments - -```markdown -This is visible %%but this is hidden%% text. - -%% -This entire block is hidden. -It won't appear in reading view. -%% -``` - -## Horizontal Rules - -```markdown ---- -*** -___ -- - - -* * * -``` - -## Properties (Frontmatter) - -Properties use YAML frontmatter at the start of a note: - -```yaml ---- -title: My Note Title -date: 2024-01-15 -tags: - - project - - important -aliases: - - My Note - - Alternative Name -cssclasses: - - custom-class -status: in-progress -rating: 4.5 -completed: false -due: 2024-02-01T14:30:00 ---- -``` - -### Property Types - -| Type | Example | -|------|---------| -| Text | `title: My Title` | -| Number | `rating: 4.5` | -| Checkbox | `completed: true` | -| Date | `date: 2024-01-15` | -| Date & Time | `due: 2024-01-15T14:30:00` | -| List | `tags: [one, two]` or YAML list | -| Links | `related: "[[Other Note]]"` | - -### Default Properties - -- `tags` - Note tags -- `aliases` - Alternative names for the note -- `cssclasses` - CSS classes applied to the note - -## Tags - -```markdown -#tag -#nested/tag -#tag-with-dashes -#tag_with_underscores - -In frontmatter: ---- -tags: - - tag1 - - nested/tag2 ---- -``` - -Tags can contain: -- Letters (any language) -- Numbers (not as first character) -- Underscores `_` -- Hyphens `-` -- Forward slashes `/` (for nesting) - -## HTML Content - -Obsidian supports HTML within Markdown: - -```markdown -
- Colored text -
- -
- Click to expand - Hidden content here. -
- -Ctrl + C +Inline footnote.^[This is inline.] ``` ## Complete Example @@ -550,13 +162,10 @@ tags: - project - active status: in-progress -priority: high --- # Project Alpha -## Overview - This project aims to [[improve workflow]] using modern techniques. > [!important] Key Deadline @@ -565,54 +174,21 @@ This project aims to [[improve workflow]] using modern techniques. ## Tasks - [x] Initial planning -- [x] Resource allocation - [ ] Development phase - [ ] Backend implementation - [ ] Frontend design -- [ ] Testing -- [ ] Deployment -## Technical Notes +## Notes -The main algorithm uses the formula $O(n \log n)$ for sorting. +The algorithm uses $O(n \log n)$ sorting. See [[Algorithm Notes#Sorting]] for details. -```python -def process_data(items): - return sorted(items, key=lambda x: x.priority) -``` +![[Architecture Diagram.png|600]] -## Architecture - -```mermaid -graph LR - A[Input] --> B[Process] - B --> C[Output] - B --> D[Cache] -``` - -## Related Documents - -- ![[Meeting Notes 2024-01-10#Decisions]] -- [[Budget Allocation|Budget]] -- [[Team Members]] - -## References - -For more details, see the official documentation[^1]. - -[^1]: https://example.com/docs - -%% -Internal notes: -- Review with team on Friday -- Consider alternative approaches -%% +Reviewed in [[Meeting Notes 2024-01-10#Decisions]]. ```` ## References -- [Basic formatting syntax](https://help.obsidian.md/syntax) -- [Advanced formatting syntax](https://help.obsidian.md/advanced-syntax) - [Obsidian Flavored Markdown](https://help.obsidian.md/obsidian-flavored-markdown) - [Internal links](https://help.obsidian.md/links) - [Embed files](https://help.obsidian.md/embeds) diff --git a/skills/obsidian-markdown/references/CALLOUTS.md b/skills/obsidian-markdown/references/CALLOUTS.md new file mode 100644 index 0000000..c086824 --- /dev/null +++ b/skills/obsidian-markdown/references/CALLOUTS.md @@ -0,0 +1,58 @@ +# Callouts Reference + +## Basic Callout + +```markdown +> [!note] +> This is a note callout. + +> [!info] Custom Title +> This callout has a custom title. + +> [!tip] Title Only +``` + +## Foldable Callouts + +```markdown +> [!faq]- Collapsed by default +> This content is hidden until expanded. + +> [!faq]+ Expanded by default +> This content is visible but can be collapsed. +``` + +## Nested Callouts + +```markdown +> [!question] Outer callout +> > [!note] Inner callout +> > Nested content +``` + +## Supported Callout Types + +| Type | Aliases | Color / Icon | +|------|---------|-------------| +| `note` | - | Blue, pencil | +| `abstract` | `summary`, `tldr` | Teal, clipboard | +| `info` | - | Blue, info | +| `todo` | - | Blue, checkbox | +| `tip` | `hint`, `important` | Cyan, flame | +| `success` | `check`, `done` | Green, checkmark | +| `question` | `help`, `faq` | Yellow, question mark | +| `warning` | `caution`, `attention` | Orange, warning | +| `failure` | `fail`, `missing` | Red, X | +| `danger` | `error` | Red, zap | +| `bug` | - | Red, bug | +| `example` | - | Purple, list | +| `quote` | `cite` | Gray, quote | + +## Custom Callouts (CSS) + +```css +.callout[data-callout="custom-type"] { + --callout-color: 255, 0, 0; + --callout-icon: lucide-alert-circle; +} +``` diff --git a/skills/obsidian-markdown/references/EMBEDS.md b/skills/obsidian-markdown/references/EMBEDS.md new file mode 100644 index 0000000..14a8989 --- /dev/null +++ b/skills/obsidian-markdown/references/EMBEDS.md @@ -0,0 +1,63 @@ +# Embeds Reference + +## Embed Notes + +```markdown +![[Note Name]] +![[Note Name#Heading]] +![[Note Name#^block-id]] +``` + +## Embed Images + +```markdown +![[image.png]] +![[image.png|640x480]] Width x Height +![[image.png|300]] Width only (maintains aspect ratio) +``` + +## External Images + +```markdown +![Alt text](https://example.com/image.png) +![Alt text|300](https://example.com/image.png) +``` + +## Embed Audio + +```markdown +![[audio.mp3]] +![[audio.ogg]] +``` + +## Embed PDF + +```markdown +![[document.pdf]] +![[document.pdf#page=3]] +![[document.pdf#height=400]] +``` + +## Embed Lists + +```markdown +![[Note#^list-id]] +``` + +Where the list has a block ID: + +```markdown +- Item 1 +- Item 2 +- Item 3 + +^list-id +``` + +## Embed Search Results + +````markdown +```query +tag:#project status:done +``` +```` diff --git a/skills/obsidian-markdown/references/PROPERTIES.md b/skills/obsidian-markdown/references/PROPERTIES.md new file mode 100644 index 0000000..e46a63a --- /dev/null +++ b/skills/obsidian-markdown/references/PROPERTIES.md @@ -0,0 +1,61 @@ +# Properties (Frontmatter) Reference + +Properties use YAML frontmatter at the start of a note: + +```yaml +--- +title: My Note Title +date: 2024-01-15 +tags: + - project + - important +aliases: + - My Note + - Alternative Name +cssclasses: + - custom-class +status: in-progress +rating: 4.5 +completed: false +due: 2024-02-01T14:30:00 +--- +``` + +## Property Types + +| Type | Example | +|------|---------| +| Text | `title: My Title` | +| Number | `rating: 4.5` | +| Checkbox | `completed: true` | +| Date | `date: 2024-01-15` | +| Date & Time | `due: 2024-01-15T14:30:00` | +| List | `tags: [one, two]` or YAML list | +| Links | `related: "[[Other Note]]"` | + +## Default Properties + +- `tags` - Note tags (searchable, shown in graph view) +- `aliases` - Alternative names for the note (used in link suggestions) +- `cssclasses` - CSS classes applied to the note in reading/editing view + +## Tags + +```markdown +#tag +#nested/tag +#tag-with-dashes +#tag_with_underscores +``` + +Tags can contain: letters (any language), numbers (not first character), underscores `_`, hyphens `-`, forward slashes `/` (for nesting). + +In frontmatter: + +```yaml +--- +tags: + - tag1 + - nested/tag2 +--- +```