Port website dev docs generated content scripts from JS to Rust to avoid intermediate parsing (#3909)
* Remove the crate dependency graph website generation intermediate generation step * Remove the message system tree website generation intermediate generation step * Code cleanup * Remove cache system * Fix Windows comment URL error * Fix incorrect artifact download URLs * Add Flatpak to comment * Make Flatpak use debug/release mode choice instead of always release mode
This commit is contained in:
parent
d9214c7292
commit
187b4c38b4
|
|
@ -145,7 +145,7 @@ jobs:
|
|||
gh api \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
/repos/${{ github.repository }}/commits/$(git rev-parse HEAD)/comments \
|
||||
repos/${{ github.repository }}/commits/$(git rev-parse HEAD)/comments \
|
||||
-f body="$COMMENT_BODY"
|
||||
else
|
||||
# Comment on the PR (use provided PR number from !build, or look it up by branch name)
|
||||
|
|
@ -172,41 +172,16 @@ jobs:
|
|||
name: graphite-web-bundle
|
||||
path: frontend/dist
|
||||
|
||||
- name: 📃 Generate code documentation info for website
|
||||
- name: 📃 Trigger website rebuild if auto-generated code docs are stale
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
mkdir -p website/generated-new
|
||||
cargo run -p crate-hierarchy-viz -- website/generated-new/crate_hierarchy.dot
|
||||
cargo run -p editor-message-tree -- website/generated-new/hierarchical_message_system_tree.txt
|
||||
|
||||
- name: 💿 Obtain cache of auto-generated code docs artifacts, to check if they've changed
|
||||
if: github.event_name == 'push'
|
||||
id: cache-website-code-docs
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: website/generated
|
||||
key: website-code-docs
|
||||
|
||||
- name: 🔍 Check if auto-generated code docs artifacts changed
|
||||
if: github.event_name == 'push'
|
||||
id: website-code-docs-changed
|
||||
run: |
|
||||
diff --brief --recursive website/generated-new website/generated || echo "changed=true" >> $GITHUB_OUTPUT
|
||||
rm -rf website/generated
|
||||
mv website/generated-new website/generated
|
||||
|
||||
- name: 💾 Save cache of auto-generated code docs artifacts
|
||||
if: github.event_name == 'push' && steps.website-code-docs-changed.outputs.changed == 'true'
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: website/generated
|
||||
key: ${{ steps.cache-website-code-docs.outputs.cache-primary-key }}
|
||||
|
||||
- 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'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run website.yml --ref master
|
||||
run: |
|
||||
cargo run -p editor-message-tree -- website/generated
|
||||
TREE=volunteer/guide/codebase-overview/hierarchical-message-system-tree
|
||||
curl -sf "https://graphite.art/$TREE.txt" -o "website/static/$TREE.live.txt" \
|
||||
&& diff -q "website/static/$TREE.txt" "website/static/$TREE.live.txt" > /dev/null \
|
||||
|| gh workflow run website.yml --ref master
|
||||
|
||||
windows:
|
||||
if: github.event_name == 'push' || inputs.windows
|
||||
|
|
@ -302,16 +277,18 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ARTIFACT_URL=$(gh api /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-windows-bundle") | .archive_download_url')
|
||||
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-windows-bundle") | .id')
|
||||
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
|
||||
fi
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_URL" ]; then
|
||||
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| 📦 **Windows Build Complete for** $(git rev-parse HEAD) |
|
||||
|-|
|
||||
| [Download artifact]($ARTIFACT_URL) |"
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then
|
||||
BODY="| 📦 **Windows Build Complete for** $(git rev-parse HEAD) |"$'\n'
|
||||
BODY+="|-|"$'\n'
|
||||
BODY+="| [Download binary]($ARTIFACT_URL) |"
|
||||
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$BODY"
|
||||
fi
|
||||
|
||||
- name: 🔑 Azure login
|
||||
|
|
@ -488,16 +465,18 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ARTIFACT_URL=$(gh api /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-mac-bundle") | .archive_download_url')
|
||||
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-mac-bundle") | .id')
|
||||
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
|
||||
fi
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_URL" ]; then
|
||||
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| 📦 **Mac Build Complete for** $(git rev-parse HEAD) |
|
||||
|-|
|
||||
| [Download artifact]($ARTIFACT_URL) |"
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then
|
||||
BODY="| 📦 **Mac Build Complete for** $(git rev-parse HEAD) |"$'\n'
|
||||
BODY+="|-|"$'\n'
|
||||
BODY+="| [Download binary]($ARTIFACT_URL) |"
|
||||
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$BODY"
|
||||
fi
|
||||
|
||||
- name: 🔏 Sign and notarize (preparation)
|
||||
|
|
@ -616,20 +595,24 @@ jobs:
|
|||
compression-level: 0
|
||||
|
||||
- name: 💬 Comment artifact link on PR
|
||||
id: linux-comment
|
||||
if: github.event_name != 'push'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ARTIFACT_URL=$(gh api /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-linux-bundle") | .archive_download_url')
|
||||
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-linux-bundle") | .id')
|
||||
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
|
||||
fi
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_URL" ]; then
|
||||
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| 📦 **Linux Build Complete for** $(git rev-parse HEAD) |
|
||||
|-|
|
||||
| [Download artifact]($ARTIFACT_URL) |"
|
||||
if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_ID" ]; then
|
||||
BODY="| 📦 **Linux Build Complete for** $(git rev-parse HEAD) |"$'\n'
|
||||
BODY+="|-|"$'\n'
|
||||
BODY+="| [Download binary]($ARTIFACT_URL) |"
|
||||
COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments -f body="$BODY" --jq '.id')
|
||||
echo "comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: 🔧 Install Flatpak tooling
|
||||
|
|
@ -640,7 +623,7 @@ jobs:
|
|||
|
||||
- name: 🏗 Build Flatpak
|
||||
run: |
|
||||
nix build .#graphite-flatpak-manifest
|
||||
nix build .#graphite${{ inputs.debug && '-dev' || '' }}-flatpak-manifest
|
||||
|
||||
rm -rf .flatpak
|
||||
mkdir -p .flatpak
|
||||
|
|
@ -660,3 +643,18 @@ jobs:
|
|||
name: graphite-flatpak
|
||||
path: .flatpak/Graphite.flatpak
|
||||
compression-level: 0
|
||||
|
||||
- name: 💬 Update PR comment with Flatpak artifact link
|
||||
if: github.event_name != 'push' && steps.linux-comment.outputs.comment_id
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-flatpak") | .id')
|
||||
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
|
||||
COMMENT_ID="${{ steps.linux-comment.outputs.comment_id }}"
|
||||
if [ -n "$ARTIFACT_ID" ]; then
|
||||
EXISTING_BODY=$(gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID --jq '.body')
|
||||
BODY="$EXISTING_BODY"$'\n'
|
||||
BODY+="| [Download Flatpak]($ARTIFACT_URL) |"
|
||||
gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID -X PATCH -f body="$BODY"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -49,30 +49,16 @@ jobs:
|
|||
# Remove the INDEX_HTML_HEAD_INCLUSION environment variable for build links (not master deploys)
|
||||
git rev-parse --abbrev-ref HEAD | grep master > /dev/null || export INDEX_HTML_HEAD_INCLUSION=""
|
||||
|
||||
- name: 💿 Obtain cache of auto-generated code docs artifacts
|
||||
id: cache-website-code-docs
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: website/generated
|
||||
key: website-code-docs
|
||||
|
||||
- name: 📁 Fallback in case auto-generated code docs artifacts weren't cached
|
||||
if: steps.cache-website-code-docs.outputs.cache-hit != 'true'
|
||||
- name: 🦀 Produce auto-generated code docs data
|
||||
run: |
|
||||
echo "🦀 Initial system version of Rust:"
|
||||
rustc --version
|
||||
rustup update stable
|
||||
echo "🦀 Latest updated version of Rust:"
|
||||
rustc --version
|
||||
cargo run -p crate-hierarchy-viz -- website/generated/crate_hierarchy.dot
|
||||
cargo run -p editor-message-tree -- website/generated/hierarchical_message_system_tree.txt
|
||||
cargo run -p crate-hierarchy-viz -- website/generated
|
||||
cargo run -p editor-message-tree -- website/generated
|
||||
|
||||
- name: 🔧 Build auto-generated code docs artifacts into HTML/SVG
|
||||
- name: 🔧 Install website npm dependencies
|
||||
run: |
|
||||
cd website
|
||||
npm ci
|
||||
npm run generate-editor-structure
|
||||
npm run generate-crate-hierarchy
|
||||
|
||||
- name: 📃 Generate node catalog documentation
|
||||
run: cargo run -p node-docs -- website/content/learn/node-catalog
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ in
|
|||
graphite-branding = lib.call ./pkgs/graphite-branding.nix;
|
||||
graphite-bundle = (lib.call ./pkgs/graphite-bundle.nix) { };
|
||||
graphite-dev-bundle = (lib.call ./pkgs/graphite-bundle.nix) { graphite = graphite-dev; };
|
||||
graphite-flatpak-manifest = lib.call ./pkgs/graphite-flatpak-manifest.nix;
|
||||
graphite-flatpak-manifest = (lib.call ./pkgs/graphite-flatpak-manifest.nix) { };
|
||||
graphite-dev-flatpak-manifest = (lib.call ./pkgs/graphite-flatpak-manifest.nix) { graphite-bundle = graphite-dev-bundle; };
|
||||
|
||||
# TODO: graphene-cli = lib.call ./pkgs/graphene-cli.nix;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
system,
|
||||
...
|
||||
}:
|
||||
{
|
||||
graphite-bundle ? self.packages.${system}.graphite-bundle,
|
||||
}:
|
||||
|
||||
(pkgs.formats.json { }).generate "art.graphite.Graphite.json" {
|
||||
app-id = "art.graphite.Graphite";
|
||||
|
|
@ -30,7 +33,7 @@
|
|||
sources = [
|
||||
{
|
||||
type = "archive";
|
||||
path = self.packages.${system}.graphite-bundle.tar;
|
||||
path = graphite-bundle.tar;
|
||||
strip-components = 0;
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ use anyhow::{Context, Result};
|
|||
use serde::Deserialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WorkspaceToml {
|
||||
|
|
@ -83,12 +85,13 @@ fn collect_all_dependencies(crate_name: &str, dep_map: &HashMap<String, HashSet<
|
|||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let output_path = std::env::args_os()
|
||||
let output_dir = std::env::args_os()
|
||||
.nth(1)
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow::anyhow!("Usage: crate-hierarchy-viz <output-file>"))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Usage: crate-hierarchy-viz <output-directory>"))?;
|
||||
let output_path = output_dir.join("crate-hierarchy.svg");
|
||||
|
||||
let workspace_root = std::env::current_dir().unwrap();
|
||||
let workspace_root = std::env::current_dir()?;
|
||||
let workspace_toml_path = workspace_root.join("Cargo.toml");
|
||||
|
||||
// Parse workspace Cargo.toml
|
||||
|
|
@ -173,17 +176,79 @@ fn main() -> Result<()> {
|
|||
|
||||
remove_transitive_dependencies(&mut crates);
|
||||
|
||||
// Generate DOT format and write to output file
|
||||
// Generate DOT format, convert to SVG, and write to output file
|
||||
let dot_content = generate_dot(&crates);
|
||||
let svg_content = dot_to_svg(&dot_content)?;
|
||||
|
||||
if let Some(parent) = output_path.parent() {
|
||||
fs::create_dir_all(parent).with_context(|| format!("Failed to create directory {:?}", parent))?;
|
||||
}
|
||||
fs::write(&output_path, &dot_content).with_context(|| format!("Failed to write to {:?}", output_path))?;
|
||||
fs::create_dir_all(&output_dir).with_context(|| format!("Failed to create directory {:?}", output_dir))?;
|
||||
fs::write(&output_path, &svg_content).with_context(|| format!("Failed to write to {:?}", output_path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert a DOT graph string to SVG by shelling out to @viz-js/viz via Node.js
|
||||
fn dot_to_svg(dot: &str) -> Result<String> {
|
||||
let temp_dir = std::env::temp_dir().join("crate-hierarchy-viz");
|
||||
fs::create_dir_all(&temp_dir).with_context(|| "Failed to create temp directory")?;
|
||||
|
||||
// Install @viz-js/viz into the temp directory if not already present
|
||||
let viz_package = temp_dir.join("node_modules").join("@viz-js").join("viz");
|
||||
if !viz_package.exists() {
|
||||
let npm = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
|
||||
let status = Command::new(npm)
|
||||
.args(["install", "--prefix", &temp_dir.to_string_lossy(), "@viz-js/viz"])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.status()
|
||||
.with_context(|| "Failed to run `npm install`. Is Node.js installed?")?;
|
||||
if !status.success() {
|
||||
anyhow::bail!("Executing `npm install @viz-js/viz` failed");
|
||||
}
|
||||
}
|
||||
|
||||
// Write a small script that reads DOT from stdin and outputs SVG
|
||||
let script_path = temp_dir.join("convert.mjs");
|
||||
fs::write(
|
||||
&script_path,
|
||||
r#"
|
||||
import { instance } from "@viz-js/viz";
|
||||
let dot = "";
|
||||
for await (const chunk of process.stdin) dot += chunk;
|
||||
const viz = await instance();
|
||||
process.stdout.write(viz.renderString(dot, { format: "svg" }));
|
||||
"#
|
||||
.trim(),
|
||||
)?;
|
||||
|
||||
let mut child = Command::new("node")
|
||||
.arg(&script_path)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| "Failed to spawn `node`. Is Node.js installed?")?;
|
||||
|
||||
// Write DOT content to stdin then close the pipe
|
||||
child
|
||||
.stdin
|
||||
.take()
|
||||
.context("Failed to get stdin handle for node process")?
|
||||
.write_all(dot.as_bytes())
|
||||
.with_context(|| "Failed to write DOT content to stdin")?;
|
||||
|
||||
let output = child.wait_with_output().with_context(|| "Failed to wait for `node` process")?;
|
||||
|
||||
// Clean up the temp script (node_modules is intentionally kept as a cache)
|
||||
let _ = fs::remove_file(&script_path);
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("DOT to SVG conversion failed (exit code {:?}):\n{}", output.status.code(), stderr);
|
||||
}
|
||||
|
||||
String::from_utf8(output.stdout).with_context(|| "SVG output was not valid UTF-8")
|
||||
}
|
||||
|
||||
fn generate_dot(crates: &[CrateInfo]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("digraph CrateHierarchy {\n");
|
||||
|
|
|
|||
|
|
@ -3,28 +3,50 @@ use editor::utility_types::DebugMessageTree;
|
|||
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>")?;
|
||||
const FRONTEND_MESSAGE_STR: &str = "FrontendMessage";
|
||||
|
||||
if let Some(parent) = output_path.parent() {
|
||||
std::fs::create_dir_all(parent).unwrap();
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let output_dir = std::env::args_os().nth(1).map(PathBuf::from).ok_or("Usage: editor-message-tree <output-directory>")?;
|
||||
std::fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let tree = Message::message_tree();
|
||||
|
||||
// Write the .txt file (plain text tree outline, served as a static download)
|
||||
let static_dir = output_dir.join("../static/volunteer/guide/codebase-overview");
|
||||
std::fs::create_dir_all(&static_dir)?;
|
||||
let mut txt_file = std::fs::File::create(static_dir.join("hierarchical-message-system-tree.txt"))?;
|
||||
write_tree_txt(&tree, &mut txt_file)?;
|
||||
|
||||
// Write the .html file (structured HTML embedded in the website page)
|
||||
let mut html = String::new();
|
||||
write_tree_html(&tree, &mut html);
|
||||
std::fs::write(output_dir.join("hierarchical-message-system-tree.html"), &html)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let result = Message::message_tree();
|
||||
let mut file = std::fs::File::create(&output_path).unwrap();
|
||||
file.write_all(format!("{} `{}#L{}`\n", result.name(), result.path(), result.line_number()).as_bytes()).unwrap();
|
||||
if let Some(variants) = result.variants() {
|
||||
// =================
|
||||
// PLAIN TEXT OUTPUT
|
||||
// =================
|
||||
|
||||
fn write_tree_txt(tree: &DebugMessageTree, file: &mut std::fs::File) -> std::io::Result<()> {
|
||||
if tree.path().is_empty() {
|
||||
file.write_all(format!("{}\n", tree.name()).as_bytes())?;
|
||||
} else {
|
||||
file.write_all(format!("{} `{}#L{}`\n", tree.name(), tree.path(), tree.line_number()).as_bytes())?;
|
||||
}
|
||||
|
||||
if let Some(variants) = tree.variants() {
|
||||
for (i, variant) in variants.iter().enumerate() {
|
||||
let is_last = i == variants.len() - 1;
|
||||
print_tree_node(variant, "", is_last, &mut file);
|
||||
write_tree_txt_node(variant, "", is_last, file)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
|
||||
// Print the current node
|
||||
fn write_tree_txt_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) -> std::io::Result<()> {
|
||||
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() {
|
||||
("├── ", format!("{prefix}│ "))
|
||||
} else if is_last {
|
||||
|
|
@ -34,33 +56,28 @@ fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &
|
|||
};
|
||||
|
||||
if tree.path().is_empty() {
|
||||
file.write_all(format!("{}{}{}\n", prefix, branch, tree.name()).as_bytes()).unwrap();
|
||||
file.write_all(format!("{}{}{}\n", prefix, branch, tree.name()).as_bytes())?;
|
||||
} else {
|
||||
file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, tree.name(), tree.path(), tree.line_number()).as_bytes())
|
||||
.unwrap();
|
||||
file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, tree.name(), tree.path(), tree.line_number()).as_bytes())?;
|
||||
}
|
||||
|
||||
// Print children if any
|
||||
if let Some(variants) = tree.variants() {
|
||||
let len = variants.len();
|
||||
for (i, variant) in variants.iter().enumerate() {
|
||||
let is_last_child = i == len - 1;
|
||||
print_tree_node(variant, &child_prefix, is_last_child, file);
|
||||
write_tree_txt_node(variant, &child_prefix, is_last_child, file)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Print message field if any
|
||||
if let Some(fields) = tree.fields() {
|
||||
let len = fields.len();
|
||||
for (i, field) in fields.iter().enumerate() {
|
||||
let is_last_field = i == len - 1;
|
||||
let branch = if is_last_field { "└── " } else { "├── " };
|
||||
|
||||
file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes()).unwrap();
|
||||
file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Print handler field if any
|
||||
if let Some(data) = tree.message_handler_fields() {
|
||||
let len = data.fields().len();
|
||||
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() {
|
||||
|
|
@ -69,36 +86,199 @@ fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &
|
|||
("└── ", format!("{prefix} "))
|
||||
};
|
||||
|
||||
const FRONTEND_MESSAGE_STR: &str = "FrontendMessage";
|
||||
if data.name().is_empty() && tree.name() != FRONTEND_MESSAGE_STR {
|
||||
panic!("{}'s MessageHandler is missing #[message_handler_data]", tree.name());
|
||||
} else if tree.name() != FRONTEND_MESSAGE_STR {
|
||||
file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, data.name(), data.path(), data.line_number()).as_bytes())
|
||||
.unwrap();
|
||||
file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, branch, data.name(), data.path(), data.line_number()).as_bytes())?;
|
||||
|
||||
for (i, field) in data.fields().iter().enumerate() {
|
||||
let is_last_field = i == len - 1;
|
||||
let branch = if is_last_field { "└── " } else { "├── " };
|
||||
|
||||
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap();
|
||||
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print data field if any
|
||||
if let Some(data) = tree.message_handler_data_fields() {
|
||||
let len = data.fields().len();
|
||||
if data.path().is_empty() {
|
||||
file.write_all(format!("{}{}{}\n", prefix, "└── ", data.name()).as_bytes()).unwrap();
|
||||
file.write_all(format!("{}{}{}\n", prefix, "└── ", data.name()).as_bytes())?;
|
||||
} else {
|
||||
file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, "└── ", data.name(), data.path(), data.line_number()).as_bytes())
|
||||
.unwrap();
|
||||
file.write_all(format!("{}{}{} `{}#L{}`\n", prefix, "└── ", data.name(), data.path(), data.line_number()).as_bytes())?;
|
||||
}
|
||||
for (i, field) in data.fields().iter().enumerate() {
|
||||
let is_last_field = i == len - 1;
|
||||
let branch = if is_last_field { "└── " } else { "├── " };
|
||||
let field = &field.0;
|
||||
file.write_all(format!("{prefix} {branch}{field}\n").as_bytes()).unwrap();
|
||||
file.write_all(format!("{prefix} {branch}{field}\n").as_bytes())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ===========
|
||||
// HTML OUTPUT
|
||||
// ===========
|
||||
|
||||
const GITHUB_BASE: &str = "https://github.com/GraphiteEditor/Graphite/blob/master/";
|
||||
const NAMING_SUFFIXES: &[&str] = &["Message", "MessageHandler", "MessageContext"];
|
||||
|
||||
fn escape_html(s: &str) -> String {
|
||||
s.replace('&', "&").replace('<', "<").replace('>', ">")
|
||||
}
|
||||
|
||||
fn github_link(path: &str, line: usize) -> String {
|
||||
let path = path.replace('\\', "/");
|
||||
let filename = path.rsplit('/').next().unwrap_or(&path);
|
||||
format!(r#"<a href="{GITHUB_BASE}{path}#L{line}" target="_blank">{filename}:{line}</a>"#)
|
||||
}
|
||||
|
||||
fn naming_convention_warning(name: &str) -> &'static str {
|
||||
// Strip generic parameters for the check (e.g. `Foo<Bar>` -> `Foo`)
|
||||
let base_name = name.split('<').next().unwrap_or(name);
|
||||
if NAMING_SUFFIXES.iter().any(|suffix| base_name.ends_with(suffix)) {
|
||||
""
|
||||
} else {
|
||||
r#"<span class="warn">(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')</span>"#
|
||||
}
|
||||
}
|
||||
|
||||
fn write_tree_html(tree: &DebugMessageTree, out: &mut String) {
|
||||
// Root node
|
||||
let link = if !tree.path().is_empty() { github_link(tree.path(), tree.line_number()) } else { String::new() };
|
||||
let escaped_name = escape_html(tree.name());
|
||||
|
||||
out.push_str("<ul>\n");
|
||||
out.push_str(&format!(r#"<li><span class="tree-node"><span class="subsystem">{escaped_name}</span>{link}</span>"#));
|
||||
|
||||
if let Some(variants) = tree.variants() {
|
||||
out.push_str(r#"<div class="nested">"#);
|
||||
write_tree_html_children(variants, out);
|
||||
out.push_str("</div>");
|
||||
}
|
||||
|
||||
out.push_str("</li>\n</ul>\n");
|
||||
}
|
||||
|
||||
fn write_tree_html_children(variants: &[DebugMessageTree], out: &mut String) {
|
||||
out.push_str("<ul>\n");
|
||||
for variant in variants {
|
||||
write_tree_html_node(variant, out);
|
||||
}
|
||||
out.push_str("</ul>\n");
|
||||
}
|
||||
|
||||
fn write_tree_html_node(tree: &DebugMessageTree, out: &mut String) {
|
||||
let has_link = !tree.path().is_empty();
|
||||
let link = if has_link { github_link(tree.path(), tree.line_number()) } else { String::new() };
|
||||
let escaped_name = escape_html(tree.name());
|
||||
|
||||
enum HtmlChild<'a> {
|
||||
Subtree(&'a DebugMessageTree),
|
||||
Field(String),
|
||||
HandlerFields(String, String, usize, Vec<String>),
|
||||
DataFields(String, String, usize, Vec<String>),
|
||||
}
|
||||
|
||||
// Collect all child entries for this node
|
||||
let mut children: Vec<HtmlChild> = Vec::new();
|
||||
|
||||
if let Some(variants) = tree.variants() {
|
||||
for variant in variants {
|
||||
children.push(HtmlChild::Subtree(variant));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(fields) = tree.fields() {
|
||||
for field in fields {
|
||||
children.push(HtmlChild::Field(field.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(data) = tree.message_handler_fields()
|
||||
&& (!data.name().is_empty() || tree.name() == FRONTEND_MESSAGE_STR)
|
||||
&& tree.name() != FRONTEND_MESSAGE_STR
|
||||
{
|
||||
children.push(HtmlChild::HandlerFields(
|
||||
data.name().to_string(),
|
||||
data.path().to_string(),
|
||||
data.line_number(),
|
||||
data.fields().iter().map(|f| f.0.clone()).collect(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(data) = tree.message_handler_data_fields() {
|
||||
children.push(HtmlChild::DataFields(
|
||||
data.name().to_string(),
|
||||
data.path().to_string(),
|
||||
data.line_number(),
|
||||
data.fields().iter().map(|f| f.0.clone()).collect(),
|
||||
));
|
||||
}
|
||||
|
||||
let has_children = !children.is_empty();
|
||||
let has_deeper_children = children.iter().any(|child| matches!(child, HtmlChild::Subtree(t) if t.variants().is_some() || t.fields().is_some()));
|
||||
|
||||
// Determine role
|
||||
let role = if has_link {
|
||||
"subsystem"
|
||||
} else if has_deeper_children {
|
||||
"submessage"
|
||||
} else {
|
||||
"message"
|
||||
};
|
||||
|
||||
// Naming convention warning (only for linked/subsystem nodes)
|
||||
let warning = if has_link { naming_convention_warning(tree.name()) } else { "" };
|
||||
|
||||
if has_children {
|
||||
out.push_str(&format!(r#"<li><span class="tree-node"><span class="{role}">{escaped_name}</span>{link}{warning}</span>"#));
|
||||
out.push_str(r#"<div class="nested"><ul>"#);
|
||||
out.push('\n');
|
||||
|
||||
for child in &children {
|
||||
match child {
|
||||
HtmlChild::Subtree(subtree) => write_tree_html_node(subtree, out),
|
||||
HtmlChild::Field(field) => write_field_html(field, out),
|
||||
HtmlChild::HandlerFields(name, path, line, fields) => write_handler_or_data_html(name, path, *line, fields, out),
|
||||
HtmlChild::DataFields(name, path, line, fields) => write_handler_or_data_html(name, path, *line, fields, out),
|
||||
}
|
||||
}
|
||||
|
||||
out.push_str("</ul>\n</div></li>\n");
|
||||
} else {
|
||||
out.push_str(&format!(r#"<li><span class="tree-leaf {role}">{escaped_name}</span>{link}{warning}</li>"#));
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn write_field_html(field: &str, out: &mut String) {
|
||||
if let Some((name, ty)) = field.split_once(':') {
|
||||
let name = escape_html(name.trim());
|
||||
let ty = escape_html(ty.trim());
|
||||
out.push_str(&format!(r#"<li><span class="tree-leaf field">{name}</span>: <span>{ty}</span></li>"#));
|
||||
} else {
|
||||
let escaped = escape_html(field);
|
||||
out.push_str(&format!(r#"<li><span class="tree-leaf message">{escaped}</span></li>"#));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
fn write_handler_or_data_html(name: &str, path: &str, line: usize, fields: &[String], out: &mut String) {
|
||||
let escaped_name = escape_html(name);
|
||||
let link = if !path.is_empty() { github_link(path, line) } else { String::new() };
|
||||
let warning = if !path.is_empty() { naming_convention_warning(name) } else { "" };
|
||||
|
||||
if fields.is_empty() {
|
||||
out.push_str(&format!(r#"<li><span class="tree-leaf subsystem">{escaped_name}</span>{link}{warning}</li>"#));
|
||||
} else {
|
||||
out.push_str(&format!(r#"<li><span class="tree-node"><span class="subsystem">{escaped_name}</span>{link}{warning}</span>"#));
|
||||
out.push_str(r#"<div class="nested"><ul>"#);
|
||||
out.push('\n');
|
||||
for field in fields {
|
||||
write_field_html(field, out);
|
||||
}
|
||||
out.push_str("</ul>\n</div></li>\n");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
/* 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}`);
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
// TODO: Port this script to Rust as part of `tools/editor-message-tree/src/main.rs`
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
type Entry = { level: number; text: string; link: string | undefined };
|
||||
|
||||
/// Parses a single line of the input text.
|
||||
function parseLine(line: string) {
|
||||
const linkRegex = /`([^`]+)`$/;
|
||||
const linkMatch = line.match(linkRegex);
|
||||
let link = undefined;
|
||||
|
||||
if (linkMatch) {
|
||||
const filePath = linkMatch[1].replace(/\\/g, "/");
|
||||
link = `https://github.com/GraphiteEditor/Graphite/blob/master/${filePath}`;
|
||||
}
|
||||
|
||||
const textContent = line
|
||||
.replace(/^[\s│├└─]*/, "")
|
||||
.replace(linkRegex, "")
|
||||
.trim();
|
||||
const indentation = line.indexOf(textContent);
|
||||
// Each level of indentation is 4 characters.
|
||||
const level = Math.floor(indentation / 4);
|
||||
|
||||
return { level, text: textContent, link };
|
||||
}
|
||||
|
||||
/// Recursively builds the HTML list from the parsed nodes.
|
||||
function buildHtmlList(nodes: Entry[], currentIndex: number, currentLevel: number) {
|
||||
if (currentIndex >= nodes.length) {
|
||||
return { html: "", nextIndex: currentIndex };
|
||||
}
|
||||
|
||||
let html = "<ul>\n";
|
||||
let i = currentIndex;
|
||||
|
||||
while (i < nodes.length && nodes[i].level >= currentLevel) {
|
||||
const node = nodes[i];
|
||||
|
||||
if (node.level > currentLevel) {
|
||||
// This case handles malformed input, skip to next valid line
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasDirectChildren = i + 1 < nodes.length && nodes[i + 1].level > node.level;
|
||||
const hasDeeperChildren = hasDirectChildren && i + 2 < nodes.length && nodes[i + 2].level > nodes[i + 1].level;
|
||||
|
||||
const linkHtml = node.link ? `<a href="${node.link}" target="_blank">${path.basename(node.link.split("#L").join(":"))}</a>` : "";
|
||||
const fieldPieces = node.text.match(/([^:]*):(.*)/);
|
||||
let escapedText;
|
||||
if (fieldPieces && fieldPieces.length === 3) {
|
||||
escapedText = [escapeHtml(fieldPieces[1].trim()), escapeHtml(fieldPieces[2].trim())];
|
||||
} else {
|
||||
escapedText = [escapeHtml(node.text)];
|
||||
}
|
||||
|
||||
let role = "message";
|
||||
if (node.link) role = "subsystem";
|
||||
else if (hasDeeperChildren) role = "submessage";
|
||||
else if (escapedText.length === 2) role = "field";
|
||||
|
||||
const partOfMessageFromNamingConvention = ["Message", "MessageHandler", "MessageContext"].some((suffix) => node.text.replace(/(.*)<.*>/g, "$1").endsWith(suffix));
|
||||
const partOfMessageViolatesNamingConvention = node.link && !partOfMessageFromNamingConvention;
|
||||
const violatesNamingConvention = partOfMessageViolatesNamingConvention
|
||||
? "<span class=\"warn\">(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')</span>"
|
||||
: "";
|
||||
|
||||
if (hasDirectChildren) {
|
||||
html += `<li><span class="tree-node"><span class="${role}">${escapedText}</span>${linkHtml}${violatesNamingConvention}</span>`;
|
||||
const childResult = buildHtmlList(nodes, i + 1, node.level + 1);
|
||||
html += `<div class="nested">${childResult.html}</div></li>\n`;
|
||||
i = childResult.nextIndex;
|
||||
} else if (role === "field") {
|
||||
html += `<li><span class="tree-leaf field">${escapedText[0]}</span>: <span>${escapedText[1]}</span>${linkHtml}</li>\n`;
|
||||
i++;
|
||||
} else {
|
||||
html += `<li><span class="tree-leaf ${role}">${escapedText[0]}</span>${linkHtml}${violatesNamingConvention}</li>\n`;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
html += "</ul>\n";
|
||||
return { html, nextIndex: i };
|
||||
}
|
||||
|
||||
function escapeHtml(text: string) {
|
||||
return text.replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
const inputFile = process.argv[2];
|
||||
const outputFile = process.argv[3];
|
||||
|
||||
if (!inputFile || !outputFile) {
|
||||
console.error("Error: Please provide the input text and output HTML file paths as arguments.");
|
||||
console.log("Usage: node generate-editor-structure.ts <input txt> <output html>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
console.error(`Error: File not found at "${inputFile}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(inputFile, "utf-8");
|
||||
const lines = fileContent.split(/\r?\n/).filter((line) => line.trim() !== "" && !line.startsWith("// filepath:"));
|
||||
const parsedNodes = lines.map(parseLine);
|
||||
|
||||
const { html } = buildHtmlList(parsedNodes, 0, 0);
|
||||
|
||||
fs.writeFileSync(outputFile, html, "utf-8");
|
||||
|
||||
console.log(`Successfully generated HTML outline at: ${outputFile}`);
|
||||
} catch (error) {
|
||||
console.error("An error occurred during processing:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ The dispatcher lives at the root of the editor hierarchy and acts as the owner o
|
|||
cargo run explore editor
|
||||
```
|
||||
|
||||
Click to explore the outline of the editor subsystem hierarchy which forms the structure of the editor's subsystems, state, and interactions.
|
||||
Click to explore the outline of the editor subsystem hierarchy which forms the structure of the editor's subsystems, state, and interactions. Also available as a searchable <a href="/volunteer/guide/codebase-overview/hierarchical-message-system-tree.txt">plain text file</a>.
|
||||
|
||||
<div class="structure-outline">
|
||||
<!-- replacements::hierarchical_message_system_tree() -->
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/node": "^25.0.9",
|
||||
"@viz-js/viz": "^3.25.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
|
|
@ -1233,13 +1232,6 @@
|
|||
"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": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"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-crate-hierarchy": "node .build-scripts/generate-crate-hierarchy.ts generated/crate_hierarchy.dot generated/crate_hierarchy.svg",
|
||||
"check": "tsc --noEmit && eslint",
|
||||
"fix": "eslint --fix"
|
||||
},
|
||||
|
|
@ -22,7 +20,6 @@
|
|||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/node": "^25.0.9",
|
||||
"@viz-js/viz": "^3.25.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
|
|
|
|||
|
|
@ -40,25 +40,21 @@
|
|||
{% endmacro text_balancer %}
|
||||
|
||||
{% macro hierarchical_message_system_tree() %}
|
||||
{%- set content = load_data(path = "../generated/hierarchical_message_system_tree.html", format = "plain", required = false) -%}
|
||||
{%- set content = load_data(path = "../generated/hierarchical-message-system-tree.html", 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 editor-message-tree -- website/generated/hierarchical_message_system_tree.txt
|
||||
cd website
|
||||
npm run generate-editor-structure</pre>" -%}
|
||||
cargo run -p editor-message-tree -- website/generated</pre>" -%}
|
||||
{{ content | default(value = fallback) | safe }}
|
||||
{% endmacro hierarchical_message_system_tree %}
|
||||
|
||||
{% macro crate_hierarchy() %}
|
||||
{%- set content = load_data(path = "../generated/crate_hierarchy.svg", format = "plain", required = false) -%}
|
||||
{%- 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>" -%}
|
||||
cargo run -p crate-hierarchy-viz -- website/generated</pre>" -%}
|
||||
{{ content | default(value = fallback) | safe }}
|
||||
{% endmacro crate_hierarchy %}
|
||||
|
|
|
|||
Loading…
Reference in New Issue