同步最新
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
---
|
||||
title: Hermes 编排 Agent + Web 可观测性方案
|
||||
date: 2026-04-29
|
||||
tags: [Hermes, Agent编排, ttyd, tmux, 可观测性]
|
||||
---
|
||||
|
||||
# Hermes 编排 Agent + Web 可观测性方案
|
||||
|
||||
## 背景
|
||||
|
||||
Hermes 作为母 Agent 编排不同的子 Agent(Codex、Claude Code 等),但全部在后台运行,无法直观看到每个 Agent 的实时输出和进度。需要将后台 tmux session 暴露到 Web 前端观察。
|
||||
|
||||
## 方案:ttyd + tmux
|
||||
|
||||
**ttyd**(GitHub 31k+ stars)可以把任意终端程序变成 Web 页面,通过 WebSocket 实时推流。
|
||||
|
||||
### 架构
|
||||
|
||||
```
|
||||
Hermes (编排大脑)
|
||||
│
|
||||
├─ tmux session codex1 ─── ttyd :8080 ──► http://localhost:8080
|
||||
├─ tmux session claude1 ─── ttyd :8081 ──► http://localhost:8081
|
||||
└─ tmux session codex2 ─── ttyd :8082 ──► http://localhost:8082
|
||||
```
|
||||
|
||||
### 使用方式
|
||||
|
||||
```bash
|
||||
# 启动 Agent 的 tmux session
|
||||
tmux new-session -d -s codex1 -x 140 -y 40
|
||||
tmux new-session -d -s claude1 -x 140 -y 40
|
||||
|
||||
# ttyd 暴露每个 session 到 web
|
||||
ttyd -p 8080 tmux attach -t codex1 &
|
||||
ttyd -p 8081 tmux attach -t claude1 &
|
||||
|
||||
# 浏览器打开
|
||||
open http://localhost:8080 # 看 Codex
|
||||
open http://localhost:8081 # 看 Claude Code
|
||||
```
|
||||
|
||||
### 类似方案
|
||||
- **Wetty / Gotty** — 与 ttyd 类似
|
||||
- **LangFuse / AgentOps** — 专业 Agent trace 平台,但偏事后日志分析,非实时终端画面
|
||||
|
||||
### 文件握手通信
|
||||
|
||||
Agent 之间通过文件交换上下文:
|
||||
|
||||
```
|
||||
/tmp/handoff.json # Codex 输出 → Claude Code 读取
|
||||
/tmp/instruction.md # Claude Code 重注入指令 → 拉起新 Codex
|
||||
/tmp/next_prompt.txt # 下一步指令
|
||||
```
|
||||
|
||||
## 关键结论
|
||||
|
||||
1. **不要** 让 Agent 之间直接互相调用(失控)
|
||||
2. **要** 用 Hermes 作为中央编排大脑
|
||||
3. **用文件作为 Agent 间通信协议**
|
||||
4. **ttyd 解决实时观察问题**,浏览器多标签同时查看所有 Agent
|
||||
|
||||
---
|
||||
|
||||
*来源:Hermes Agent 对话 (2026-04-29)*
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
tags:
|
||||
- AI
|
||||
---
|
||||
## 理想的AI编码流程
|
||||
- 策划发起需求
|
||||
- AI理解需求
|
||||
- harness工程执行
|
||||
- AI Review
|
||||
- 是否与需求对齐
|
||||
- 六大基本原则
|
||||
- hook发起PR
|
||||
- 经过CI
|
||||
- 编译门禁
|
||||
- 核心流程门禁
|
||||
- AirTest跑最主要的流程
|
||||
- 模块单例测试门禁
|
||||
- 异步与状态门禁
|
||||
|
||||
### 可验证是AI提高代码正确率的关键
|
||||
- AirTest编写最主要的几条流程作为验证
|
||||
- 是否可以正确投掷
|
||||
- 进行一轮跑测
|
||||
- error日志
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
###
|
||||
根据[[护栏与AI]]的流程,我还需要做
|
||||
- 云端部署harness
|
||||
- 围栏的搭建
|
||||
- AirTest的案例编写
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
title: TMP字体Atlas内存优化实战
|
||||
source: https://mp.weixin.qq.com/s/iPSxQvg-riGh66efHlSyLA
|
||||
author: 侑虎科技
|
||||
date: 2026-05-06
|
||||
tags: [Unity, TMP, 内存优化, Atlas, UWA]
|
||||
---
|
||||
|
||||
# 【厚积薄发】从64MB降至5MB,TMP字体Atlas内存优化实战
|
||||
|
||||
> 来源:UWA公众号 - 第474篇UWA技术知识分享
|
||||
|
||||
## 实战案例一:字体Atlas双实例冗余 + RW动态图集内存过高
|
||||
|
||||
**问题:** GOT Online报告显示,项目游戏字体Atlas出现双实例冗余且开启RW动态图集内存占用过高。
|
||||
|
||||
**原因:**
|
||||
- 字体Atlas实例数量为2,通常是因为该字体既在初始包中的初始场景中被引用,又在后续AssetBundle资源中重复引用
|
||||
- RW(Read/Write Enabled)开启导致CPU和GPU各存一份,内存翻倍
|
||||
|
||||
**解决方案:**
|
||||
1. 将初始场景中使用的字符单独创建对应的小字体,避免初始场景中有大图集和大字体的引用
|
||||
2. 预先收集游戏的大部分字符集,将Atlas设置为**静态图集配置**,可减少16MB占用
|
||||
3. 不在静态图集中的字符,通过TMP的**Fallback机制**,设定一个小分辨率(如512×512)的动态Atlas进行字符补充
|
||||
|
||||
## 实战案例二:TMPAsset内嵌Atlas纹理无法压缩
|
||||
|
||||
**问题:** TMPAsset内嵌Atlas纹理属于子资源,无法单独进行纹理压缩参数配置。
|
||||
|
||||
**解决方案(Editor脚本改造):**
|
||||
1. 通过Editor编辑器脚本对该纹理进行独立复制
|
||||
2. 将TMPAsset材质球关联替换为复制后的新纹理
|
||||
3. 移除原TMPAsset内的内嵌纹理
|
||||
4. 剥离后的独立纹理可自由配置压缩格式(ASTC6×6、ASTC8×8等),进一步降低内存开销
|
||||
5. 真机测试:压缩后文字美术表现无明显差异
|
||||
|
||||
## 优化前后对比
|
||||
|
||||
| 项目 | 内存 |
|
||||
|------|------|
|
||||
| 优化前 | **64MB** |
|
||||
| 优化后合计 | **4.75MB** |
|
||||
| 初始场景 512×512 静态图集 | 0.25MB |
|
||||
| 4096×4096 静态图集(ASTC8×8) | 4MB |
|
||||
| Fallback兜底纹理 512×512(Alpha8) | 0.5MB |
|
||||
|
||||
## 相关资源
|
||||
- UWA社区:community.uwa4d.com
|
||||
- UWA官网:www.uwa4d.com
|
||||
- UWA学堂:edu.uwa4d.com
|
||||
- QQ群:793972859
|
||||
@@ -1,656 +0,0 @@
|
||||
---
|
||||
name: json-canvas
|
||||
description: Create and edit JSON Canvas files (.canvas) with nodes, edges, groups, and connections. Use when working with .canvas files, creating visual canvases, mind maps, flowcharts, or when the user mentions Canvas files in Obsidian.
|
||||
---
|
||||
|
||||
# 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [],
|
||||
"edges": []
|
||||
}
|
||||
```
|
||||
|
||||
- `nodes` (optional): Array of node objects
|
||||
- `edges` (optional): Array of edge objects connecting nodes
|
||||
|
||||
## 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)
|
||||
|
||||
### 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` |
|
||||
| `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) |
|
||||
|
||||
### Text Nodes
|
||||
|
||||
Text nodes contain Markdown content.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "6f0ad84f44ce9c17",
|
||||
"type": "text",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 400,
|
||||
"height": 200,
|
||||
"text": "# Hello World\n\nThis is **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 |
|
||||
|
||||
### File Nodes
|
||||
|
||||
File nodes reference files or attachments (images, videos, PDFs, notes, etc.).
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "a1b2c3d4e5f67890",
|
||||
"type": "file",
|
||||
"x": 500,
|
||||
"y": 0,
|
||||
"width": 400,
|
||||
"height": 300,
|
||||
"file": "Attachments/diagram.png"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "b2c3d4e5f6789012",
|
||||
"type": "file",
|
||||
"x": 500,
|
||||
"y": 400,
|
||||
"width": 400,
|
||||
"height": 300,
|
||||
"file": "Notes/Project Overview.md",
|
||||
"subpath": "#Implementation"
|
||||
}
|
||||
```
|
||||
|
||||
| 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.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "c3d4e5f678901234",
|
||||
"type": "link",
|
||||
"x": 1000,
|
||||
"y": 0,
|
||||
"width": 400,
|
||||
"height": 200,
|
||||
"url": "https://obsidian.md"
|
||||
}
|
||||
```
|
||||
|
||||
| Attribute | Required | Type | Description |
|
||||
|-----------|----------|------|-------------|
|
||||
| `url` | Yes | string | External URL |
|
||||
|
||||
### Group Nodes
|
||||
|
||||
Group nodes are visual containers for organizing other nodes.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "d4e5f6789012345a",
|
||||
"type": "group",
|
||||
"x": -50,
|
||||
"y": -50,
|
||||
"width": 1000,
|
||||
"height": 600,
|
||||
"label": "Project Overview",
|
||||
"color": "4"
|
||||
}
|
||||
```
|
||||
|
||||
```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.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "f67890123456789a",
|
||||
"fromNode": "6f0ad84f44ce9c17",
|
||||
"toNode": "a1b2c3d4e5f67890"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "0123456789abcdef",
|
||||
"fromNode": "6f0ad84f44ce9c17",
|
||||
"fromSide": "right",
|
||||
"fromEnd": "none",
|
||||
"toNode": "b2c3d4e5f6789012",
|
||||
"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"
|
||||
}
|
||||
```
|
||||
|
||||
| Preset | Color |
|
||||
|--------|-------|
|
||||
| `"1"` | Red |
|
||||
| `"2"` | Orange |
|
||||
| `"3"` | Yellow |
|
||||
| `"4"` | Green |
|
||||
| `"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## ID Generation
|
||||
|
||||
Node and edge IDs must be unique strings. Obsidian generates 16-character hexadecimal IDs:
|
||||
|
||||
```json
|
||||
"id": "6f0ad84f44ce9c17"
|
||||
"id": "a3b2c1d0e9f8g7h6"
|
||||
"id": "1234567890abcdef"
|
||||
```
|
||||
|
||||
This format is a 16-character lowercase hex string (64-bit random value).
|
||||
|
||||
## 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
|
||||
|
||||
| Node Type | Suggested Width | Suggested Height |
|
||||
|-----------|-----------------|------------------|
|
||||
| Small text | 200-300 | 80-150 |
|
||||
| Medium text | 300-450 | 150-300 |
|
||||
| Large text | 400-600 | 300-500 |
|
||||
| File preview | 300-500 | 200-400 |
|
||||
| Link preview | 250-400 | 100-200 |
|
||||
| Group | Varies | Varies |
|
||||
|
||||
### Spacing
|
||||
|
||||
- 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
|
||||
|
||||
## Validation Rules
|
||||
|
||||
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
|
||||
|
||||
## References
|
||||
|
||||
- [JSON Canvas Spec 1.0](https://jsoncanvas.org/spec/1.0/)
|
||||
- [JSON Canvas GitHub](https://github.com/obsidianmd/jsoncanvas)
|
||||
@@ -1,136 +0,0 @@
|
||||
---
|
||||
name: obsidian-auto-cards
|
||||
description: 从当前笔记/文件夹自动提取知识点,在同级创建“卡片/”并生成原子卡片(可选生成卡片目录)。适用于整理 Obsidian 笔记、把长文拆成可复用知识卡。
|
||||
---
|
||||
|
||||
# Obsidian 自动卡片生成 Skill(提示词)
|
||||
|
||||
你是一个 Obsidian 笔记整理助手。你的目标是在**不破坏原笔记**的前提下,把笔记中的“知识点”抽取为可复用的**原子卡片**。
|
||||
|
||||
## 作用范围与安全边界
|
||||
|
||||
- 只在用户指定的目录下工作。
|
||||
- 只创建/更新该目录下的 `卡片/` 子文件夹内容,以及可选的 `卡片/卡片-目录.md`。
|
||||
- **不要**改动 `.obsidian/`、插件配置、工作区布局、已有 `.base` 文件(除非用户明确要求)。
|
||||
- 默认不移动/重命名原始笔记文件。
|
||||
|
||||
## 触发方式
|
||||
|
||||
当用户说以下任意一句时启用本技能:
|
||||
- “生成卡片”
|
||||
- “从这篇笔记生成卡片”
|
||||
- “整理这个文件夹,自动生成卡片”
|
||||
|
||||
## 需要的输入(尽量少问)
|
||||
|
||||
优先从上下文推断;推断失败时只问 1 个问题:
|
||||
|
||||
1) 目标:单文件还是整个文件夹?
|
||||
- 单文件:给出 `note_path`
|
||||
- 文件夹:给出 `folder_path`(递归处理其下所有 `.md`,但排除 `卡片/`)
|
||||
|
||||
可选参数(用户没说就用默认):
|
||||
- `cards_folder_name`:默认 `卡片`
|
||||
- `max_cards_per_note`:默认 `12`
|
||||
- `overwrite`:默认 `false`(若卡片已存在则跳过或仅补充“来源”)
|
||||
- `create_index`:默认 `true`
|
||||
|
||||
## 输出要求(卡片质量)
|
||||
|
||||
每张卡片必须满足:
|
||||
- **一个卡片只讲一个知识点**(原子性)。
|
||||
- **可读 + 可检索**:标题清晰;包含关键词/标签;有来源回链。
|
||||
- **可复用**:优先写成“定义 + 要点 + 示例/坑点”结构。
|
||||
- 不要把整篇文章复制进卡片;保持短小精悍。
|
||||
|
||||
## 知识点自动检索规则(从强到弱)
|
||||
|
||||
从笔记中按优先级提取候选知识点:
|
||||
1) “术语/概念定义”:形如 `X 是…`、`X:…`、`定义/概念/原理` 小节。
|
||||
2) “对比与结论”:形如 `区别/优缺点/适用场景/结论` 小节。
|
||||
3) “方法/步骤/流程”:步骤列表、流程图说明、伪代码。
|
||||
4) “坑点/注意事项”:包含 `注意/坑/陷阱/误区/常见问题` 的段落或列表项。
|
||||
5) “关键公式/参数/阈值”:数学块、关键配置项列表。
|
||||
|
||||
抽取时保留最少必要上下文(最多 5~10 行等价信息),并把原文细节留在来源笔记中。
|
||||
|
||||
## 目录与文件组织规则
|
||||
|
||||
### 卡片文件夹
|
||||
|
||||
- 对于单文件 `X/笔记.md`:在同目录 `X/卡片/` 创建卡片。
|
||||
- 对于文件夹 `X/`:在 `X/卡片/` 创建卡片;扫描 `X/` 下所有 `.md`(递归),但:
|
||||
- 排除 `X/卡片/` 下的文件
|
||||
- 排除 `*-目录.md`、`00-Home.md`(除非用户要求)
|
||||
|
||||
### 卡片命名
|
||||
|
||||
文件名优先用知识点标题:
|
||||
- `卡片/<知识点标题>.md`
|
||||
- 若同名冲突:追加来源笔记名或编号:`<标题>(<来源笔记>)` 或 `<标题>-2`
|
||||
- 过滤非法文件名字符:`\\ / : * ? \" < > |`
|
||||
|
||||
## 卡片模板(必须用这个)
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: card
|
||||
created: {{yyyy-MM-dd}}
|
||||
source: [[<来源笔记相对路径或文件名>]]
|
||||
source_section: "<可选:标题/小节>"
|
||||
tags:
|
||||
- 卡片
|
||||
- <可选:从来源 frontmatter 继承的 tag>
|
||||
---
|
||||
|
||||
## 定义
|
||||
一句话说清楚它是什么/解决什么问题。
|
||||
|
||||
## 要点
|
||||
- 关键点 1
|
||||
- 关键点 2
|
||||
|
||||
## 示例 / 用法(可选)
|
||||
最小例子、伪代码或使用场景。
|
||||
|
||||
## 注意 / 坑(可选)
|
||||
- 常见误区或边界条件。
|
||||
```
|
||||
|
||||
## 去重与更新策略(默认保守)
|
||||
|
||||
- 若 `卡片/<标题>.md` 已存在:
|
||||
- `overwrite=false`:不覆盖正文;只在末尾追加一条 `source` 或补充缺失的 frontmatter 字段(尽量不打扰用户写的内容)。
|
||||
- 用户明确说“覆盖/重生成”才允许重写。
|
||||
|
||||
## 生成卡片目录(可选,默认开)
|
||||
|
||||
在 `卡片/卡片-目录.md` 生成/更新目录:
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags:
|
||||
- 目录
|
||||
- 卡片
|
||||
---
|
||||
|
||||
# 卡片目录
|
||||
|
||||
```dataview
|
||||
LIST
|
||||
FROM "(这里放卡片文件夹路径)"
|
||||
WHERE contains(tags, "卡片")
|
||||
SORT file.name ASC
|
||||
```
|
||||
```
|
||||
|
||||
如果用户不使用 Dataview,就改成纯 wikilink 列表。
|
||||
|
||||
## 执行步骤(你必须按这个流程做)
|
||||
|
||||
1) 确定目标(单文件 or 文件夹)与 `卡片/` 路径;必要时创建文件夹。
|
||||
2) 扫描来源笔记,按“知识点检索规则”提取候选列表,并做去重。
|
||||
3) 逐个生成卡片文件(应用模板、写入来源回链与标签)。
|
||||
4) 可选:生成/更新 `卡片-目录.md`。
|
||||
5) 最终汇报:生成了多少张卡片、写到了哪些路径;若有跳过/冲突也要列出原因。
|
||||
|
||||
@@ -1,651 +0,0 @@
|
||||
---
|
||||
name: obsidian-bases
|
||||
description: Create and edit Obsidian Bases (.base files) with views, filters, formulas, and summaries. Use when working with .base files, creating database-like views of notes, or when the user mentions Bases, table views, card views, filters, or formulas in Obsidian.
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
## File Format
|
||||
|
||||
Base files use the `.base` extension and contain valid YAML. They can also be embedded in Markdown code blocks.
|
||||
|
||||
## Complete Schema
|
||||
|
||||
```yaml
|
||||
# Global filters apply to ALL views in the base
|
||||
filters:
|
||||
# Can be a single filter string
|
||||
# OR a recursive filter object with and/or/not
|
||||
and: []
|
||||
or: []
|
||||
not: []
|
||||
|
||||
# Define formula properties that can be used across all views
|
||||
formulas:
|
||||
formula_name: 'expression'
|
||||
|
||||
# Configure display names and settings for properties
|
||||
properties:
|
||||
property_name:
|
||||
displayName: "Display Name"
|
||||
formula.formula_name:
|
||||
displayName: "Formula Display Name"
|
||||
file.ext:
|
||||
displayName: "Extension"
|
||||
|
||||
# Define custom summary formulas
|
||||
summaries:
|
||||
custom_summary_name: 'values.mean().round(3)'
|
||||
|
||||
# Define one or more views
|
||||
views:
|
||||
- type: table | cards | list | map
|
||||
name: "View Name"
|
||||
limit: 10 # Optional: limit results
|
||||
groupBy: # Optional: group results
|
||||
property: property_name
|
||||
direction: ASC | DESC
|
||||
filters: # View-specific filters
|
||||
and: []
|
||||
order: # Properties to display in order
|
||||
- file.name
|
||||
- property_name
|
||||
- formula.formula_name
|
||||
summaries: # Map properties to summary formulas
|
||||
property_name: Average
|
||||
```
|
||||
|
||||
## Filter Syntax
|
||||
|
||||
Filters narrow down results. They can be applied globally or per-view.
|
||||
|
||||
### Filter Structure
|
||||
|
||||
```yaml
|
||||
# Single filter
|
||||
filters: 'status == "done"'
|
||||
|
||||
# AND - all conditions must be true
|
||||
filters:
|
||||
and:
|
||||
- 'status == "done"'
|
||||
- 'priority > 3'
|
||||
|
||||
# OR - any condition can be true
|
||||
filters:
|
||||
or:
|
||||
- 'file.hasTag("book")'
|
||||
- 'file.hasTag("article")'
|
||||
|
||||
# NOT - exclude matching items
|
||||
filters:
|
||||
not:
|
||||
- 'file.hasTag("archived")'
|
||||
|
||||
# Nested filters
|
||||
filters:
|
||||
or:
|
||||
- file.hasTag("tag")
|
||||
- and:
|
||||
- file.hasTag("book")
|
||||
- file.hasLink("Textbook")
|
||||
- not:
|
||||
- file.hasTag("book")
|
||||
- file.inFolder("Required Reading")
|
||||
```
|
||||
|
||||
### Filter Operators
|
||||
|
||||
| Operator | Description |
|
||||
|----------|-------------|
|
||||
| `==` | equals |
|
||||
| `!=` | not equal |
|
||||
| `>` | greater than |
|
||||
| `<` | less than |
|
||||
| `>=` | greater than or equal |
|
||||
| `<=` | less than or equal |
|
||||
| `&&` | logical and |
|
||||
| `\|\|` | logical or |
|
||||
| <code>!</code> | logical not |
|
||||
|
||||
## Properties
|
||||
|
||||
### Three Types of Properties
|
||||
|
||||
1. **Note properties** - From frontmatter: `note.author` or just `author`
|
||||
2. **File properties** - File metadata: `file.name`, `file.mtime`, etc.
|
||||
3. **Formula properties** - Computed values: `formula.my_formula`
|
||||
|
||||
### File Properties Reference
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `file.name` | String | File name |
|
||||
| `file.basename` | String | File name without extension |
|
||||
| `file.path` | String | Full path to file |
|
||||
| `file.folder` | String | Parent folder path |
|
||||
| `file.ext` | String | File extension |
|
||||
| `file.size` | Number | File size in bytes |
|
||||
| `file.ctime` | Date | Created time |
|
||||
| `file.mtime` | Date | Modified time |
|
||||
| `file.tags` | List | All tags in file |
|
||||
| `file.links` | List | Internal links in file |
|
||||
| `file.backlinks` | List | Files linking to this file |
|
||||
| `file.embeds` | List | Embeds in the note |
|
||||
| `file.properties` | Object | All frontmatter properties |
|
||||
|
||||
### The `this` Keyword
|
||||
|
||||
- In main content area: refers to the base file itself
|
||||
- When embedded: refers to the embedding file
|
||||
- In sidebar: refers to the active file in main content
|
||||
|
||||
## Formula Syntax
|
||||
|
||||
Formulas compute values from properties. Defined in the `formulas` section.
|
||||
|
||||
```yaml
|
||||
formulas:
|
||||
# Simple arithmetic
|
||||
total: "price * quantity"
|
||||
|
||||
# Conditional logic
|
||||
status_icon: 'if(done, "✅", "⏳")'
|
||||
|
||||
# String formatting
|
||||
formatted_price: 'if(price, price.toFixed(2) + " dollars")'
|
||||
|
||||
# Date formatting
|
||||
created: 'file.ctime.format("YYYY-MM-DD")'
|
||||
|
||||
# Calculate days since created (use .days for Duration)
|
||||
days_old: '(now() - file.ctime).days'
|
||||
|
||||
# Calculate days until due date
|
||||
days_until_due: 'if(due_date, (date(due_date) - today()).days, "")'
|
||||
```
|
||||
|
||||
## 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 |
|
||||
|
||||
## View Types
|
||||
|
||||
### Table View
|
||||
|
||||
```yaml
|
||||
views:
|
||||
- type: table
|
||||
name: "My Table"
|
||||
order:
|
||||
- file.name
|
||||
- status
|
||||
- due_date
|
||||
summaries:
|
||||
price: Sum
|
||||
count: Average
|
||||
```
|
||||
|
||||
### Cards View
|
||||
|
||||
```yaml
|
||||
views:
|
||||
- type: cards
|
||||
name: "Gallery"
|
||||
order:
|
||||
- file.name
|
||||
- cover_image
|
||||
- description
|
||||
```
|
||||
|
||||
### List View
|
||||
|
||||
```yaml
|
||||
views:
|
||||
- type: list
|
||||
name: "Simple List"
|
||||
order:
|
||||
- file.name
|
||||
- status
|
||||
```
|
||||
|
||||
### Map View
|
||||
|
||||
Requires latitude/longitude properties and the Maps community plugin.
|
||||
|
||||
```yaml
|
||||
views:
|
||||
- type: map
|
||||
name: "Locations"
|
||||
# Map-specific settings for lat/lng properties
|
||||
```
|
||||
|
||||
## Default Summary Formulas
|
||||
|
||||
| Name | Input Type | Description |
|
||||
|------|------------|-------------|
|
||||
| `Average` | Number | Mathematical mean |
|
||||
| `Min` | Number | Smallest number |
|
||||
| `Max` | Number | Largest number |
|
||||
| `Sum` | Number | Sum of all numbers |
|
||||
| `Range` | Number | Max - Min |
|
||||
| `Median` | Number | Mathematical median |
|
||||
| `Stddev` | Number | Standard deviation |
|
||||
| `Earliest` | Date | Earliest date |
|
||||
| `Latest` | Date | Latest date |
|
||||
| `Range` | Date | Latest - Earliest |
|
||||
| `Checked` | Boolean | Count of true values |
|
||||
| `Unchecked` | Boolean | Count of false values |
|
||||
| `Empty` | Any | Count of empty values |
|
||||
| `Filled` | Any | Count of non-empty values |
|
||||
| `Unique` | Any | Count of unique values |
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Task Tracker Base
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- file.hasTag("task")
|
||||
- 'file.ext == "md"'
|
||||
|
||||
formulas:
|
||||
days_until_due: 'if(due, (date(due) - today()).days, "")'
|
||||
is_overdue: 'if(due, date(due) < today() && status != "done", false)'
|
||||
priority_label: 'if(priority == 1, "🔴 High", if(priority == 2, "🟡 Medium", "🟢 Low"))'
|
||||
|
||||
properties:
|
||||
status:
|
||||
displayName: Status
|
||||
formula.days_until_due:
|
||||
displayName: "Days Until Due"
|
||||
formula.priority_label:
|
||||
displayName: Priority
|
||||
|
||||
views:
|
||||
- type: table
|
||||
name: "Active Tasks"
|
||||
filters:
|
||||
and:
|
||||
- 'status != "done"'
|
||||
order:
|
||||
- file.name
|
||||
- status
|
||||
- formula.priority_label
|
||||
- due
|
||||
- formula.days_until_due
|
||||
groupBy:
|
||||
property: status
|
||||
direction: ASC
|
||||
summaries:
|
||||
formula.days_until_due: Average
|
||||
|
||||
- type: table
|
||||
name: "Completed"
|
||||
filters:
|
||||
and:
|
||||
- 'status == "done"'
|
||||
order:
|
||||
- file.name
|
||||
- completed_date
|
||||
```
|
||||
|
||||
### Reading List Base
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
or:
|
||||
- file.hasTag("book")
|
||||
- file.hasTag("article")
|
||||
|
||||
formulas:
|
||||
reading_time: 'if(pages, (pages * 2).toString() + " min", "")'
|
||||
status_icon: 'if(status == "reading", "📖", if(status == "done", "✅", "📚"))'
|
||||
year_read: 'if(finished_date, date(finished_date).year, "")'
|
||||
|
||||
properties:
|
||||
author:
|
||||
displayName: Author
|
||||
formula.status_icon:
|
||||
displayName: ""
|
||||
formula.reading_time:
|
||||
displayName: "Est. Time"
|
||||
|
||||
views:
|
||||
- type: cards
|
||||
name: "Library"
|
||||
order:
|
||||
- cover
|
||||
- file.name
|
||||
- author
|
||||
- formula.status_icon
|
||||
filters:
|
||||
not:
|
||||
- 'status == "dropped"'
|
||||
|
||||
- type: table
|
||||
name: "Reading List"
|
||||
filters:
|
||||
and:
|
||||
- 'status == "to-read"'
|
||||
order:
|
||||
- file.name
|
||||
- author
|
||||
- pages
|
||||
- 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
|
||||
filters:
|
||||
and:
|
||||
- file.inFolder("Daily Notes")
|
||||
- '/^\d{4}-\d{2}-\d{2}$/.matches(file.basename)'
|
||||
|
||||
formulas:
|
||||
word_estimate: '(file.size / 5).round(0)'
|
||||
day_of_week: 'date(file.basename).format("dddd")'
|
||||
|
||||
properties:
|
||||
formula.day_of_week:
|
||||
displayName: "Day"
|
||||
formula.word_estimate:
|
||||
displayName: "~Words"
|
||||
|
||||
views:
|
||||
- type: table
|
||||
name: "Recent Notes"
|
||||
limit: 30
|
||||
order:
|
||||
- file.name
|
||||
- formula.day_of_week
|
||||
- formula.word_estimate
|
||||
- file.mtime
|
||||
```
|
||||
|
||||
## Embedding Bases
|
||||
|
||||
Embed in Markdown files:
|
||||
|
||||
```markdown
|
||||
![[MyBase.base]]
|
||||
|
||||
<!-- Specific view -->
|
||||
![[MyBase.base#View Name]]
|
||||
```
|
||||
|
||||
## YAML Quoting Rules
|
||||
|
||||
- Use single quotes for formulas containing double quotes: `'if(done, "Yes", "No")'`
|
||||
- Use double quotes for simple strings: `"My View Name"`
|
||||
- Escape nested quotes properly in complex expressions
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Filter by Tag
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- file.hasTag("project")
|
||||
```
|
||||
|
||||
### Filter by Folder
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- file.inFolder("Notes")
|
||||
```
|
||||
|
||||
### Filter by Date Range
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- 'file.mtime > now() - "7d"'
|
||||
```
|
||||
|
||||
### Filter by Property Value
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- 'status == "active"'
|
||||
- 'priority >= 3'
|
||||
```
|
||||
|
||||
### Combine Multiple Conditions
|
||||
```yaml
|
||||
filters:
|
||||
or:
|
||||
- and:
|
||||
- file.hasTag("important")
|
||||
- 'status != "done"'
|
||||
- and:
|
||||
- 'priority == 1'
|
||||
- 'due != ""'
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Bases Syntax](https://help.obsidian.md/bases/syntax)
|
||||
- [Functions](https://help.obsidian.md/bases/functions)
|
||||
- [Views](https://help.obsidian.md/bases/views)
|
||||
- [Formulas](https://help.obsidian.md/formulas)
|
||||
@@ -1,211 +0,0 @@
|
||||
---
|
||||
name: obsidian-canvas-creator
|
||||
description: Create Obsidian Canvas files from text content, supporting both MindMap and freeform layouts. Use this skill when users want to visualize content as an interactive canvas, create mind maps, or organize information spatially in Obsidian format.
|
||||
---
|
||||
|
||||
# Obsidian Canvas Creator
|
||||
|
||||
Transform text content into structured Obsidian Canvas files with support for MindMap and freeform layouts.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- User requests to create a canvas, mind map, or visual diagram from text
|
||||
- User wants to organize information spatially
|
||||
- User mentions "Obsidian Canvas" or similar visualization tools
|
||||
- Converting structured content (articles, notes, outlines) into visual format
|
||||
|
||||
## Core Workflow
|
||||
|
||||
### 1. Analyze Content
|
||||
|
||||
Read and understand the input content:
|
||||
- Identify main topics and hierarchical relationships
|
||||
- Extract key points, facts, and supporting details
|
||||
- Note any existing structure (headings, lists, sections)
|
||||
|
||||
### 2. Determine Layout Type
|
||||
|
||||
Ask user to choose or infer from context:
|
||||
|
||||
**MindMap Layout:**
|
||||
- Radial structure from center
|
||||
- Parent-child relationships
|
||||
- Clear hierarchy
|
||||
- Good for: brainstorming, topic exploration, hierarchical content
|
||||
|
||||
**Freeform Layout:**
|
||||
- Custom positioning
|
||||
- Flexible relationships
|
||||
- Multiple connection types
|
||||
- Good for: complex networks, non-hierarchical content, custom arrangements
|
||||
|
||||
### 3. Plan Structure
|
||||
|
||||
**For MindMap:**
|
||||
- Identify central concept (root node)
|
||||
- Map primary branches (main topics)
|
||||
- Organize secondary branches (subtopics)
|
||||
- Position leaf nodes (details)
|
||||
|
||||
**For Freeform:**
|
||||
- Group related concepts
|
||||
- Identify connection patterns
|
||||
- Plan spatial zones
|
||||
- Consider visual flow
|
||||
|
||||
### 4. Generate Canvas
|
||||
|
||||
Create JSON following the Canvas specification:
|
||||
|
||||
**Node Creation:**
|
||||
- Assign unique 8-12 character hex IDs
|
||||
- Set appropriate dimensions based on content length
|
||||
- Apply consistent color schemes
|
||||
- Ensure no coordinate overlaps
|
||||
|
||||
**Edge Creation:**
|
||||
- Connect parent-child relationships
|
||||
- Use appropriate arrow styles
|
||||
- Add labels for complex relationships
|
||||
- Choose line styles (straight for hierarchy, curved for cross-references)
|
||||
|
||||
**Grouping (Optional):**
|
||||
- Create visual containers for related nodes
|
||||
- Use subtle background colors
|
||||
- Add descriptive labels
|
||||
|
||||
### 5. Apply Layout Algorithm
|
||||
|
||||
**MindMap Layout Calculations:**
|
||||
|
||||
Refer to `references/layout-algorithms.md` for detailed algorithms. Key principles:
|
||||
|
||||
- Center root at (0, 0)
|
||||
- Distribute primary nodes radially
|
||||
- Space secondary nodes based on sibling count
|
||||
- Maintain minimum spacing: 320px horizontal, 200px vertical
|
||||
|
||||
**Freeform Layout Principles:**
|
||||
|
||||
- Start with logical groupings
|
||||
- Position groups with clear separation
|
||||
- Connect across groups with curved edges
|
||||
- Balance visual weight across canvas
|
||||
|
||||
### 6. Validate and Output
|
||||
|
||||
Before outputting:
|
||||
|
||||
**Validation Checklist:**
|
||||
- All nodes have unique IDs
|
||||
- No coordinate overlaps (check distance > node dimensions + spacing)
|
||||
- All edges reference valid node IDs
|
||||
- Groups (if any) have labels
|
||||
- Colors use consistent format (hex or preset numbers)
|
||||
- JSON is properly escaped (Chinese quotes: 『』 for double, 「」 for single)
|
||||
|
||||
**Output Format:**
|
||||
- Complete, valid JSON Canvas file
|
||||
- No additional explanation text
|
||||
- Directly importable into Obsidian
|
||||
|
||||
## Node Sizing Guidelines
|
||||
|
||||
**Text Length-Based Sizing:**
|
||||
- Short text (<30 chars): 220 × 100 px
|
||||
- Medium text (30-60 chars): 260 × 120 px
|
||||
- Long text (60-100 chars): 320 × 140 px
|
||||
- Very long text (>100 chars): 320 × 180 px
|
||||
|
||||
## Color Schemes
|
||||
|
||||
**Preset Colors (Recommended):**
|
||||
- `"1"` - Red (warnings, important)
|
||||
- `"2"` - Orange (action items)
|
||||
- `"3"` - Yellow (questions, notes)
|
||||
- `"4"` - Green (positive, completed)
|
||||
- `"5"` - Cyan (information, details)
|
||||
- `"6"` - Purple (concepts, abstract)
|
||||
|
||||
**Custom Hex Colors:**
|
||||
Use for brand consistency or specific themes. Always use uppercase format: `"#4A90E2"`
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Quote Handling:**
|
||||
- Chinese double quotes → 『』
|
||||
- Chinese single quotes → 「」
|
||||
- English double quotes → `\"`
|
||||
|
||||
2. **ID Generation:**
|
||||
- 8-12 character random hex strings
|
||||
- Must be unique across all nodes and edges
|
||||
|
||||
3. **Z-Index Order:**
|
||||
- Output groups first (bottom layer)
|
||||
- Then subgroups
|
||||
- Finally text/link nodes (top layer)
|
||||
|
||||
4. **Spacing Requirements:**
|
||||
- Minimum horizontal: 320px between node centers
|
||||
- Minimum vertical: 200px between node centers
|
||||
- Account for node dimensions when calculating
|
||||
|
||||
5. **JSON Structure:**
|
||||
- Top level contains only `nodes` and `edges` arrays
|
||||
- No extra wrapping objects
|
||||
- No comments in output
|
||||
|
||||
6. **No Emoji:**
|
||||
- Do not use any Emoji symbols in node text
|
||||
- Use color coding or text labels for visual distinction instead
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple MindMap Request
|
||||
User: "Create a mind map about solar system planets"
|
||||
|
||||
Process:
|
||||
1. Identify center: "Solar System"
|
||||
2. Primary branches: Inner Planets, Outer Planets, Dwarf Planets
|
||||
3. Secondary nodes: Individual planets with key facts
|
||||
4. Apply radial layout
|
||||
5. Generate JSON with proper spacing
|
||||
|
||||
### Freeform Content Request
|
||||
User: "Turn this article into a canvas" + [article text]
|
||||
|
||||
Process:
|
||||
1. Extract article structure (intro, body sections, conclusion)
|
||||
2. Identify key concepts and relationships
|
||||
3. Group related sections spatially
|
||||
4. Connect with labeled edges
|
||||
5. Apply freeform layout with clear zones
|
||||
|
||||
## Reference Documents
|
||||
|
||||
- **Canvas Specification**: `references/canvas-spec.md` - Complete JSON Canvas format specification
|
||||
- **Layout Algorithms**: `references/layout-algorithms.md` - Detailed positioning algorithms for both layout types
|
||||
|
||||
Load these references when:
|
||||
- Need specification details for edge cases
|
||||
- Implementing complex layout calculations
|
||||
- Troubleshooting validation errors
|
||||
|
||||
## Tips for Quality Canvases
|
||||
|
||||
1. **Keep text concise**: Each node should be scannable (<2 lines preferred)
|
||||
2. **Use hierarchy**: Group by importance and relationship
|
||||
3. **Balance the canvas**: Distribute nodes to avoid clustering
|
||||
4. **Strategic colors**: Use colors to encode meaning, not just decoration
|
||||
5. **Meaningful connections**: Only add edges that clarify relationships
|
||||
6. **Test in Obsidian**: Verify the output opens correctly
|
||||
|
||||
## Common Pitfalls to Avoid
|
||||
|
||||
- Overlapping nodes (always check distances)
|
||||
- Inconsistent quote escaping (breaks JSON parsing)
|
||||
- Missing group labels (causes sidebar navigation issues)
|
||||
- Too much text in nodes (use file nodes for long content)
|
||||
- Duplicate IDs (each must be unique)
|
||||
- Unconnected nodes (unless intentional islands)
|
||||
@@ -1,132 +0,0 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "group01",
|
||||
"type": "group",
|
||||
"x": -50,
|
||||
"y": -50,
|
||||
"width": 600,
|
||||
"height": 400,
|
||||
"label": "Group 1 - Core Concepts",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "group02",
|
||||
"type": "group",
|
||||
"x": 650,
|
||||
"y": -50,
|
||||
"width": 600,
|
||||
"height": 400,
|
||||
"label": "Group 2 - Applications",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "node01",
|
||||
"type": "text",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Concept A",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "node02",
|
||||
"type": "text",
|
||||
"x": 290,
|
||||
"y": 0,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Concept B",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "node03",
|
||||
"type": "text",
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Concept C",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "node04",
|
||||
"type": "text",
|
||||
"x": 290,
|
||||
"y": 150,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Concept D",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "node05",
|
||||
"type": "text",
|
||||
"x": 700,
|
||||
"y": 0,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Application 1",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "node06",
|
||||
"type": "text",
|
||||
"x": 990,
|
||||
"y": 0,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Application 2",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "node07",
|
||||
"type": "text",
|
||||
"x": 700,
|
||||
"y": 150,
|
||||
"width": 240,
|
||||
"height": 100,
|
||||
"text": "Application 3",
|
||||
"color": "5"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e1",
|
||||
"fromNode": "node01",
|
||||
"fromSide": "bottom",
|
||||
"toNode": "node03",
|
||||
"toSide": "top",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e2",
|
||||
"fromNode": "node02",
|
||||
"fromSide": "bottom",
|
||||
"toNode": "node04",
|
||||
"toSide": "top",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e3",
|
||||
"fromNode": "node01",
|
||||
"fromSide": "right",
|
||||
"toNode": "node05",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow",
|
||||
"label": "leads to",
|
||||
"color": "3"
|
||||
},
|
||||
{
|
||||
"id": "e4",
|
||||
"fromNode": "node02",
|
||||
"fromSide": "right",
|
||||
"toNode": "node06",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow",
|
||||
"label": "enables",
|
||||
"color": "3"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "root001",
|
||||
"type": "text",
|
||||
"x": -150,
|
||||
"y": -60,
|
||||
"width": 300,
|
||||
"height": 120,
|
||||
"text": "# Central Topic\n\nMain concept goes here",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "branch01",
|
||||
"type": "text",
|
||||
"x": 250,
|
||||
"y": -200,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Branch 1\n\nFirst main idea",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "branch02",
|
||||
"type": "text",
|
||||
"x": 250,
|
||||
"y": -50,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Branch 2\n\nSecond main idea",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "branch03",
|
||||
"type": "text",
|
||||
"x": 250,
|
||||
"y": 100,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Branch 3\n\nThird main idea",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "detail01",
|
||||
"type": "text",
|
||||
"x": 550,
|
||||
"y": -200,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"text": "Detail A",
|
||||
"color": "6"
|
||||
},
|
||||
{
|
||||
"id": "detail02",
|
||||
"type": "text",
|
||||
"x": 550,
|
||||
"y": -100,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"text": "Detail B",
|
||||
"color": "6"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e1",
|
||||
"fromNode": "root001",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch01",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e2",
|
||||
"fromNode": "root001",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch02",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e3",
|
||||
"fromNode": "root001",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch03",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e4",
|
||||
"fromNode": "branch01",
|
||||
"fromSide": "right",
|
||||
"toNode": "detail01",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e5",
|
||||
"fromNode": "branch01",
|
||||
"fromSide": "right",
|
||||
"toNode": "detail02",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
# JSON Canvas Specification for Obsidian
|
||||
|
||||
Version 1.0 — 2024-03-11
|
||||
|
||||
## Overview
|
||||
|
||||
JSON Canvas is a format for representing infinite canvas documents. This specification defines the structure for creating canvas files compatible with Obsidian.
|
||||
|
||||
## Top Level Structure
|
||||
|
||||
The root JSON object contains two optional arrays:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [...],
|
||||
"edges": [...]
|
||||
}
|
||||
```
|
||||
|
||||
- `nodes` (optional, array): All canvas objects (text, files, links, groups)
|
||||
- `edges` (optional, array): All connections between nodes
|
||||
|
||||
## Node Types
|
||||
|
||||
### Common Attributes
|
||||
|
||||
All nodes share these required attributes:
|
||||
|
||||
- `id` (required, string): Unique identifier for the node
|
||||
- `type` (required, string): Node type (`text`, `file`, `link`, `group`)
|
||||
- `x` (required, integer): X position in pixels
|
||||
- `y` (required, integer): Y position in pixels
|
||||
- `width` (required, integer): Width in pixels
|
||||
- `height` (required, integer): Height in pixels
|
||||
- `color` (optional, string/number): Color (hex `"#FF0000"` or preset `"1"`)
|
||||
|
||||
### Text Nodes
|
||||
|
||||
Store plain text with Markdown formatting.
|
||||
|
||||
**Required Attributes:**
|
||||
- `text` (string): Content in Markdown syntax
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "abc123",
|
||||
"type": "text",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 250,
|
||||
"height": 100,
|
||||
"text": "# Main Topic\n\nKey point here",
|
||||
"color": "4"
|
||||
}
|
||||
```
|
||||
|
||||
### File Nodes
|
||||
|
||||
Reference other files or attachments (images, PDFs, etc.).
|
||||
|
||||
**Required Attributes:**
|
||||
- `file` (string): Path to file in the vault
|
||||
|
||||
**Optional Attributes:**
|
||||
- `subpath` (string): Link to specific heading/block (starts with `#`)
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "def456",
|
||||
"type": "file",
|
||||
"x": 300,
|
||||
"y": 0,
|
||||
"width": 400,
|
||||
"height": 300,
|
||||
"file": "Images/diagram.png"
|
||||
}
|
||||
```
|
||||
|
||||
**With Subpath:**
|
||||
```json
|
||||
{
|
||||
"id": "ghi789",
|
||||
"type": "file",
|
||||
"x": 0,
|
||||
"y": 200,
|
||||
"width": 250,
|
||||
"height": 100,
|
||||
"file": "Notes/Meeting Notes.md",
|
||||
"subpath": "#Action Items"
|
||||
}
|
||||
```
|
||||
|
||||
### Link Nodes
|
||||
|
||||
Reference external URLs.
|
||||
|
||||
**Required Attributes:**
|
||||
- `url` (string): Full URL including protocol
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "jkl012",
|
||||
"type": "link",
|
||||
"x": 0,
|
||||
"y": -200,
|
||||
"width": 250,
|
||||
"height": 100,
|
||||
"url": "https://obsidian.md",
|
||||
"color": "5"
|
||||
}
|
||||
```
|
||||
|
||||
### Group Nodes
|
||||
|
||||
Visual containers for organizing related nodes.
|
||||
|
||||
**Optional Attributes:**
|
||||
- `label` (string): Text label for the group (recommended)
|
||||
- `background` (string): Path to background image
|
||||
- `backgroundStyle` (string): Image rendering style
|
||||
- `cover`: Fill entire node
|
||||
- `ratio`: Maintain aspect ratio
|
||||
- `repeat`: Tile as pattern
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "group1",
|
||||
"type": "group",
|
||||
"x": -50,
|
||||
"y": -50,
|
||||
"width": 600,
|
||||
"height": 400,
|
||||
"label": "Main Concepts",
|
||||
"color": "4"
|
||||
}
|
||||
```
|
||||
|
||||
**With Background:**
|
||||
```json
|
||||
{
|
||||
"id": "group2",
|
||||
"type": "group",
|
||||
"x": 700,
|
||||
"y": 0,
|
||||
"width": 500,
|
||||
"height": 600,
|
||||
"label": "Reference Materials",
|
||||
"background": "Images/texture.png",
|
||||
"backgroundStyle": "repeat"
|
||||
}
|
||||
```
|
||||
|
||||
## Z-Index and Layering
|
||||
|
||||
Nodes are displayed in array order:
|
||||
- **First node**: Bottom layer (rendered below others)
|
||||
- **Last node**: Top layer (rendered above others)
|
||||
|
||||
**Best Practice Order:**
|
||||
1. Group nodes (backgrounds)
|
||||
2. Sub-groups
|
||||
3. Regular nodes (text, file, link)
|
||||
|
||||
This ensures groups appear behind content.
|
||||
|
||||
## Edges (Connections)
|
||||
|
||||
Edges connect nodes with lines.
|
||||
|
||||
**Required Attributes:**
|
||||
- `id` (required, string): Unique identifier
|
||||
- `fromNode` (required, string): Starting node ID
|
||||
- `toNode` (required, string): Ending node ID
|
||||
|
||||
**Optional Attributes:**
|
||||
- `fromSide` (string): Starting edge side
|
||||
- Values: `top`, `right`, `bottom`, `left`
|
||||
- `fromEnd` (string): Start endpoint shape
|
||||
- Values: `none` (default), `arrow`
|
||||
- `toSide` (string): Ending edge side
|
||||
- Values: `top`, `right`, `bottom`, `left`
|
||||
- `toEnd` (string): End endpoint shape
|
||||
- Values: `arrow` (default), `none`
|
||||
- `color` (string/number): Edge color
|
||||
- `label` (string): Text label on edge
|
||||
|
||||
**Example - Simple Connection:**
|
||||
```json
|
||||
{
|
||||
"id": "edge1",
|
||||
"fromNode": "abc123",
|
||||
"toNode": "def456"
|
||||
}
|
||||
```
|
||||
|
||||
**Example - Fully Specified:**
|
||||
```json
|
||||
{
|
||||
"id": "edge2",
|
||||
"fromNode": "def456",
|
||||
"fromSide": "bottom",
|
||||
"fromEnd": "none",
|
||||
"toNode": "ghi789",
|
||||
"toSide": "top",
|
||||
"toEnd": "arrow",
|
||||
"color": "3",
|
||||
"label": "leads to"
|
||||
}
|
||||
```
|
||||
|
||||
## Color System
|
||||
|
||||
### Preset Colors
|
||||
|
||||
Use string numbers `"1"` through `"6"`:
|
||||
|
||||
- `"1"` - Red
|
||||
- `"2"` - Orange
|
||||
- `"3"` - Yellow
|
||||
- `"4"` - Green
|
||||
- `"5"` - Cyan
|
||||
- `"6"` - Purple
|
||||
|
||||
**Note:** Exact colors adapt to Obsidian's theme. These provide semantic meaning across light/dark modes.
|
||||
|
||||
### Custom Hex Colors
|
||||
|
||||
Use hex format: `"#RRGGBB"`
|
||||
|
||||
**Examples:**
|
||||
- `"#4A90E2"` (blue)
|
||||
- `"#50E3C2"` (teal)
|
||||
- `"#F5A623"` (orange)
|
||||
|
||||
**Best Practice:** Use consistent format within a canvas (all hex OR all presets).
|
||||
|
||||
## Complete Example
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "group001",
|
||||
"type": "group",
|
||||
"x": -50,
|
||||
"y": -50,
|
||||
"width": 700,
|
||||
"height": 500,
|
||||
"label": "Core Concepts",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "center01",
|
||||
"type": "text",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 300,
|
||||
"height": 120,
|
||||
"text": "# Central Topic\n\nMain idea here",
|
||||
"color": "4"
|
||||
},
|
||||
{
|
||||
"id": "branch01",
|
||||
"type": "text",
|
||||
"x": 400,
|
||||
"y": -100,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Subtopic A",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "branch02",
|
||||
"type": "text",
|
||||
"x": 400,
|
||||
"y": 100,
|
||||
"width": 220,
|
||||
"height": 100,
|
||||
"text": "Subtopic B",
|
||||
"color": "5"
|
||||
},
|
||||
{
|
||||
"id": "detail01",
|
||||
"type": "text",
|
||||
"x": 700,
|
||||
"y": -100,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"text": "Detail 1",
|
||||
"color": "6"
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "e1",
|
||||
"fromNode": "center01",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch01",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e2",
|
||||
"fromNode": "center01",
|
||||
"fromSide": "right",
|
||||
"toNode": "branch02",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "e3",
|
||||
"fromNode": "branch01",
|
||||
"fromSide": "right",
|
||||
"toNode": "detail01",
|
||||
"toSide": "left",
|
||||
"toEnd": "arrow",
|
||||
"color": "3"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Requirements
|
||||
|
||||
When creating canvas files, ensure:
|
||||
|
||||
1. **Unique IDs**: All `id` values must be unique across nodes and edges
|
||||
2. **Valid References**: All edge `fromNode` and `toNode` must reference existing node IDs
|
||||
3. **Required Fields**: All required attributes are present for each type
|
||||
4. **Valid Coordinates**: All position/dimension values are integers
|
||||
5. **Color Format**: Colors use either hex (`"#RRGGBB"`) or preset strings (`"1"` to `"6"`)
|
||||
6. **Quote Escaping**: Special characters properly escaped in JSON strings
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: Canvas won't open in Obsidian
|
||||
**Solutions:**
|
||||
- Validate JSON syntax (use JSON validator)
|
||||
- Check all IDs are unique
|
||||
- Verify all edge references exist
|
||||
- Ensure required fields present
|
||||
|
||||
### Issue: Nodes appear overlapped
|
||||
**Solutions:**
|
||||
- Increase spacing between coordinates
|
||||
- Account for node dimensions in positioning
|
||||
- Use minimum spacing: 320px horizontal, 200px vertical
|
||||
|
||||
### Issue: Groups don't show properly
|
||||
**Solutions:**
|
||||
- Ensure groups appear before content nodes in array
|
||||
- Add explicit `label` to all groups
|
||||
- Check group dimensions encompass child nodes
|
||||
|
||||
### Issue: Colors don't match expectations
|
||||
**Solutions:**
|
||||
- Use consistent color format (all hex OR all presets)
|
||||
- Remember presets adapt to theme
|
||||
- Test in both light and dark mode if using custom colors
|
||||
|
||||
### Issue: Text appears truncated
|
||||
**Solutions:**
|
||||
- Increase node dimensions
|
||||
- Break long text into multiple nodes
|
||||
- Use file nodes for lengthy content
|
||||
|
||||
## Character Encoding for Chinese Content
|
||||
|
||||
When canvas contains Chinese text, apply these transformations:
|
||||
|
||||
- Chinese double quotes `"` → `『』`
|
||||
- Chinese single quotes `'` → `「」`
|
||||
- English double quotes must be escaped: `\"`
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"text": "『核心概念』包含:「子概念A」和「子概念B」"
|
||||
}
|
||||
```
|
||||
|
||||
This prevents JSON parsing errors with mixed-language content.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Large Canvases**: Keep node count reasonable (<500 for smooth performance)
|
||||
- **Image Files**: Use compressed images for backgrounds
|
||||
- **Text Length**: Keep node text concise; use file nodes for long content
|
||||
- **Edge Complexity**: Minimize crossing edges for clarity
|
||||
|
||||
## Future Extensions
|
||||
|
||||
This specification may be extended with:
|
||||
- Additional node types
|
||||
- More edge styling options
|
||||
- Animation properties
|
||||
- Interactive behaviors
|
||||
|
||||
Always check Obsidian documentation for latest Canvas features.
|
||||
@@ -1,614 +0,0 @@
|
||||
# Layout Algorithms for Obsidian Canvas
|
||||
|
||||
Detailed algorithms for positioning nodes in MindMap and Freeform layouts.
|
||||
|
||||
## Layout Principles
|
||||
|
||||
### Universal Spacing Constants
|
||||
|
||||
```
|
||||
HORIZONTAL_SPACING = 320 // Minimum horizontal space between node centers
|
||||
VERTICAL_SPACING = 200 // Minimum vertical space between node centers
|
||||
NODE_PADDING = 20 // Internal padding within nodes
|
||||
```
|
||||
|
||||
### Collision Detection
|
||||
|
||||
Before finalizing any node position, verify:
|
||||
|
||||
```python
|
||||
def check_collision(node1, node2):
|
||||
"""Returns True if nodes overlap or are too close"""
|
||||
center1_x = node1.x + node1.width / 2
|
||||
center1_y = node1.y + node1.height / 2
|
||||
center2_x = node2.x + node2.width / 2
|
||||
center2_y = node2.y + node2.height / 2
|
||||
|
||||
dx = abs(center1_x - center2_x)
|
||||
dy = abs(center1_y - center2_y)
|
||||
|
||||
min_dx = (node1.width + node2.width) / 2 + HORIZONTAL_SPACING
|
||||
min_dy = (node1.height + node2.height) / 2 + VERTICAL_SPACING
|
||||
|
||||
return dx < min_dx or dy < min_dy
|
||||
```
|
||||
|
||||
## MindMap Layout Algorithm
|
||||
|
||||
### 1. Radial Tree Layout
|
||||
|
||||
Place root at center, arrange children radially.
|
||||
|
||||
#### Step 1: Position Root Node
|
||||
|
||||
```python
|
||||
root = {
|
||||
"x": 0 - (root_width / 2), # Center horizontally
|
||||
"y": 0 - (root_height / 2), # Center vertically
|
||||
"width": root_width,
|
||||
"height": root_height
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2: Calculate Primary Branch Positions
|
||||
|
||||
Distribute first-level children around root:
|
||||
|
||||
```python
|
||||
def position_primary_branches(root, children, radius=400):
|
||||
"""Position first-level children in a circle around root"""
|
||||
n = len(children)
|
||||
angle_step = 2 * pi / n
|
||||
|
||||
positions = []
|
||||
for i, child in enumerate(children):
|
||||
angle = i * angle_step
|
||||
|
||||
# Calculate position on circle
|
||||
x = root.center_x + radius * cos(angle) - child.width / 2
|
||||
y = root.center_y + radius * sin(angle) - child.height / 2
|
||||
|
||||
positions.append({"x": x, "y": y})
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
**Radius Selection:**
|
||||
- Small canvases (≤10 children): 400px
|
||||
- Medium canvases (11-20 children): 500px
|
||||
- Large canvases (>20 children): 600px
|
||||
|
||||
#### Step 3: Position Secondary Branches
|
||||
|
||||
For each primary branch, arrange its children:
|
||||
|
||||
**Horizontal Layout** (preferred for most cases):
|
||||
|
||||
```python
|
||||
def position_secondary_horizontal(parent, children, distance=350):
|
||||
"""Arrange children horizontally to the right of parent"""
|
||||
n = len(children)
|
||||
total_height = sum(child.height for child in children)
|
||||
total_spacing = (n - 1) * VERTICAL_SPACING
|
||||
|
||||
# Start position (top of vertical arrangement)
|
||||
start_y = parent.center_y - (total_height + total_spacing) / 2
|
||||
|
||||
positions = []
|
||||
current_y = start_y
|
||||
|
||||
for child in children:
|
||||
x = parent.x + parent.width + distance
|
||||
y = current_y
|
||||
|
||||
positions.append({"x": x, "y": y})
|
||||
current_y += child.height + VERTICAL_SPACING
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
**Vertical Layout** (for left/right primary branches):
|
||||
|
||||
```python
|
||||
def position_secondary_vertical(parent, children, distance=250):
|
||||
"""Arrange children vertically below parent"""
|
||||
n = len(children)
|
||||
total_width = sum(child.width for child in children)
|
||||
total_spacing = (n - 1) * HORIZONTAL_SPACING
|
||||
|
||||
# Start position (left of horizontal arrangement)
|
||||
start_x = parent.center_x - (total_width + total_spacing) / 2
|
||||
|
||||
positions = []
|
||||
current_x = start_x
|
||||
|
||||
for child in children:
|
||||
x = current_x
|
||||
y = parent.y + parent.height + distance
|
||||
|
||||
positions.append({"x": x, "y": y})
|
||||
current_x += child.width + HORIZONTAL_SPACING
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
#### Step 4: Balance and Adjust
|
||||
|
||||
After initial placement, check for collisions and adjust:
|
||||
|
||||
```python
|
||||
def balance_layout(nodes):
|
||||
"""Adjust nodes to prevent overlaps"""
|
||||
max_iterations = 10
|
||||
|
||||
for iteration in range(max_iterations):
|
||||
collisions = find_all_collisions(nodes)
|
||||
if not collisions:
|
||||
break
|
||||
|
||||
for node1, node2 in collisions:
|
||||
# Move node2 away from node1
|
||||
dx = node2.center_x - node1.center_x
|
||||
dy = node2.center_y - node1.center_y
|
||||
distance = sqrt(dx*dx + dy*dy)
|
||||
|
||||
# Calculate required distance
|
||||
min_dist = calculate_min_distance(node1, node2)
|
||||
|
||||
if distance > 0:
|
||||
# Move proportionally
|
||||
move_x = (dx / distance) * (min_dist - distance) / 2
|
||||
move_y = (dy / distance) * (min_dist - distance) / 2
|
||||
|
||||
node2.x += move_x
|
||||
node2.y += move_y
|
||||
```
|
||||
|
||||
### 2. Tree Layout (Hierarchical Top-Down)
|
||||
|
||||
Alternative for deep hierarchies.
|
||||
|
||||
#### Positioning Formula
|
||||
|
||||
```python
|
||||
def position_tree_layout(root, tree):
|
||||
"""Top-down tree layout"""
|
||||
# Level 0 (root)
|
||||
root.x = 0 - root.width / 2
|
||||
root.y = 0 - root.height / 2
|
||||
|
||||
# Process each level
|
||||
for level in range(1, max_depth):
|
||||
nodes_at_level = get_nodes_at_level(tree, level)
|
||||
|
||||
# Calculate horizontal spacing
|
||||
total_width = sum(node.width for node in nodes_at_level)
|
||||
total_spacing = (len(nodes_at_level) - 1) * HORIZONTAL_SPACING
|
||||
|
||||
start_x = -(total_width + total_spacing) / 2
|
||||
y = level * (150 + VERTICAL_SPACING) # 150px level height
|
||||
|
||||
current_x = start_x
|
||||
for node in nodes_at_level:
|
||||
node.x = current_x
|
||||
node.y = y
|
||||
current_x += node.width + HORIZONTAL_SPACING
|
||||
```
|
||||
|
||||
## Freeform Layout Algorithm
|
||||
|
||||
### 1. Content-Based Grouping
|
||||
|
||||
First, identify natural groupings in content:
|
||||
|
||||
```python
|
||||
def identify_groups(nodes, content_structure):
|
||||
"""Group nodes by semantic relationships"""
|
||||
groups = []
|
||||
|
||||
# Analyze content structure
|
||||
for section in content_structure:
|
||||
group_nodes = [node for node in nodes if node.section == section]
|
||||
|
||||
if len(group_nodes) > 1:
|
||||
groups.append({
|
||||
"label": section.title,
|
||||
"nodes": group_nodes
|
||||
})
|
||||
|
||||
return groups
|
||||
```
|
||||
|
||||
### 2. Grid-Based Zone Layout
|
||||
|
||||
Divide canvas into zones for different groups:
|
||||
|
||||
```python
|
||||
def layout_zones(groups, canvas_width=2000, canvas_height=1500):
|
||||
"""Arrange groups in grid zones"""
|
||||
n_groups = len(groups)
|
||||
|
||||
# Calculate grid dimensions
|
||||
cols = ceil(sqrt(n_groups))
|
||||
rows = ceil(n_groups / cols)
|
||||
|
||||
zone_width = canvas_width / cols
|
||||
zone_height = canvas_height / rows
|
||||
|
||||
# Assign zones
|
||||
zones = []
|
||||
for i, group in enumerate(groups):
|
||||
col = i % cols
|
||||
row = i // cols
|
||||
|
||||
zone = {
|
||||
"x": col * zone_width - canvas_width / 2,
|
||||
"y": row * zone_height - canvas_height / 2,
|
||||
"width": zone_width * 0.9, # Leave 10% margin
|
||||
"height": zone_height * 0.9,
|
||||
"group": group
|
||||
}
|
||||
zones.append(zone)
|
||||
|
||||
return zones
|
||||
```
|
||||
|
||||
### 3. Within-Zone Node Positioning
|
||||
|
||||
Position nodes within each zone:
|
||||
|
||||
**Option A: Organic Flow**
|
||||
|
||||
```python
|
||||
def position_organic(zone, nodes):
|
||||
"""Organic, flowing arrangement within zone"""
|
||||
positions = []
|
||||
|
||||
# Start at zone top-left with margin
|
||||
current_x = zone.x + 50
|
||||
current_y = zone.y + 50
|
||||
row_height = 0
|
||||
|
||||
for node in nodes:
|
||||
# Check if node fits in current row
|
||||
if current_x + node.width > zone.x + zone.width - 50:
|
||||
# Move to next row
|
||||
current_x = zone.x + 50
|
||||
current_y += row_height + VERTICAL_SPACING
|
||||
row_height = 0
|
||||
|
||||
positions.append({
|
||||
"x": current_x,
|
||||
"y": current_y
|
||||
})
|
||||
|
||||
current_x += node.width + HORIZONTAL_SPACING
|
||||
row_height = max(row_height, node.height)
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
**Option B: Structured Grid**
|
||||
|
||||
```python
|
||||
def position_grid(zone, nodes):
|
||||
"""Grid arrangement within zone"""
|
||||
n = len(nodes)
|
||||
cols = ceil(sqrt(n))
|
||||
rows = ceil(n / cols)
|
||||
|
||||
cell_width = (zone.width - 100) / cols # 50px margin each side
|
||||
cell_height = (zone.height - 100) / rows
|
||||
|
||||
positions = []
|
||||
for i, node in enumerate(nodes):
|
||||
col = i % cols
|
||||
row = i // cols
|
||||
|
||||
# Center node in cell
|
||||
x = zone.x + 50 + col * cell_width + (cell_width - node.width) / 2
|
||||
y = zone.y + 50 + row * cell_height + (cell_height - node.height) / 2
|
||||
|
||||
positions.append({"x": x, "y": y})
|
||||
|
||||
return positions
|
||||
```
|
||||
|
||||
### 4. Cross-Zone Connections
|
||||
|
||||
Calculate optimal edge paths between zones:
|
||||
|
||||
```python
|
||||
def calculate_edge_path(from_node, to_node):
|
||||
"""Determine edge connection points"""
|
||||
# Calculate centers
|
||||
from_center = (from_node.x + from_node.width/2,
|
||||
from_node.y + from_node.height/2)
|
||||
to_center = (to_node.x + to_node.width/2,
|
||||
to_node.y + to_node.height/2)
|
||||
|
||||
# Determine best sides to connect
|
||||
dx = to_center[0] - from_center[0]
|
||||
dy = to_center[1] - from_center[1]
|
||||
|
||||
# Choose sides based on direction
|
||||
if abs(dx) > abs(dy):
|
||||
# Horizontal connection
|
||||
from_side = "right" if dx > 0 else "left"
|
||||
to_side = "left" if dx > 0 else "right"
|
||||
else:
|
||||
# Vertical connection
|
||||
from_side = "bottom" if dy > 0 else "top"
|
||||
to_side = "top" if dy > 0 else "bottom"
|
||||
|
||||
return {
|
||||
"fromSide": from_side,
|
||||
"toSide": to_side
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Force-Directed Layout
|
||||
|
||||
For complex networks with many cross-connections:
|
||||
|
||||
```python
|
||||
def force_directed_layout(nodes, edges, iterations=100):
|
||||
"""Spring-based layout algorithm"""
|
||||
# Constants
|
||||
SPRING_LENGTH = 200
|
||||
SPRING_CONSTANT = 0.1
|
||||
REPULSION_CONSTANT = 5000
|
||||
|
||||
for iteration in range(iterations):
|
||||
# Calculate repulsive forces (all pairs)
|
||||
for node1 in nodes:
|
||||
force_x, force_y = 0, 0
|
||||
|
||||
for node2 in nodes:
|
||||
if node1 == node2:
|
||||
continue
|
||||
|
||||
dx = node1.x - node2.x
|
||||
dy = node1.y - node2.y
|
||||
distance = sqrt(dx*dx + dy*dy)
|
||||
|
||||
if distance > 0:
|
||||
# Repulsive force
|
||||
force = REPULSION_CONSTANT / (distance * distance)
|
||||
force_x += (dx / distance) * force
|
||||
force_y += (dy / distance) * force
|
||||
|
||||
node1.force_x = force_x
|
||||
node1.force_y = force_y
|
||||
|
||||
# Calculate attractive forces (connected nodes)
|
||||
for edge in edges:
|
||||
node1 = get_node(edge.fromNode)
|
||||
node2 = get_node(edge.toNode)
|
||||
|
||||
dx = node2.x - node1.x
|
||||
dy = node2.y - node1.y
|
||||
distance = sqrt(dx*dx + dy*dy)
|
||||
|
||||
# Spring force
|
||||
force = SPRING_CONSTANT * (distance - SPRING_LENGTH)
|
||||
|
||||
node1.force_x += (dx / distance) * force
|
||||
node1.force_y += (dy / distance) * force
|
||||
node2.force_x -= (dx / distance) * force
|
||||
node2.force_y -= (dy / distance) * force
|
||||
|
||||
# Apply forces
|
||||
for node in nodes:
|
||||
node.x += node.force_x
|
||||
node.y += node.force_y
|
||||
```
|
||||
|
||||
### Hierarchical Clustering
|
||||
|
||||
Group related nodes automatically:
|
||||
|
||||
```python
|
||||
def hierarchical_cluster(nodes, similarity_threshold=0.7):
|
||||
"""Cluster nodes by content similarity"""
|
||||
clusters = []
|
||||
|
||||
# Calculate similarity matrix
|
||||
similarity = calculate_similarity_matrix(nodes)
|
||||
|
||||
# Agglomerative clustering
|
||||
current_clusters = [[node] for node in nodes]
|
||||
|
||||
while len(current_clusters) > 1:
|
||||
# Find most similar clusters
|
||||
max_sim = 0
|
||||
merge_i, merge_j = 0, 1
|
||||
|
||||
for i in range(len(current_clusters)):
|
||||
for j in range(i + 1, len(current_clusters)):
|
||||
sim = cluster_similarity(current_clusters[i],
|
||||
current_clusters[j],
|
||||
similarity)
|
||||
if sim > max_sim:
|
||||
max_sim = sim
|
||||
merge_i, merge_j = i, j
|
||||
|
||||
if max_sim < similarity_threshold:
|
||||
break
|
||||
|
||||
# Merge clusters
|
||||
current_clusters[merge_i].extend(current_clusters[merge_j])
|
||||
current_clusters.pop(merge_j)
|
||||
|
||||
return current_clusters
|
||||
```
|
||||
|
||||
## Layout Optimization
|
||||
|
||||
### Minimize Edge Crossings
|
||||
|
||||
```python
|
||||
def minimize_crossings(nodes, edges):
|
||||
"""Reduce edge crossing through node repositioning"""
|
||||
crossings = count_crossings(edges)
|
||||
|
||||
# Try swapping adjacent nodes
|
||||
improved = True
|
||||
while improved:
|
||||
improved = False
|
||||
|
||||
for i in range(len(nodes) - 1):
|
||||
# Swap nodes i and i+1
|
||||
swap_positions(nodes[i], nodes[i+1])
|
||||
new_crossings = count_crossings(edges)
|
||||
|
||||
if new_crossings < crossings:
|
||||
crossings = new_crossings
|
||||
improved = True
|
||||
else:
|
||||
# Swap back
|
||||
swap_positions(nodes[i], nodes[i+1])
|
||||
```
|
||||
|
||||
### Visual Balance
|
||||
|
||||
```python
|
||||
def calculate_visual_weight(canvas):
|
||||
"""Calculate center of mass for visual balance"""
|
||||
total_weight = 0
|
||||
weighted_x = 0
|
||||
weighted_y = 0
|
||||
|
||||
for node in canvas.nodes:
|
||||
# Weight is proportional to area
|
||||
weight = node.width * node.height
|
||||
total_weight += weight
|
||||
|
||||
weighted_x += node.center_x * weight
|
||||
weighted_y += node.center_y * weight
|
||||
|
||||
center_x = weighted_x / total_weight
|
||||
center_y = weighted_y / total_weight
|
||||
|
||||
# Shift entire canvas to center at (0, 0)
|
||||
offset_x = -center_x
|
||||
offset_y = -center_y
|
||||
|
||||
for node in canvas.nodes:
|
||||
node.x += offset_x
|
||||
node.y += offset_y
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Spatial Indexing
|
||||
|
||||
For large canvases, use spatial indexing to speed up collision detection:
|
||||
|
||||
```python
|
||||
class SpatialGrid:
|
||||
"""Grid-based spatial index for fast collision detection"""
|
||||
|
||||
def __init__(self, cell_size=500):
|
||||
self.cell_size = cell_size
|
||||
self.grid = {}
|
||||
|
||||
def add_node(self, node):
|
||||
"""Add node to grid"""
|
||||
cells = self.get_cells(node)
|
||||
for cell in cells:
|
||||
if cell not in self.grid:
|
||||
self.grid[cell] = []
|
||||
self.grid[cell].append(node)
|
||||
|
||||
def get_cells(self, node):
|
||||
"""Get grid cells node occupies"""
|
||||
min_x = int(node.x / self.cell_size)
|
||||
max_x = int((node.x + node.width) / self.cell_size)
|
||||
min_y = int(node.y / self.cell_size)
|
||||
max_y = int((node.y + node.height) / self.cell_size)
|
||||
|
||||
cells = []
|
||||
for x in range(min_x, max_x + 1):
|
||||
for y in range(min_y, max_y + 1):
|
||||
cells.append((x, y))
|
||||
return cells
|
||||
|
||||
def get_nearby_nodes(self, node):
|
||||
"""Get nodes in nearby cells"""
|
||||
cells = self.get_cells(node)
|
||||
nearby = set()
|
||||
|
||||
for cell in cells:
|
||||
if cell in self.grid:
|
||||
nearby.update(self.grid[cell])
|
||||
|
||||
return nearby
|
||||
```
|
||||
|
||||
## Common Layout Patterns
|
||||
|
||||
### Timeline Layout
|
||||
|
||||
For chronological content:
|
||||
|
||||
```python
|
||||
def layout_timeline(events, direction="horizontal"):
|
||||
"""Create timeline layout"""
|
||||
if direction == "horizontal":
|
||||
for i, event in enumerate(events):
|
||||
event.x = i * (event.width + HORIZONTAL_SPACING)
|
||||
event.y = 0
|
||||
else: # vertical
|
||||
for i, event in enumerate(events):
|
||||
event.x = 0
|
||||
event.y = i * (event.height + VERTICAL_SPACING)
|
||||
```
|
||||
|
||||
### Circular Layout
|
||||
|
||||
For cyclical processes:
|
||||
|
||||
```python
|
||||
def layout_circular(nodes, radius=500):
|
||||
"""Arrange nodes in a circle"""
|
||||
n = len(nodes)
|
||||
angle_step = 2 * pi / n
|
||||
|
||||
for i, node in enumerate(nodes):
|
||||
angle = i * angle_step
|
||||
node.x = radius * cos(angle) - node.width / 2
|
||||
node.y = radius * sin(angle) - node.height / 2
|
||||
```
|
||||
|
||||
### Matrix Layout
|
||||
|
||||
For comparing multiple dimensions:
|
||||
|
||||
```python
|
||||
def layout_matrix(nodes, rows, cols):
|
||||
"""Arrange nodes in a matrix"""
|
||||
cell_width = 400
|
||||
cell_height = 250
|
||||
|
||||
for i, node in enumerate(nodes):
|
||||
row = i // cols
|
||||
col = i % cols
|
||||
|
||||
node.x = col * cell_width
|
||||
node.y = row * cell_height
|
||||
```
|
||||
|
||||
## Quality Checks
|
||||
|
||||
Before finalizing layout, verify:
|
||||
|
||||
1. **No Overlaps**: All nodes have minimum spacing
|
||||
2. **Balanced**: Visual center near (0, 0)
|
||||
3. **Accessible**: All nodes reachable via edges
|
||||
4. **Readable**: Text sizes appropriate for zoom level
|
||||
5. **Efficient**: Edge paths reasonably direct
|
||||
|
||||
Use these algorithms as foundations, adapting to specific content and user preferences.
|
||||
@@ -1,620 +0,0 @@
|
||||
---
|
||||
name: obsidian-markdown
|
||||
description: Create and edit Obsidian Flavored Markdown with wikilinks, embeds, callouts, properties, and other Obsidian-specific syntax. Use when working with .md files in Obsidian, or when the user mentions wikilinks, callouts, frontmatter, tags, embeds, or Obsidian notes.
|
||||
---
|
||||
|
||||
# Obsidian Flavored Markdown Skill
|
||||
|
||||
This skill enables skills-compatible agents to create and edit valid Obsidian Flavored Markdown, including all Obsidian-specific syntax extensions.
|
||||
|
||||
## Overview
|
||||
|
||||
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.)
|
||||
|
||||
## 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: `\*`, `\_`, `\#`, `` \` ``, `\|`, `\~`
|
||||
|
||||
## Internal Links (Wikilinks)
|
||||
|
||||
### Basic Links
|
||||
|
||||
```markdown
|
||||
[[Note Name]]
|
||||
[[Note Name.md]]
|
||||
[[Note Name|Display Text]]
|
||||
```
|
||||
|
||||
### Link to Headings
|
||||
|
||||
```markdown
|
||||
[[Note Name#Heading]]
|
||||
[[Note Name#Heading|Custom Text]]
|
||||
[[#Heading in same note]]
|
||||
[[##Search all headings in vault]]
|
||||
```
|
||||
|
||||
### Link to Blocks
|
||||
|
||||
```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
|
||||
|
||||
^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
|
||||
|
||||
```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
|
||||

