mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add the crate dependency graph visualization to the contributor guide (#3907)
* Add the crate dependency graph visualization to the contributor guide * Code review fixes * And more
This commit is contained in:
35
.github/workflows/build.yml
vendored
35
.github/workflows/build.yml
vendored
@@ -92,8 +92,7 @@ jobs:
|
|||||||
|
|
||||||
- name: ✂ Replace template in <head> of index.html
|
- name: ✂ Replace template in <head> of index.html
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
run: |
|
run: sed -i "s|<!-- INDEX_HTML_HEAD_REPLACEMENT -->|$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
|
||||||
sed -i "s|<!-- INDEX_HTML_HEAD_REPLACEMENT -->|$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
|
|
||||||
|
|
||||||
- name: 🌐 Build Graphite web code
|
- name: 🌐 Build Graphite web code
|
||||||
env:
|
env:
|
||||||
@@ -165,8 +164,7 @@ jobs:
|
|||||||
|
|
||||||
- name: ✂ Strip analytics script from built output for clean artifact
|
- name: ✂ Strip analytics script from built output for clean artifact
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
run: |
|
run: sed -i "s|$INDEX_HTML_HEAD_REPLACEMENT||" frontend/dist/index.html
|
||||||
sed -i "s|$INDEX_HTML_HEAD_REPLACEMENT||" frontend/dist/index.html
|
|
||||||
|
|
||||||
- name: 📦 Upload web bundle artifact
|
- name: 📦 Upload web bundle artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
@@ -177,49 +175,38 @@ jobs:
|
|||||||
- name: 📃 Generate code documentation info for website
|
- name: 📃 Generate code documentation info for website
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
run: |
|
run: |
|
||||||
cd tools/editor-message-tree
|
mkdir -p website/generated-new
|
||||||
cargo run
|
cargo run -p crate-hierarchy-viz -- website/generated-new/crate_hierarchy.dot
|
||||||
cd ../..
|
cargo run -p editor-message-tree -- website/generated-new/hierarchical_message_system_tree.txt
|
||||||
mkdir -p artifacts-generated
|
|
||||||
mv website/generated/hierarchical_message_system_tree.txt artifacts-generated/hierarchical_message_system_tree.txt
|
|
||||||
|
|
||||||
- name: 💿 Obtain cache of auto-generated code docs artifacts, to check if they've changed
|
- name: 💿 Obtain cache of auto-generated code docs artifacts, to check if they've changed
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
id: cache-website-code-docs
|
id: cache-website-code-docs
|
||||||
uses: actions/cache/restore@v5
|
uses: actions/cache/restore@v5
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: website/generated
|
||||||
key: website-code-docs
|
key: website-code-docs
|
||||||
|
|
||||||
- name: 🔍 Check if auto-generated code docs artifacts changed
|
- name: 🔍 Check if auto-generated code docs artifacts changed
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
id: website-code-docs-changed
|
id: website-code-docs-changed
|
||||||
run: |
|
run: |
|
||||||
if ! diff --brief --recursive artifacts-generated artifacts; then
|
diff --brief --recursive website/generated-new website/generated || echo "changed=true" >> $GITHUB_OUTPUT
|
||||||
echo "Auto-generated code docs artifacts have changed."
|
rm -rf website/generated
|
||||||
rm -rf artifacts
|
mv website/generated-new website/generated
|
||||||
mv artifacts-generated artifacts
|
|
||||||
echo "changed=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "Auto-generated code docs artifacts have not changed."
|
|
||||||
rm -rf artifacts
|
|
||||||
rm -rf artifacts-generated
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: 💾 Save cache of auto-generated code docs artifacts
|
- name: 💾 Save cache of auto-generated code docs artifacts
|
||||||
if: github.event_name == 'push' && steps.website-code-docs-changed.outputs.changed == 'true'
|
if: github.event_name == 'push' && steps.website-code-docs-changed.outputs.changed == 'true'
|
||||||
uses: actions/cache/save@v5
|
uses: actions/cache/save@v5
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: website/generated
|
||||||
key: ${{ steps.cache-website-code-docs.outputs.cache-primary-key }}
|
key: ${{ steps.cache-website-code-docs.outputs.cache-primary-key }}
|
||||||
|
|
||||||
- name: ♻️ Trigger website rebuild if the auto-generated code docs artifacts have changed
|
- name: ♻️ Trigger website rebuild if the auto-generated code docs artifacts have changed
|
||||||
if: github.event_name == 'push' && steps.website-code-docs-changed.outputs.changed == 'true'
|
if: github.event_name == 'push' && steps.website-code-docs-changed.outputs.changed == 'true'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: gh workflow run website.yml --ref master
|
||||||
rm -rf artifacts
|
|
||||||
gh workflow run website.yml --ref master
|
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
if: github.event_name == 'push' || inputs.windows
|
if: github.event_name == 'push' || inputs.windows
|
||||||
|
|||||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -57,8 +57,7 @@ jobs:
|
|||||||
|
|
||||||
- name: 🦀 Fetch Rust dependencies
|
- name: 🦀 Fetch Rust dependencies
|
||||||
if: steps.skip-check.outputs.skip-check != 'true'
|
if: steps.skip-check.outputs.skip-check != 'true'
|
||||||
run: |
|
run: cargo fetch --locked
|
||||||
cargo fetch --locked
|
|
||||||
|
|
||||||
- name: 🌐 Build Graphite web code
|
- name: 🌐 Build Graphite web code
|
||||||
if: steps.skip-check.outputs.skip-check != 'true'
|
if: steps.skip-check.outputs.skip-check != 'true'
|
||||||
@@ -72,8 +71,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
run: |
|
run: npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-dev" --commit-dirty=true
|
||||||
npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-dev" --commit-dirty=true
|
|
||||||
|
|
||||||
- name: 👕 Lint Graphite web formatting
|
- name: 👕 Lint Graphite web formatting
|
||||||
if: steps.skip-check.outputs.skip-check != 'true'
|
if: steps.skip-check.outputs.skip-check != 'true'
|
||||||
@@ -110,15 +108,13 @@ jobs:
|
|||||||
|
|
||||||
- name: 🦀 Fetch Rust dependencies
|
- name: 🦀 Fetch Rust dependencies
|
||||||
if: steps.skip-check.outputs.skip-check != 'true'
|
if: steps.skip-check.outputs.skip-check != 'true'
|
||||||
run: |
|
run: cargo fetch --locked
|
||||||
cargo fetch --locked
|
|
||||||
|
|
||||||
- name: 🧪 Run Rust tests
|
- name: 🧪 Run Rust tests
|
||||||
if: steps.skip-check.outputs.skip-check != 'true'
|
if: steps.skip-check.outputs.skip-check != 'true'
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
run: |
|
run: mold -run cargo test --all-features
|
||||||
mold -run cargo test --all-features
|
|
||||||
|
|
||||||
# Rust format check on GitHub runner
|
# Rust format check on GitHub runner
|
||||||
rust-fmt:
|
rust-fmt:
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install iai-callgrind
|
- name: Install iai-callgrind
|
||||||
if: steps.cache-iai.outputs.cache-hit != 'true'
|
if: steps.cache-iai.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: cargo install iai-callgrind-runner@0.16.1
|
||||||
cargo install iai-callgrind-runner@0.16.1
|
|
||||||
|
|
||||||
- name: Checkout master branch
|
- name: Checkout master branch
|
||||||
run: |
|
run: |
|
||||||
@@ -84,8 +83,7 @@ jobs:
|
|||||||
cargo bench --bench run_cached_iai -- --save-baseline=master
|
cargo bench --bench run_cached_iai -- --save-baseline=master
|
||||||
|
|
||||||
- name: Checkout PR branch
|
- name: Checkout PR branch
|
||||||
run: |
|
run: git checkout ${{ github.event.pull_request.head.sha }}
|
||||||
git checkout ${{ github.event.pull_request.head.sha }}
|
|
||||||
|
|
||||||
- name: Run PR benchmarks
|
- name: Run PR benchmarks
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -47,8 +47,7 @@ jobs:
|
|||||||
target: wasm32-unknown-unknown
|
target: wasm32-unknown-unknown
|
||||||
|
|
||||||
- name: ✂ Replace template in <head> of index.html
|
- name: ✂ Replace template in <head> of index.html
|
||||||
run: |
|
run: sed -i "s|<!-- INDEX_HTML_HEAD_REPLACEMENT -->|$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
|
||||||
sed -i "s|<!-- INDEX_HTML_HEAD_REPLACEMENT -->|$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
|
|
||||||
|
|
||||||
- name: 🌐 Build Graphite web code
|
- name: 🌐 Build Graphite web code
|
||||||
env:
|
env:
|
||||||
@@ -59,8 +58,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
run: |
|
run: npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-editor" --branch="master" --commit-dirty=true
|
||||||
npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-editor" --branch="master" --commit-dirty=true
|
|
||||||
|
|
||||||
- name: 📦 Upload assets to GitHub release
|
- name: 📦 Upload assets to GitHub release
|
||||||
env:
|
env:
|
||||||
|
|||||||
26
.github/workflows/website.yml
vendored
26
.github/workflows/website.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
|||||||
id: cache-website-code-docs
|
id: cache-website-code-docs
|
||||||
uses: actions/cache/restore@v5
|
uses: actions/cache/restore@v5
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: website/generated
|
||||||
key: website-code-docs
|
key: website-code-docs
|
||||||
|
|
||||||
- name: 📁 Fallback in case auto-generated code docs artifacts weren't cached
|
- name: 📁 Fallback in case auto-generated code docs artifacts weren't cached
|
||||||
@@ -64,33 +64,24 @@ jobs:
|
|||||||
rustup update stable
|
rustup update stable
|
||||||
echo "🦀 Latest updated version of Rust:"
|
echo "🦀 Latest updated version of Rust:"
|
||||||
rustc --version
|
rustc --version
|
||||||
cd tools/editor-message-tree
|
cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.dot
|
||||||
cargo run
|
cargo run -p editor-message-tree -- website/generated/hierarchical_message_system_tree.txt
|
||||||
cd ../..
|
|
||||||
mkdir artifacts
|
|
||||||
mv website/generated/hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt
|
|
||||||
|
|
||||||
- name: 🚚 Move `artifacts` contents to website/generated
|
- name: 🔧 Build auto-generated code docs artifacts into HTML/SVG
|
||||||
run: |
|
|
||||||
mkdir -p website/generated
|
|
||||||
mv artifacts/* website/generated/
|
|
||||||
|
|
||||||
- name: 🔧 Build auto-generated code docs artifacts into HTML
|
|
||||||
run: |
|
run: |
|
||||||
cd website
|
cd website
|
||||||
|
npm ci
|
||||||
npm run generate-editor-structure
|
npm run generate-editor-structure
|
||||||
|
npm run generate-crate-hierarchy
|
||||||
|
|
||||||
- name: 📃 Generate node catalog documentation
|
- name: 📃 Generate node catalog documentation
|
||||||
run: |
|
run: cargo run -p node-docs -- website/content/learn/node-catalog
|
||||||
cd tools/node-docs
|
|
||||||
cargo run
|
|
||||||
|
|
||||||
- name: 🌐 Build Graphite website with Zola
|
- name: 🌐 Build Graphite website with Zola
|
||||||
env:
|
env:
|
||||||
MODE: prod
|
MODE: prod
|
||||||
run: |
|
run: |
|
||||||
cd website
|
cd website
|
||||||
npm ci
|
|
||||||
npm run check
|
npm run check
|
||||||
zola --config config.toml build --minify
|
zola --config config.toml build --minify
|
||||||
|
|
||||||
@@ -99,5 +90,4 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
run: |
|
run: npx wrangler@3 pages deploy "website/public" --project-name="graphite-website" --commit-dirty=true
|
||||||
npx wrangler@3 pages deploy "website/public" --project-name="graphite-website" --commit-dirty=true
|
|
||||||
|
|||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -1106,7 +1106,7 @@ name = "crate-hierarchy-viz"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"glob",
|
||||||
"serde",
|
"serde",
|
||||||
"toml 0.8.23",
|
"toml 0.8.23",
|
||||||
]
|
]
|
||||||
@@ -2064,6 +2064,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glow"
|
name = "glow"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ fn explore_usage() {
|
|||||||
println!("OPTIONS:");
|
println!("OPTIONS:");
|
||||||
println!("<tool>:");
|
println!("<tool>:");
|
||||||
println!(" bisect Binary search through recent commits to find which introduced a bug or feature");
|
println!(" bisect Binary search through recent commits to find which introduced a bug or feature");
|
||||||
|
println!(" deps View the crate dependency graph for the workspace");
|
||||||
println!(" editor View an interactive outline of the editor's message system architecture");
|
println!(" editor View an interactive outline of the editor's message system architecture");
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
@@ -67,6 +68,7 @@ fn run_task(task: &Task) -> Result<(), Error> {
|
|||||||
if let Action::Explore(tool) = &task.action {
|
if let Action::Explore(tool) = &task.action {
|
||||||
match tool.as_deref() {
|
match tool.as_deref() {
|
||||||
Some("bisect") => return open_url("https://graphite.art/volunteer/guide/codebase-overview/debugging-tips/#build-bisect-tool"),
|
Some("bisect") => return open_url("https://graphite.art/volunteer/guide/codebase-overview/debugging-tips/#build-bisect-tool"),
|
||||||
|
Some("deps") => return open_url("https://graphite.art/volunteer/guide/codebase-overview/#crate-dependency-graph"),
|
||||||
Some("editor") => return open_url("https://graphite.art/volunteer/guide/codebase-overview/editor-structure/#editor-outline"),
|
Some("editor") => return open_url("https://graphite.art/volunteer/guide/codebase-overview/editor-structure/#editor-outline"),
|
||||||
None | Some("--help") => {
|
None | Some("--help") => {
|
||||||
explore_usage();
|
explore_usage();
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ authors.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
clap = { workspace = true, features = ["derive"] }
|
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
glob = "0.3"
|
||||||
|
|||||||
@@ -1,37 +1,8 @@
|
|||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, ValueEnum};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, ValueEnum)]
|
|
||||||
enum OutputFormat {
|
|
||||||
/// Output DOT format (GraphViz)
|
|
||||||
Dot,
|
|
||||||
/// Output PNG image (requires GraphViz)
|
|
||||||
Png,
|
|
||||||
/// Output SVG image (requires GraphViz)
|
|
||||||
Svg,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(name = "crate-hierarchy-viz")]
|
|
||||||
#[command(about = "Visualize the crate hierarchy in the Graphite workspace")]
|
|
||||||
struct Args {
|
|
||||||
/// Workspace root directory (defaults to current directory)
|
|
||||||
#[arg(short, long)]
|
|
||||||
workspace: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Output file (defaults to stdout for DOT format, required for PNG/SVG)
|
|
||||||
#[arg(short, long)]
|
|
||||||
output: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Output format
|
|
||||||
#[arg(short, long, value_enum, default_value = "dot")]
|
|
||||||
format: OutputFormat,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct WorkspaceToml {
|
struct WorkspaceToml {
|
||||||
@@ -41,20 +12,6 @@ struct WorkspaceToml {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct WorkspaceConfig {
|
struct WorkspaceConfig {
|
||||||
members: Vec<String>,
|
members: Vec<String>,
|
||||||
dependencies: Option<HashMap<String, WorkspaceDependency>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a workspace-level dependency in Cargo.toml
|
|
||||||
/// The Simple variant's String is needed for serde deserialization but never read directly
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
enum WorkspaceDependency {
|
|
||||||
Simple(String),
|
|
||||||
Detailed {
|
|
||||||
#[serde(flatten)]
|
|
||||||
_other: HashMap<String, toml::Value>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -77,18 +34,15 @@ enum CrateDependency {
|
|||||||
Simple(String),
|
Simple(String),
|
||||||
Detailed {
|
Detailed {
|
||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
workspace: Option<bool>,
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
other: HashMap<String, toml::Value>,
|
other: HashMap<String, toml::Value>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
struct CrateInfo {
|
struct CrateInfo {
|
||||||
name: String,
|
name: String,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
dependencies: Vec<String>,
|
dependencies: Vec<String>,
|
||||||
external_dependencies: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove transitive dependencies from the crate list.
|
/// Remove transitive dependencies from the crate list.
|
||||||
@@ -121,7 +75,6 @@ fn collect_all_dependencies(crate_name: &str, dep_map: &HashMap<String, HashSet<
|
|||||||
if !visited.insert(crate_name.to_string()) {
|
if !visited.insert(crate_name.to_string()) {
|
||||||
return; // Already visited, avoid cycles
|
return; // Already visited, avoid cycles
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(deps) = dep_map.get(crate_name) {
|
if let Some(deps) = dep_map.get(crate_name) {
|
||||||
for dep in deps {
|
for dep in deps {
|
||||||
collect_all_dependencies(dep, dep_map, visited);
|
collect_all_dependencies(dep, dep_map, visited);
|
||||||
@@ -130,24 +83,50 @@ fn collect_all_dependencies(crate_name: &str, dep_map: &HashMap<String, HashSet<
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args = Args::parse();
|
let output_path = std::env::args_os()
|
||||||
|
.nth(1)
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Usage: crate-hierarchy-viz <output-file>"))?;
|
||||||
|
|
||||||
let workspace_root = args.workspace.unwrap_or_else(|| std::env::current_dir().unwrap());
|
let workspace_root = std::env::current_dir().unwrap();
|
||||||
let workspace_toml_path = workspace_root.join("Cargo.toml");
|
let workspace_toml_path = workspace_root.join("Cargo.toml");
|
||||||
|
|
||||||
// Parse workspace Cargo.toml
|
// Parse workspace Cargo.toml
|
||||||
let workspace_content = fs::read_to_string(&workspace_toml_path).with_context(|| format!("Failed to read {:?}", workspace_toml_path))?;
|
let workspace_content = fs::read_to_string(&workspace_toml_path).with_context(|| format!("Failed to read {:?}", workspace_toml_path))?;
|
||||||
let workspace_toml: WorkspaceToml = toml::from_str(&workspace_content).with_context(|| "Failed to parse workspace Cargo.toml")?;
|
let workspace_toml: WorkspaceToml = toml::from_str(&workspace_content).with_context(|| "Failed to parse workspace Cargo.toml")?;
|
||||||
|
|
||||||
// Get workspace dependencies (external crates defined at workspace level)
|
// Expand glob patterns in workspace members (e.g., "node-graph/libraries/*")
|
||||||
let workspace_deps: HashSet<String> = workspace_toml.workspace.dependencies.unwrap_or_default().keys().cloned().collect();
|
let mut resolved_members = Vec::new();
|
||||||
|
let mut seen_members = HashSet::new();
|
||||||
// Parse each member crate and build name mapping
|
let abs_root = workspace_root.canonicalize().unwrap_or_else(|_| workspace_root.clone());
|
||||||
let mut crates = Vec::new();
|
|
||||||
let mut workspace_crate_names = HashSet::new();
|
|
||||||
|
|
||||||
// First pass: collect all workspace crate names
|
|
||||||
for member in &workspace_toml.workspace.members {
|
for member in &workspace_toml.workspace.members {
|
||||||
|
if member.contains('*') {
|
||||||
|
let pattern = abs_root.join(member).to_string_lossy().to_string();
|
||||||
|
let matched: Vec<_> = glob::glob(&pattern)
|
||||||
|
.with_context(|| format!("Failed to expand glob pattern: {member}"))?
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.filter_map(|path| path.strip_prefix(&abs_root).ok().map(|p| p.to_string_lossy().to_string()))
|
||||||
|
.collect();
|
||||||
|
if matched.is_empty() {
|
||||||
|
eprintln!("Warning: No matches for glob pattern: {member}");
|
||||||
|
}
|
||||||
|
for m in matched {
|
||||||
|
let normalized = m.replace('\\', "/");
|
||||||
|
if seen_members.insert(normalized.clone()) {
|
||||||
|
resolved_members.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let normalized = member.replace('\\', "/");
|
||||||
|
if seen_members.insert(normalized.clone()) {
|
||||||
|
resolved_members.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse each member crate's Cargo.toml
|
||||||
|
let mut parsed_crates = Vec::new();
|
||||||
|
for member in &resolved_members {
|
||||||
let crate_path = workspace_root.join(member);
|
let crate_path = workspace_root.join(member);
|
||||||
let cargo_toml_path = crate_path.join("Cargo.toml");
|
let cargo_toml_path = crate_path.join("Cargo.toml");
|
||||||
|
|
||||||
@@ -159,243 +138,114 @@ fn main() -> Result<()> {
|
|||||||
let crate_content = fs::read_to_string(&cargo_toml_path).with_context(|| format!("Failed to read {:?}", cargo_toml_path))?;
|
let crate_content = fs::read_to_string(&cargo_toml_path).with_context(|| format!("Failed to read {:?}", cargo_toml_path))?;
|
||||||
let crate_toml: CrateToml = toml::from_str(&crate_content).with_context(|| format!("Failed to parse Cargo.toml for {}", member))?;
|
let crate_toml: CrateToml = toml::from_str(&crate_content).with_context(|| format!("Failed to parse Cargo.toml for {}", member))?;
|
||||||
|
|
||||||
workspace_crate_names.insert(crate_toml.package.name.clone());
|
parsed_crates.push((crate_path, crate_toml));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: parse dependencies now that we know all workspace crate names
|
// Collect all workspace crate names
|
||||||
for member in &workspace_toml.workspace.members {
|
let workspace_crate_names: HashSet<String> = parsed_crates.iter().map(|(_, toml)| toml.package.name.clone()).collect();
|
||||||
let crate_path = workspace_root.join(member);
|
|
||||||
let cargo_toml_path = crate_path.join("Cargo.toml");
|
|
||||||
|
|
||||||
if !cargo_toml_path.exists() {
|
// Build dependency graph, keeping only workspace-internal dependencies
|
||||||
continue;
|
let mut crates: Vec<CrateInfo> = parsed_crates
|
||||||
}
|
.into_iter()
|
||||||
|
.map(|(path, crate_toml)| {
|
||||||
|
let dependencies = crate_toml
|
||||||
|
.dependencies
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(dep_name, dep_config)| {
|
||||||
|
// Resolve the actual package name (handles renamed dependencies)
|
||||||
|
let actual_name = match &dep_config {
|
||||||
|
CrateDependency::Detailed { other, .. } => other.get("package").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or(dep_name),
|
||||||
|
CrateDependency::Simple(_) => dep_name,
|
||||||
|
};
|
||||||
|
// Only keep dependencies that are workspace crates
|
||||||
|
workspace_crate_names.contains(&actual_name).then_some(actual_name)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let crate_content = fs::read_to_string(&cargo_toml_path).with_context(|| format!("Failed to read {:?}", cargo_toml_path))?;
|
CrateInfo {
|
||||||
let crate_toml: CrateToml = toml::from_str(&crate_content).with_context(|| format!("Failed to parse Cargo.toml for {}", member))?;
|
name: crate_toml.package.name,
|
||||||
|
path,
|
||||||
let mut dependencies = Vec::new();
|
dependencies,
|
||||||
let mut external_dependencies = Vec::new();
|
|
||||||
|
|
||||||
if let Some(deps) = &crate_toml.dependencies {
|
|
||||||
for (dep_name, dep_config) in deps {
|
|
||||||
let is_workspace_crate = workspace_crate_names.contains(dep_name);
|
|
||||||
let is_workspace_dep = workspace_deps.contains(dep_name);
|
|
||||||
|
|
||||||
let is_local_dep = match dep_config {
|
|
||||||
CrateDependency::Detailed { workspace: Some(true), .. } => is_workspace_dep,
|
|
||||||
CrateDependency::Detailed { path: Some(_), .. } => true,
|
|
||||||
CrateDependency::Simple(_) => is_workspace_dep,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if this dependency has a different package name
|
|
||||||
let actual_dep_name = match dep_config {
|
|
||||||
CrateDependency::Detailed { other, .. } => {
|
|
||||||
// Check if there's a "package" field that renames the dependency
|
|
||||||
if let Some(toml::Value::String(package_name)) = other.get("package") {
|
|
||||||
package_name.clone()
|
|
||||||
} else {
|
|
||||||
dep_name.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => dep_name.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_actual_workspace_crate = workspace_crate_names.contains(&actual_dep_name);
|
|
||||||
|
|
||||||
if is_workspace_crate || is_actual_workspace_crate || is_local_dep {
|
|
||||||
dependencies.push(actual_dep_name);
|
|
||||||
} else {
|
|
||||||
external_dependencies.push(actual_dep_name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
crates.push(CrateInfo {
|
|
||||||
name: crate_toml.package.name.clone(),
|
|
||||||
path: crate_path,
|
|
||||||
dependencies,
|
|
||||||
external_dependencies,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter dependencies to only include workspace crates
|
|
||||||
for crate_info in &mut crates {
|
|
||||||
crate_info.dependencies.retain(|dep| workspace_crate_names.contains(dep));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove transitive dependencies
|
|
||||||
remove_transitive_dependencies(&mut crates);
|
remove_transitive_dependencies(&mut crates);
|
||||||
|
|
||||||
// Generate DOT format
|
// Generate DOT format and write to output file
|
||||||
let dot_content = generate_dot_format(&crates)?;
|
let dot_content = generate_dot(&crates);
|
||||||
|
|
||||||
// Handle output based on format
|
if let Some(parent) = output_path.parent() {
|
||||||
match args.format {
|
fs::create_dir_all(parent).with_context(|| format!("Failed to create directory {:?}", parent))?;
|
||||||
OutputFormat::Dot => {
|
|
||||||
// Write DOT output
|
|
||||||
if let Some(output_path) = args.output {
|
|
||||||
fs::write(&output_path, &dot_content).with_context(|| format!("Failed to write to {:?}", output_path))?;
|
|
||||||
println!("DOT output written to: {:?}", output_path);
|
|
||||||
} else {
|
|
||||||
print!("{}", dot_content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutputFormat::Png | OutputFormat::Svg => {
|
|
||||||
// Require output file for PNG/SVG
|
|
||||||
let output_path = args.output.ok_or_else(|| anyhow!("Output file (-o/--output) is required for PNG/SVG formats"))?;
|
|
||||||
|
|
||||||
// Check if dot command is available
|
|
||||||
let dot_check = Command::new("dot").arg("-V").output();
|
|
||||||
if dot_check.is_err() || !dot_check.as_ref().unwrap().status.success() {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"GraphViz 'dot' command not found. Please install GraphViz to generate PNG/SVG output.\n\
|
|
||||||
On Ubuntu/Debian: sudo apt-get install graphviz\n\
|
|
||||||
On macOS: brew install graphviz\n\
|
|
||||||
On Windows: Download from https://graphviz.org/download/"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the format argument for dot
|
|
||||||
let format_arg = match args.format {
|
|
||||||
OutputFormat::Png => "png",
|
|
||||||
OutputFormat::Svg => "svg",
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run dot command to generate the output
|
|
||||||
let mut dot_process = Command::new("dot")
|
|
||||||
.arg(format!("-T{}", format_arg))
|
|
||||||
.arg("-o")
|
|
||||||
.arg(&output_path)
|
|
||||||
.stdin(std::process::Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.with_context(|| "Failed to spawn 'dot' command")?;
|
|
||||||
|
|
||||||
// Write DOT content to stdin
|
|
||||||
use std::io::Write;
|
|
||||||
if let Some(mut stdin) = dot_process.stdin.take() {
|
|
||||||
stdin.write_all(dot_content.as_bytes()).with_context(|| "Failed to write DOT content to 'dot' command")?;
|
|
||||||
// Close stdin to signal EOF
|
|
||||||
drop(stdin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the command to complete
|
|
||||||
let status = dot_process.wait().with_context(|| "Failed to wait for 'dot' command")?;
|
|
||||||
if !status.success() {
|
|
||||||
return Err(anyhow!("'dot' command failed with exit code: {:?}", status.code()));
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{} output written to: {:?}", format_arg.to_uppercase(), output_path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
fs::write(&output_path, &dot_content).with_context(|| format!("Failed to write to {:?}", output_path))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_dot_format(crates: &[CrateInfo]) -> Result<String> {
|
fn generate_dot(crates: &[CrateInfo]) -> String {
|
||||||
let mut output = String::new();
|
let mut out = String::new();
|
||||||
output.push_str("digraph CrateHierarchy {\n");
|
out.push_str("digraph CrateHierarchy {\n");
|
||||||
output.push_str(" rankdir=LR;\n");
|
out.push_str(" rankdir=LR;\n");
|
||||||
output.push_str(" node [shape=box, style=\"rounded,filled\", fillcolor=lightblue];\n");
|
out.push_str(" node [shape=box, style=\"rounded,filled\", fillcolor=lightblue];\n");
|
||||||
output.push_str(" edge [color=gray];\n\n");
|
out.push_str(" edge [color=gray];\n\n");
|
||||||
|
|
||||||
// Add subgraphs for different categories
|
// Define subgraph clusters
|
||||||
output.push_str(" subgraph cluster_core {\n");
|
let clusters: &[(&str, &str, &str, Box<dyn Fn(&CrateInfo) -> bool>)] = &[
|
||||||
output.push_str(" label=\"Core Components\";\n");
|
(
|
||||||
output.push_str(" style=filled;\n");
|
"cluster_core",
|
||||||
output.push_str(" fillcolor=lightgray;\n");
|
"Core Components",
|
||||||
|
"lightgray",
|
||||||
|
Box::new(|c| (c.name.starts_with("graphite-") || c.name == "editor" || c.name == "graphene-cli") && !c.name.contains("desktop")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cluster_nodegraph",
|
||||||
|
"Node Graph System",
|
||||||
|
"lightyellow",
|
||||||
|
Box::new(|c| c.name == "graph-craft" || c.name == "interpreted-executor" || c.name == "node-macro" || c.name == "preprocessor" || c.name == "graphene-cli"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cluster_node_libraries",
|
||||||
|
"Node Graph Libraries",
|
||||||
|
"lightcyan",
|
||||||
|
Box::new(|c| c.path.to_string_lossy().replace('\\', "/").contains("node-graph/libraries")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cluster_nodes",
|
||||||
|
"Nodes",
|
||||||
|
"lightblue",
|
||||||
|
Box::new(|c| c.path.to_string_lossy().replace('\\', "/").contains("node-graph/nodes")),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cluster_desktop",
|
||||||
|
"Desktop",
|
||||||
|
"lightgreen",
|
||||||
|
Box::new(|c| c.path.to_string_lossy().replace('\\', "/").contains("desktop")),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
let core_crates: Vec<_> = crates
|
for (id, label, color, filter) in clusters {
|
||||||
.iter()
|
out.push_str(&format!(" subgraph {id} {{\n"));
|
||||||
.filter(|c| (c.name.starts_with("graphite-") || c.name == "editor" || c.name == "graphene-cli") && !c.name.contains("desktop"))
|
out.push_str(&format!(" label=\"{label}\";\n"));
|
||||||
.collect();
|
out.push_str(" style=filled;\n");
|
||||||
|
out.push_str(&format!(" fillcolor={color};\n"));
|
||||||
for crate_info in &core_crates {
|
for c in crates.iter().filter(|c| filter(c)) {
|
||||||
output.push_str(&format!(" \"{}\";\n", crate_info.name));
|
out.push_str(&format!(" \"{}\";\n", c.name));
|
||||||
|
}
|
||||||
|
out.push_str(" }\n\n");
|
||||||
}
|
}
|
||||||
output.push_str(" }\n\n");
|
|
||||||
|
|
||||||
output.push_str(" subgraph cluster_nodegraph {\n");
|
// Add dependency edges
|
||||||
output.push_str(" label=\"Node Graph System\";\n");
|
|
||||||
output.push_str(" style=filled;\n");
|
|
||||||
output.push_str(" fillcolor=lightyellow;\n");
|
|
||||||
|
|
||||||
let nodegraph_crates: Vec<_> = crates
|
|
||||||
.iter()
|
|
||||||
.filter(|c| c.name == "graph-craft" || c.name == "interpreted-executor" || c.name == "node-macro" || c.name == "preprocessor" || c.name == "graphene-cli")
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for crate_info in &nodegraph_crates {
|
|
||||||
output.push_str(&format!(" \"{}\";\n", crate_info.name));
|
|
||||||
}
|
|
||||||
output.push_str(" }\n\n");
|
|
||||||
|
|
||||||
output.push_str(" subgraph cluster_node_libraries {\n");
|
|
||||||
output.push_str(" label=\"Node Graph Libraries\";\n");
|
|
||||||
output.push_str(" style=filled;\n");
|
|
||||||
output.push_str(" fillcolor=lightcyan;\n");
|
|
||||||
|
|
||||||
let node_library_crates: Vec<_> = crates
|
|
||||||
.iter()
|
|
||||||
.filter(|c| {
|
|
||||||
let path_str = c.path.to_string_lossy();
|
|
||||||
path_str.contains("node-graph/libraries")
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for crate_info in &node_library_crates {
|
|
||||||
output.push_str(&format!(" \"{}\";\n", crate_info.name));
|
|
||||||
}
|
|
||||||
output.push_str(" }\n\n");
|
|
||||||
|
|
||||||
output.push_str(" subgraph cluster_nodes {\n");
|
|
||||||
output.push_str(" label=\"Nodes\";\n");
|
|
||||||
output.push_str(" style=filled;\n");
|
|
||||||
output.push_str(" fillcolor=lightblue;\n");
|
|
||||||
|
|
||||||
let node_crates: Vec<_> = crates
|
|
||||||
.iter()
|
|
||||||
.filter(|c| {
|
|
||||||
let path_str = c.path.to_string_lossy();
|
|
||||||
path_str.contains("node-graph/nodes")
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for crate_info in &node_crates {
|
|
||||||
output.push_str(&format!(" \"{}\";\n", crate_info.name));
|
|
||||||
}
|
|
||||||
output.push_str(" }\n\n");
|
|
||||||
|
|
||||||
output.push_str(" subgraph cluster_desktop{\n");
|
|
||||||
output.push_str(" label=\"Desktop\";\n");
|
|
||||||
output.push_str(" style=filled;\n");
|
|
||||||
output.push_str(" fillcolor=lightgreen;\n");
|
|
||||||
|
|
||||||
let desktop_crates: Vec<_> = crates
|
|
||||||
.iter()
|
|
||||||
.filter(|c| {
|
|
||||||
let path_str = c.path.to_string_lossy();
|
|
||||||
path_str.contains("desktop")
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for crate_info in &desktop_crates {
|
|
||||||
output.push_str(&format!(" \"{}\";\n", crate_info.name));
|
|
||||||
}
|
|
||||||
output.push_str(" }\n\n");
|
|
||||||
|
|
||||||
// Add dependencies as edges
|
|
||||||
for crate_info in crates {
|
for crate_info in crates {
|
||||||
for dep in &crate_info.dependencies {
|
for dep in &crate_info.dependencies {
|
||||||
if dep == "dyn-any" || dep == "node-macro" {
|
if dep == "dyn-any" || dep == "node-macro" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
output.push_str(&format!(" \"{}\" -> \"{}\";\n", crate_info.name, dep));
|
out.push_str(&format!(" \"{}\" -> \"{}\";\n", crate_info.name, dep));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output.push_str("}\n");
|
out.push_str("}\n");
|
||||||
Ok(output)
|
out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
use editor::messages::message::Message;
|
use editor::messages::message::Message;
|
||||||
use editor::utility_types::DebugMessageTree;
|
use editor::utility_types::DebugMessageTree;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let output_path = std::env::args_os().nth(1).map(PathBuf::from).ok_or("Usage: editor-message-tree <output-file>")?;
|
||||||
|
|
||||||
|
if let Some(parent) = output_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let result = Message::message_tree();
|
let result = Message::message_tree();
|
||||||
std::fs::create_dir_all("../../website/generated").unwrap();
|
let mut file = std::fs::File::create(&output_path).unwrap();
|
||||||
let mut file = std::fs::File::create("../../website/generated/hierarchical_message_system_tree.txt").unwrap();
|
|
||||||
file.write_all(format!("{} `{}#L{}`\n", result.name(), result.path(), result.line_number()).as_bytes()).unwrap();
|
file.write_all(format!("{} `{}#L{}`\n", result.name(), result.path(), result.line_number()).as_bytes()).unwrap();
|
||||||
if let Some(variants) = result.variants() {
|
if let Some(variants) = result.variants() {
|
||||||
for (i, variant) in variants.iter().enumerate() {
|
for (i, variant) in variants.iter().enumerate() {
|
||||||
@@ -13,6 +19,8 @@ fn main() {
|
|||||||
print_tree_node(variant, "", is_last, &mut file);
|
print_tree_node(variant, "", is_last, &mut file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
|
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ use crate::utility::*;
|
|||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn main() {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let output_path = std::env::args_os()
|
||||||
|
.nth(1)
|
||||||
|
.ok_or("Usage: node-docs <output-directory>")?
|
||||||
|
.into_string()
|
||||||
|
.map_err(|_| "Output path is not valid UTF-8")?;
|
||||||
|
|
||||||
// TODO: Also obtain document nodes, not only proto nodes
|
// TODO: Also obtain document nodes, not only proto nodes
|
||||||
let nodes = graphene_std::registry::NODE_METADATA.lock().unwrap();
|
let nodes = graphene_std::registry::NODE_METADATA.lock().unwrap();
|
||||||
|
|
||||||
@@ -22,7 +28,7 @@ fn main() {
|
|||||||
categories.sort();
|
categories.sort();
|
||||||
|
|
||||||
// Create _index.md for the node catalog page
|
// Create _index.md for the node catalog page
|
||||||
page_catalog::write_catalog_index_page(&categories);
|
page_catalog::write_catalog_index_page(&output_path, &categories);
|
||||||
|
|
||||||
// Create node category pages and individual node pages
|
// Create node category pages and individual node pages
|
||||||
for (index, category) in categories.iter().map(|c| if !OMIT_HIDDEN && c.is_empty() { "Hidden" } else { c }).filter(|c| !c.is_empty()).enumerate() {
|
for (index, category) in categories.iter().map(|c| if !OMIT_HIDDEN && c.is_empty() { "Hidden" } else { c }).filter(|c| !c.is_empty()).enumerate() {
|
||||||
@@ -32,7 +38,7 @@ fn main() {
|
|||||||
|
|
||||||
// Create _index.md file for category
|
// Create _index.md file for category
|
||||||
let category_path_part = sanitize_path(&category.to_case(Case::Kebab));
|
let category_path_part = sanitize_path(&category.to_case(Case::Kebab));
|
||||||
let category_path = format!("{NODE_CATALOG_PATH}/{category_path_part}");
|
let category_path = format!("{output_path}/{category_path_part}");
|
||||||
page_category::write_category_index_page(index, category, &nodes, &category_path);
|
page_category::write_category_index_page(index, category, &nodes, &category_path);
|
||||||
|
|
||||||
// Create individual node pages
|
// Create individual node pages
|
||||||
@@ -40,4 +46,6 @@ fn main() {
|
|||||||
page_node::write_node_page(index, id, metadata, &category_path);
|
page_node::write_node_page(index, id, metadata, &category_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ use convert_case::{Case, Casing};
|
|||||||
use indoc::formatdoc;
|
use indoc::formatdoc;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
pub fn write_catalog_index_page(categories: &[String]) {
|
pub fn write_catalog_index_page(output_path: &str, categories: &[String]) {
|
||||||
if std::path::Path::new(NODE_CATALOG_PATH).exists() {
|
if std::path::Path::new(output_path).exists() {
|
||||||
std::fs::remove_dir_all(NODE_CATALOG_PATH).expect("Failed to remove existing node catalog directory");
|
std::fs::remove_dir_all(output_path).expect("Failed to remove existing node catalog directory");
|
||||||
}
|
}
|
||||||
std::fs::create_dir_all(NODE_CATALOG_PATH).expect("Failed to create node catalog directory");
|
std::fs::create_dir_all(output_path).expect("Failed to create node catalog directory");
|
||||||
let page_path = format!("{NODE_CATALOG_PATH}/_index.md");
|
let page_path = format!("{output_path}/_index.md");
|
||||||
let mut page = std::fs::File::create(&page_path).expect("Failed to create index file");
|
let mut page = std::fs::File::create(&page_path).expect("Failed to create index file");
|
||||||
|
|
||||||
write_frontmatter(&mut page);
|
write_frontmatter(&mut page);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use graph_craft::proto::NodeMetadata;
|
use graph_craft::proto::NodeMetadata;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
pub const NODE_CATALOG_PATH: &str = "../../website/content/learn/node-catalog";
|
|
||||||
pub const OMIT_HIDDEN: bool = true;
|
pub const OMIT_HIDDEN: bool = true;
|
||||||
|
|
||||||
pub fn category_description(category: &str) -> &str {
|
pub fn category_description(category: &str) -> &str {
|
||||||
|
|||||||
19
website/.build-scripts/generate-crate-hierarchy.ts
Normal file
19
website/.build-scripts/generate-crate-hierarchy.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
import { instance } from "@viz-js/viz";
|
||||||
|
|
||||||
|
const [inputFile, outputFile] = process.argv.slice(2);
|
||||||
|
if (!inputFile || !outputFile) {
|
||||||
|
console.error("Usage: node generate-crate-hierarchy.ts <input.dot> <output.svg>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dot = fs.readFileSync(inputFile, "utf-8");
|
||||||
|
|
||||||
|
const viz = await instance();
|
||||||
|
const svg = viz.renderString(dot, { format: "svg" });
|
||||||
|
|
||||||
|
fs.writeFileSync(outputFile, svg);
|
||||||
|
console.log(`SVG output written to: ${outputFile}`);
|
||||||
@@ -5,8 +5,8 @@ page_template = "book.html"
|
|||||||
|
|
||||||
[extra]
|
[extra]
|
||||||
order = 2 # Chapter number
|
order = 2 # Chapter number
|
||||||
js = ["/js/component/youtube-embed.js"]
|
js = ["/js/component/youtube-embed.js", "/js/page/contributor-guide/crate-hierarchy.js"]
|
||||||
css = ["/component/youtube-embed.css"]
|
css = ["/component/youtube-embed.css", "/page/contributor-guide/crate-hierarchy.css"]
|
||||||
+++
|
+++
|
||||||
|
|
||||||
The best introduction for getting up-to-speed with Graphite contribution comes from watching this webcast recording. Before asking questions in Discord, please watch the full video because it gives a comprehensive overview of most things you will need to know.
|
The best introduction for getting up-to-speed with Graphite contribution comes from watching this webcast recording. Before asking questions in Discord, please watch the full video because it gives a comprehensive overview of most things you will need to know.
|
||||||
@@ -37,6 +37,16 @@ The frontend is the GUI for Graphite which users see and interact with. It is bu
|
|||||||
|
|
||||||
[Graphene](../graphene/) is the node graph engine which manages and renders the documents. It is itself a programming language, where Graphene programs are compiled while being edited live by the user, and where executing the program renders the document.
|
[Graphene](../graphene/) is the node graph engine which manages and renders the documents. It is itself a programming language, where Graphene programs are compiled while being edited live by the user, and where executing the program renders the document.
|
||||||
|
|
||||||
|
## Crate dependency graph
|
||||||
|
|
||||||
|
This diagram shows the structure of the crates that comprise the Graphite codebase and how they depend on each other. Every Arrow points from a crate to another which it depends on.
|
||||||
|
|
||||||
|
<div class="crate-hierarchy">
|
||||||
|
|
||||||
|
<!-- replacements::crate_hierarchy() -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## Frontend/backend communication
|
## Frontend/backend communication
|
||||||
|
|
||||||
Frontend-to-backend communication is achieved through a thin Rust translation layer in [`/frontend/wasm/src/editor_api.rs`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/wasm/src/editor_api.rs) which wraps the editor backend's Rust-based message system API and provides the TypeScript-compatible API of callable functions. These wrapper functions are compiled by [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) into autogenerated TS functions that serve as an entry point from TS into the Wasm binary.
|
Frontend-to-backend communication is achieved through a thin Rust translation layer in [`/frontend/wasm/src/editor_api.rs`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/wasm/src/editor_api.rs) which wraps the editor backend's Rust-based message system API and provides the TypeScript-compatible API of callable functions. These wrapper functions are compiled by [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) into autogenerated TS functions that serve as an entry point from TS into the Wasm binary.
|
||||||
|
|||||||
8
website/package-lock.json
generated
8
website/package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"@eslint/eslintrc": "^3.3.3",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@types/node": "^25.0.9",
|
"@types/node": "^25.0.9",
|
||||||
|
"@viz-js/viz": "^3.25.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
@@ -1232,6 +1233,13 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@viz-js/viz": {
|
||||||
|
"version": "3.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@viz-js/viz/-/viz-3.25.0.tgz",
|
||||||
|
"integrity": "sha512-dM7zAYMdf7mcRz5Kdb+YJb6+qv5Rjk0rPZ18gROdpMrP/3S7RFOp8uxybeiz5RypHrE1zo1vccA8Twh4mIcLZw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "node .build-scripts/install.ts",
|
"postinstall": "node .build-scripts/install.ts",
|
||||||
"generate-editor-structure": "node .build-scripts/generate-editor-structure.ts generated/hierarchical_message_system_tree.txt generated/hierarchical_message_system_tree.html",
|
"generate-editor-structure": "node .build-scripts/generate-editor-structure.ts generated/hierarchical_message_system_tree.txt generated/hierarchical_message_system_tree.html",
|
||||||
|
"generate-crate-hierarchy": "node .build-scripts/generate-crate-hierarchy.ts generated/crate_hierarchy.dot generated/crate_hierarchy.svg",
|
||||||
"check": "tsc --noEmit && eslint",
|
"check": "tsc --noEmit && eslint",
|
||||||
"fix": "eslint --fix"
|
"fix": "eslint --fix"
|
||||||
},
|
},
|
||||||
@@ -21,11 +22,12 @@
|
|||||||
"@eslint/eslintrc": "^3.3.3",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@types/node": "^25.0.9",
|
"@types/node": "^25.0.9",
|
||||||
|
"@viz-js/viz": "^3.25.0",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-prettier": "^5.5.5",
|
"eslint-plugin-prettier": "^5.5.5",
|
||||||
"eslint": "^9.39.2",
|
|
||||||
"prettier": "^3.8.0",
|
"prettier": "^3.8.0",
|
||||||
"sass": "1.97.2",
|
"sass": "1.97.2",
|
||||||
"tar": "^7.5.6",
|
"tar": "^7.5.6",
|
||||||
|
|||||||
73
website/sass/page/contributor-guide/crate-hierarchy.scss
Normal file
73
website/sass/page/contributor-guide/crate-hierarchy.scss
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
.crate-hierarchy {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.crate-hierarchy-controls {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
button {
|
||||||
|
position: relative;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: var(--color-navy);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// + and - icon geometry
|
||||||
|
&::before,
|
||||||
|
&.zoom-in::after {
|
||||||
|
content: "";
|
||||||
|
background: white;
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 14px;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.zoom-in::after {
|
||||||
|
width: 2px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.crate-hierarchy-viewport {
|
||||||
|
touch-action: none;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: grab;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
181
website/static/js/page/contributor-guide/crate-hierarchy.js
Normal file
181
website/static/js/page/contributor-guide/crate-hierarchy.js
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const container = document.querySelector(".crate-hierarchy");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const svg = container.querySelector("svg");
|
||||||
|
if (!svg) return;
|
||||||
|
|
||||||
|
// Wrap SVG in a viewport container
|
||||||
|
const viewport = document.createElement("div");
|
||||||
|
viewport.className = "crate-hierarchy-viewport";
|
||||||
|
svg?.parentNode?.insertBefore(viewport, svg);
|
||||||
|
viewport.appendChild(svg);
|
||||||
|
|
||||||
|
// Remove any width/height attributes so CSS controls sizing
|
||||||
|
svg.removeAttribute("width");
|
||||||
|
svg.removeAttribute("height");
|
||||||
|
|
||||||
|
// Create zoom controls
|
||||||
|
const controls = document.createElement("div");
|
||||||
|
controls.className = "crate-hierarchy-controls";
|
||||||
|
controls.innerHTML = `<button class="zoom-in"></button><button class="zoom-out"></button>`;
|
||||||
|
container.insertBefore(controls, viewport);
|
||||||
|
const zoomInBtn = controls.querySelector(".zoom-in");
|
||||||
|
const zoomOutBtn = controls.querySelector(".zoom-out");
|
||||||
|
if (!(zoomInBtn instanceof HTMLButtonElement) || !(zoomOutBtn instanceof HTMLButtonElement)) return;
|
||||||
|
|
||||||
|
// Lock the viewport height to the SVG's natural rendered height (ignoring any zoom transform)
|
||||||
|
const updateViewportHeight = () => {
|
||||||
|
const prevTransform = svg.style.transform;
|
||||||
|
svg.style.transform = "";
|
||||||
|
viewport.style.height = `${svg.getBoundingClientRect().height}px`;
|
||||||
|
svg.style.transform = prevTransform;
|
||||||
|
};
|
||||||
|
updateViewportHeight();
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
updateViewportHeight();
|
||||||
|
applyTransform();
|
||||||
|
});
|
||||||
|
|
||||||
|
const MIN_SCALE = 1;
|
||||||
|
const MAX_SCALE = 4;
|
||||||
|
const ZOOM_STEP = 0.15;
|
||||||
|
const BUTTON_ZOOM_STEP = 0.5;
|
||||||
|
const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
|
let scale = MIN_SCALE;
|
||||||
|
let panX = 0;
|
||||||
|
let panY = 0;
|
||||||
|
let animationFrameId = 0;
|
||||||
|
let isDragging = false;
|
||||||
|
let dragStartX = 0;
|
||||||
|
let dragStartY = 0;
|
||||||
|
let panStartX = 0;
|
||||||
|
let panStartY = 0;
|
||||||
|
|
||||||
|
function clampPan() {
|
||||||
|
const viewportRect = viewport.getBoundingClientRect();
|
||||||
|
const viewportW = viewportRect.width;
|
||||||
|
const viewportH = viewportRect.height;
|
||||||
|
|
||||||
|
// The SVG is scaled to fill the viewport width at scale=1
|
||||||
|
const scaledW = viewportW * scale;
|
||||||
|
const scaledH = svg?.getBoundingClientRect()?.height || 0;
|
||||||
|
|
||||||
|
// How much overflow exists on each axis
|
||||||
|
const overflowX = Math.max(0, scaledW - viewportW);
|
||||||
|
const overflowY = Math.max(0, scaledH - viewportH);
|
||||||
|
|
||||||
|
// Pan is constrained so scaled content edges don't pull away from viewport edges
|
||||||
|
panX = Math.min(0, Math.max(-overflowX, panX));
|
||||||
|
panY = Math.min(0, Math.max(-overflowY, panY));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateButtons() {
|
||||||
|
if (zoomInBtn instanceof HTMLButtonElement) zoomInBtn.disabled = scale >= MAX_SCALE;
|
||||||
|
if (zoomOutBtn instanceof HTMLButtonElement) zoomOutBtn.disabled = scale <= MIN_SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTransform() {
|
||||||
|
clampPan();
|
||||||
|
if (svg) svg.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomAt(/** @type {number} */ clientX, /** @type {number} */ clientY, /** @type {number} */ newScale) {
|
||||||
|
const viewportRect = viewport.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Point in viewport-local coordinates
|
||||||
|
const pointX = clientX - viewportRect.left;
|
||||||
|
const pointY = clientY - viewportRect.top;
|
||||||
|
|
||||||
|
// Where this point maps in the pre-zoom content
|
||||||
|
const contentX = (pointX - panX) / scale;
|
||||||
|
const contentY = (pointY - panY) / scale;
|
||||||
|
|
||||||
|
scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale));
|
||||||
|
|
||||||
|
// Adjust pan so the same content point stays under the cursor
|
||||||
|
panX = pointX - contentX * scale;
|
||||||
|
panY = pointY - contentY * scale;
|
||||||
|
|
||||||
|
applyTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateZoomAt(/** @type {number} */ clientX, /** @type {number} */ clientY, /** @type {number} */ newTargetScale) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
|
||||||
|
const targetScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newTargetScale));
|
||||||
|
const startScale = scale;
|
||||||
|
const startPanX = panX;
|
||||||
|
const startPanY = panY;
|
||||||
|
|
||||||
|
const viewportRect = viewport.getBoundingClientRect();
|
||||||
|
const pointX = clientX - viewportRect.left;
|
||||||
|
const pointY = clientY - viewportRect.top;
|
||||||
|
const contentX = (pointX - panX) / scale;
|
||||||
|
const contentY = (pointY - panY) / scale;
|
||||||
|
|
||||||
|
const targetPanX = pointX - contentX * targetScale;
|
||||||
|
const targetPanY = pointY - contentY * targetScale;
|
||||||
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
const step = (/** @type {number} */ now) => {
|
||||||
|
const t = Math.min(1, (now - startTime) / ANIMATION_DURATION);
|
||||||
|
const ease = t * (2 - t); // ease-out quadratic
|
||||||
|
scale = startScale + (targetScale - startScale) * ease;
|
||||||
|
panX = startPanX + (targetPanX - startPanX) * ease;
|
||||||
|
panY = startPanY + (targetPanY - startPanY) * ease;
|
||||||
|
applyTransform();
|
||||||
|
if (t < 1) animationFrameId = requestAnimationFrame(step);
|
||||||
|
};
|
||||||
|
animationFrameId = requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll wheel zoom
|
||||||
|
viewport.addEventListener(
|
||||||
|
"wheel",
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
|
||||||
|
zoomAt(e.clientX, e.clientY, scale + delta);
|
||||||
|
},
|
||||||
|
{ passive: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Button zoom (animated, zoom toward center of viewport)
|
||||||
|
zoomInBtn?.addEventListener("click", () => {
|
||||||
|
const rect = viewport.getBoundingClientRect();
|
||||||
|
animateZoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, scale + BUTTON_ZOOM_STEP);
|
||||||
|
});
|
||||||
|
zoomOutBtn?.addEventListener("click", () => {
|
||||||
|
const rect = viewport.getBoundingClientRect();
|
||||||
|
animateZoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, scale - BUTTON_ZOOM_STEP);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click-drag to pan
|
||||||
|
viewport.addEventListener("pointerdown", (e) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
e.preventDefault();
|
||||||
|
isDragging = true;
|
||||||
|
dragStartX = e.clientX;
|
||||||
|
dragStartY = e.clientY;
|
||||||
|
panStartX = panX;
|
||||||
|
panStartY = panY;
|
||||||
|
viewport.setPointerCapture(e.pointerId);
|
||||||
|
viewport.style.cursor = "grabbing";
|
||||||
|
});
|
||||||
|
window.addEventListener("pointermove", (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
panX = panStartX + (e.clientX - dragStartX);
|
||||||
|
panY = panStartY + (e.clientY - dragStartY);
|
||||||
|
applyTransform();
|
||||||
|
});
|
||||||
|
window.addEventListener("pointerup", () => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
isDragging = false;
|
||||||
|
viewport.style.cursor = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
applyTransform();
|
||||||
|
});
|
||||||
@@ -133,10 +133,12 @@
|
|||||||
{%- filter replace(from = "<!-- replacements::blog_posts(count = 2) -->", to = replacements::blog_posts(count = 2)) -%}
|
{%- filter replace(from = "<!-- replacements::blog_posts(count = 2) -->", to = replacements::blog_posts(count = 2)) -%}
|
||||||
{%- filter replace(from = "<!-- replacements::text_balancer() -->", to = replacements::text_balancer()) -%}
|
{%- filter replace(from = "<!-- replacements::text_balancer() -->", to = replacements::text_balancer()) -%}
|
||||||
{%- filter replace(from = "<!-- replacements::hierarchical_message_system_tree() -->", to = replacements::hierarchical_message_system_tree()) -%}
|
{%- filter replace(from = "<!-- replacements::hierarchical_message_system_tree() -->", to = replacements::hierarchical_message_system_tree()) -%}
|
||||||
|
{%- filter replace(from = "<!-- replacements::crate_hierarchy() -->", to = replacements::crate_hierarchy()) -%}
|
||||||
{%- block content -%}{%- endblock -%}
|
{%- block content -%}{%- endblock -%}
|
||||||
{%- endfilter -%}
|
{%- endfilter -%}
|
||||||
{%- endfilter -%}
|
{%- endfilter -%}
|
||||||
{%- endfilter -%}
|
{%- endfilter -%}
|
||||||
|
{%- endfilter -%}
|
||||||
{# This is a comment. It exists to prevent the {%- -%} on the lines above from removing the line break between the `content` block and `</main>` #}
|
{# This is a comment. It exists to prevent the {%- -%} on the lines above from removing the line break between the `content` block and `</main>` #}
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
|||||||
@@ -45,9 +45,20 @@
|
|||||||
|
|
||||||
TO TEST IT LOCALLY, FROM THE ROOT OF THE PROJECT, RUN:
|
TO TEST IT LOCALLY, FROM THE ROOT OF THE PROJECT, RUN:
|
||||||
|
|
||||||
cd tools/editor-message-tree
|
cargo run -p editor-message-tree -- website/generated/hierarchical_message_system_tree.txt
|
||||||
cargo run
|
cd website
|
||||||
cd ../../website
|
|
||||||
npm run generate-editor-structure</pre>" -%}
|
npm run generate-editor-structure</pre>" -%}
|
||||||
{{ content | default(value = fallback) | safe }}
|
{{ content | default(value = fallback) | safe }}
|
||||||
{% endmacro hierarchical_message_system_tree %}
|
{% endmacro hierarchical_message_system_tree %}
|
||||||
|
|
||||||
|
{% macro crate_hierarchy() %}
|
||||||
|
{%- set content = load_data(path = "../generated/crate_hierarchy.svg", format = "plain", required = false) -%}
|
||||||
|
{%- set fallback = "<pre>THIS CONTENT IS FILLED IN WHEN CI BUILDS THE WEBSITE.
|
||||||
|
|
||||||
|
TO TEST IT LOCALLY, FROM THE ROOT OF THE PROJECT, RUN:
|
||||||
|
|
||||||
|
cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.dot
|
||||||
|
cd website
|
||||||
|
npm run generate-crate-hierarchy</pre>" -%}
|
||||||
|
{{ content | default(value = fallback) | safe }}
|
||||||
|
{% endmacro crate_hierarchy %}
|
||||||
|
|||||||
Reference in New Issue
Block a user