|
||||

|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
````
|
||||
|
||||
## Callouts
|
||||
|
||||
### 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 | 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
|
||||
|
||||
```markdown
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Nested item
|
||||
- Another nested
|
||||
- Item 3
|
||||
|
||||
* Also works with asterisks
|
||||
+ Or plus signs
|
||||
```
|
||||
|
||||
### Ordered Lists
|
||||
|
||||
```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]] |
|
||||
```
|
||||
|
||||
## Math (LaTeX)
|
||||
|
||||
### Inline Math
|
||||
|
||||
```markdown
|
||||
This is inline math: $e^{i\pi} + 1 = 0$
|
||||
```
|
||||
|
||||
### Block Math
|
||||
|
||||
```markdown
|
||||
$$
|
||||
\begin{vmatrix}
|
||||
a & b \\
|
||||
c & d
|
||||
\end{vmatrix} = ad - bc
|
||||
$$
|
||||
```
|
||||
|
||||
### 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
|
||||
```mermaid
|
||||
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;
|
||||
```
|
||||
````
|
||||
|
||||
## Footnotes
|
||||
|
||||
```markdown
|
||||
This sentence has a footnote[^1].
|
||||
|
||||
[^1]: This is the 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
|
||||
<div class="custom-container">
|
||||
<span style="color: red;">Colored text</span>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
Hidden content here.
|
||||
</details>
|
||||
|
||||
<kbd>Ctrl</kbd> + <kbd>C</kbd>
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
````markdown
|
||||
---
|
||||
title: Project Alpha
|
||||
date: 2024-01-15
|
||||
tags:
|
||||
- project
|
||||
- active
|
||||
status: in-progress
|
||||
priority: high
|
||||
---
|
||||
|
||||
# Project Alpha
|
||||
|
||||
## Overview
|
||||
|
||||
This project aims to [[improve workflow]] using modern techniques.
|
||||
|
||||
> [!important] Key Deadline
|
||||
> The first milestone is due on ==January 30th==.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Initial planning
|
||||
- [x] Resource allocation
|
||||
- [ ] Development phase
|
||||
- [ ] Backend implementation
|
||||
- [ ] Frontend design
|
||||
- [ ] Testing
|
||||
- [ ] Deployment
|
||||
|
||||
## Technical Notes
|
||||
|
||||
The main algorithm uses the formula $O(n \log n)$ for sorting.
|
||||
|
||||
```python
|
||||
def process_data(items):
|
||||
return sorted(items, key=lambda x: x.priority)
|
||||
```
|
||||
|
||||
## 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
|
||||
%%
|
||||
````
|
||||
|
||||
## 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)
|
||||
- [Callouts](https://help.obsidian.md/callouts)
|
||||
- [Properties](https://help.obsidian.md/properties)
|
||||
@@ -1,3 +0,0 @@
|
||||
views:
|
||||
- type: table
|
||||
name: 表格
|
||||
Reference in New Issue
Block a user