diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 4b9a6ed4..5e5c27ff 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -44,6 +44,34 @@ 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@v3 + with: + path: artifacts + 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' + run: | + echo "πŸ¦€ Initial system version of Rust:" + rustc --version + rustup update stable + echo "πŸ¦€ Latest updated version of Rust:" + rustc --version + cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree + mkdir artifacts + mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt + + - name: 🚚 Move `artifacts` contents to `website/other/editor-structure` + run: | + mv artifacts/* website/other/editor-structure + + - name: πŸ”§ Build auto-generated code docs artifacts into HTML + run: | + cd website/other/editor-structure + node generate.js hierarchical_message_system_tree.txt replacement.html + - name: 🌐 Build Graphite website with Zola env: MODE: prod @@ -84,29 +112,6 @@ jobs: mkdir -p website/public mv website/other/dist/* website/public - - name: πŸ’Ώ Obtain cache of auto-generated code docs artifacts - id: cache-website-code-docs - uses: actions/cache/restore@v3 - with: - path: artifacts - 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' - run: | - echo "πŸ¦€ Initial system version of Rust:" - rustc --version - rustup update stable - echo "πŸ¦€ Latest updated version of Rust:" - rustc --version - cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree - mkdir artifacts - mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt - - - name: 🚚 Move `artifacts` contents to `website/public` - run: | - mv artifacts/* website/public - - name: πŸ“€ Publish to Cloudflare Pages id: cloudflare uses: cloudflare/pages-action@1 diff --git a/proc-macros/src/helpers.rs b/proc-macros/src/helpers.rs index 230f792c..4beb44f4 100644 --- a/proc-macros/src/helpers.rs +++ b/proc-macros/src/helpers.rs @@ -72,6 +72,20 @@ pub fn clean_rust_type_syntax(input: String) -> String { chars.next(); } } + '-' => { + if let Some('>') = chars.peek() { + while let Some(' ') = result.chars().rev().next() { + result.pop(); + } + result.push_str(" -> "); + chars.next(); + while let Some(' ') = chars.peek() { + chars.next(); + } + } else { + result.push(c); + } + } ':' => { if let Some(':') = chars.peek() { while let Some(' ') = result.chars().rev().next() { diff --git a/proc-macros/src/message_handler_data_attr.rs b/proc-macros/src/message_handler_data_attr.rs index ea3e634c..6c1c88b8 100644 --- a/proc-macros/src/message_handler_data_attr.rs +++ b/proc-macros/src/message_handler_data_attr.rs @@ -33,9 +33,6 @@ pub fn message_handler_data_attr_impl(attr: TokenStream, input_item: TokenStream let message_type = &args.args[0]; let data_type = &args.args[1]; - // Check if the attribute is "CustomData" - let is_custom_data = attr.to_string().contains("CustomData"); - let impl_item = match data_type { syn::GenericArgument::Type(t) => { match t { @@ -43,32 +40,17 @@ pub fn message_handler_data_attr_impl(attr: TokenStream, input_item: TokenStream // Get just the base identifier (ToolMessageData) without generics let type_name = &type_path.path.segments.first().unwrap().ident; - if is_custom_data { - quote! { - #input_item - impl #message_type { - pub fn message_handler_data_str() -> MessageData { - custom_data() - } - pub fn message_handler_str() -> MessageData { - MessageData::new(format!("{}",stringify!(#input_type)), #input_type::field_types(), #input_type::path()) + quote! { + #input_item + impl #message_type { + pub fn message_handler_data_str() -> MessageData + { + MessageData::new(format!("{}", stringify!(#type_name)), #type_name::field_types(), #type_name::path()) - } } - } - } else { - quote! { - #input_item - impl #message_type { - pub fn message_handler_data_str() -> MessageData - { - MessageData::new(format!("{}",stringify!(#type_name)), #type_name::field_types(), #type_name::path()) + pub fn message_handler_str() -> MessageData { + MessageData::new(format!("{}", stringify!(#input_type)), #input_type::field_types(), #input_type::path()) - } - pub fn message_handler_str() -> MessageData { - MessageData::new(format!("{}",stringify!(#input_type)), #input_type::field_types(), #input_type::path()) - - } } } } @@ -77,7 +59,7 @@ pub fn message_handler_data_attr_impl(attr: TokenStream, input_item: TokenStream #input_item impl #message_type { pub fn message_handler_str() -> MessageData { - MessageData::new(format!("{}",stringify!(#input_type)), #input_type::field_types(), #input_type::path()) + MessageData::new(format!("{}", stringify!(#input_type)), #input_type::field_types(), #input_type::path()) } } }, @@ -92,11 +74,11 @@ pub fn message_handler_data_attr_impl(attr: TokenStream, input_item: TokenStream #input_item impl #message_type { pub fn message_handler_data_str() -> MessageData { - MessageData::new(format!("{}", #tr),#type_ident::field_types(), #type_ident::path()) + MessageData::new(format!("{}", #tr), #type_ident::field_types(), #type_ident::path()) } pub fn message_handler_str() -> MessageData { - MessageData::new(format!("{}",stringify!(#input_type)), #input_type::field_types(), #input_type::path()) + MessageData::new(format!("{}", stringify!(#input_type)), #input_type::field_types(), #input_type::path()) } } diff --git a/website/.gitignore b/website/.gitignore index 0fc0af5d..c9dbb633 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -3,3 +3,4 @@ public/ static/fonts/ static/syntax-highlighting.css static/text-balancer.js +other/editor-structure/replacement.html diff --git a/website/content/volunteer/guide/_unpublished/product-design/_index.md b/website/content/volunteer/guide/_unpublished/product-design/_index.md deleted file mode 100644 index 48efda62..00000000 --- a/website/content/volunteer/guide/_unpublished/product-design/_index.md +++ /dev/null @@ -1,18 +0,0 @@ -+++ -title = "Product design" -template = "book.html" -page_template = "book.html" - -[extra] -order = 5 # Chapter number -+++ - -**NOTE: Developers probably don't need to read this chapter.** - -A vital part in the success of open source software development is having a clear vision and direction for the product design. Failure to pull that off is a leading factor in the stagnation and mediocrity of many software projects. - -The Graphite project's founder and its principal product designer, Keavon, is in charge of maintaining and evolving the product design to ensure cohesion within the development process. This ranges from building the user interface design system and content layout as a mockup before it's replicated in HTML/CSS, to determining user workflows, to translating high-level goals into the constraints and requirements of specific features for the development team. - -The hardest part is translating an always-evolving network of ideas and thoughts comprehensively into written form in order to communicate the big (and little) ideas to collaborators. As the product develops, the design grows alongside the code and real usage of the developed features helps bring clarity to what's planned next. Since the design process never stops evolving, any attempt to write it down is inevitably doomed to become out of date. And the multitudes of mental state can't possibly all be written down. - -Despite the challenges with documenting design, this chapter aims to provide a living catalog of ideas surrounding some topics in the product design process. It won't be organized, nor complete, nor will it be always current. The best way to seek clarity about a topic is asking Keavon about it on Discord. **This chapter is optional reading** and is likely of minimal relevance to most code contributors. diff --git a/website/content/volunteer/guide/_unpublished/product-design/glossary-of-terminology.md b/website/content/volunteer/guide/_unpublished/product-design/glossary-of-terminology.md deleted file mode 100644 index 0923a654..00000000 --- a/website/content/volunteer/guide/_unpublished/product-design/glossary-of-terminology.md +++ /dev/null @@ -1,68 +0,0 @@ -+++ -title = "Glossary of terminology" - -[extra] -order = 3 # Page number after chapter intro -+++ - -**NOTE: This is old. Some parts may not match current usage.** - -### Document -A design source file created and edited in the Graphite editor. Saved to disk as a Graphite Design Document in a _GDD file_. Documents can be included as _layers_ inside other documents, and in doing so they take the form of _groups_. The _layer graph_ contents of a _group_ actually belong to the _embedded_ document's _subgraph_. Because a document is a _group_ which is a _layer_ in the _layer graph_, documents have _properties_ such as the _frames_ in the _canvas_. Documents are composed of a layer graph, a defined set of properties of set _data types_ that are _imported_ and _exported_, and the _properties_ of the _root layer_. -### Asset -A portable mechanism for distributing a "compiled" Graphite _document_ in a format that is immediately ready for rendering. Saved to disk as a Graphite Digital Asset in a _GDA file_. Assets are created by "flattening" a _document's_ complex, nested _layer graph_ structure into a single, simple directed acyclic graph (DAG). The Graphite editor internally maintains an asset version of any open _document_ in order to draw the _canvas_ live in the _viewport_. An asset also includes certain exposed _properties_ of specified _data types_ that are _imported_ and _exported_, as defined by the asset's author in the source _document's_ _layer graph_. They can be shared and _embedded_ in another _layer graph_ as a black box (meaning it can't be expanded to reveal or edit its interior graph), as compared to _embedded_ _documents_ from _GDD files_ which are white boxes (they can be expanded to reveal their _subgraph_ which can be edited). Assets are helpful for defining custom _nodes_ that perform some useful functionality. Tangible examples include custom procedural effects, shape generators, and image filters. Many of the Graphite editor's own built-in _nodes_ are actually assets rather than being implemented directly in code. The _Asset Manager_ panel helps maintain these assets from various sources. The _Asset Store_ can be used to share and sell assets for easy inclusion in other projects. -### GDD file -Graphite Design Document. A binary serialization of a _document_ source file. The format includes a chain of _operations_ that describe changes to the _layer graph_ and the _properties_ of _layers_ throughout the history of the document since its creation. It also stores certain metadata and the raw data of _embedded_ files. Because GDD files are editable (unlike _GDA files_), the _layers_ of GDD files imported into another _document_ may be expanded in its _layer graph_ to reveal and modify their contents using a copy-on-write scheme stored to the _asset's_ _layer_. -### GDA file -Graphite Digital Asset. A binary serialization of an _asset_ file. Because GDA files are read-only and can't be edited (unlike _GDD files_), the _layers_ created from _assets_ do not offer an ability to be expanded in the _layer graph_ of a _document_ that _embeds_ them. GDA files are useful for sharing _assets_ when their authors do not wish to provide the source _documents_ to author them. _DGA files_ are also the input format included in games that utilize the _Graphite Renderer Core Library_ to render graphical content at runtime, as well as similar applications like headless renderers on web servers and image processing pipelines. -### Window -### Main window -### Popout window -### Title bar -### Status bar -### Workspace -The part of the Graphite editor's UI that houses the _panels_ in a _window_. The workspace occupies the large space below the _title bar_ and above the _status bar_ of the _main window_. It occupies the entirety of _popout windows_ (window buttons are added in the _tab bar_). -### Workspace layout -The specific configuration of panels in the _main window_ and any _popout windows_. Workspace layout presets are provided by the Graphite editor and users may customize and save their own. -### Tab bar -The bar at the top of a _panel group_ which includes a clickable tab for each panel that is docked there. Each tab bar has at least one tab and one active tab. -### Active tab -The one tab in a _tab bar_ that is currently active. The user can click any inactive tab to make it become the active tab. The active tab shows the _panel content_ beneath it unless it is a _folded panel_. -### Folded panel -A shrunken _panel_ showing only the _tab bar_. A _panel_ consists of the _tab bar_ and _panel content_ except when the latter is folded away. The user may click the _active tab_ to fold and restore a panel, however a panel cannot be folded if there are no other unfolded panels in its column. -### Panel -### Panel content -### Control bar -The bar that spans horizontally across the top of a _panel_ (located under the _tab bar_) which displays controls related to the _panel_. -### Viewport -The area that takes up the main space in a _panel_ (located beneath the _control bar_) which displays the primary content of the _panel_. -### Shelf -The bar that spans vertically along the left side of some _panels_ (located left of the _viewport_) which displays a catalog of available items, such as document editing _tools_ or common _nodes_. -### Tool -An instrument for interactively editing _documents_ through a collection of related behavior. Each tool puts the editor into a mode that provides the ability to perform certain _operations_ on the document interactively. Each _operation_ is run based on the current context of mouse and modifier buttons, key presses, tool options, selected layers, editor state, and document state. The _operations_ that get run are appended to the document history and update the underlying _layer graph_ in real time. -### Canvas -The infinite coordinate system that shows the visual output of an open _document_ at the current zoom level and pan position. It is drawn in the document panel's _viewport_ within the area inside the scroll bars on the bottom/right edges and the _rulers_ on the top/left edges. The canvas can be panned and zoomed in order to display all or part of the artwork in any _frames_. A canvas has a coordinate system spanning infinitely in all directions with an origin always located at the top left of the primary _artboard_. The purpose of an infinite canvas is to offer a convenient editing experience when there is no logical edge to the artwork, for example a loosely-arranged board of logo design concepts, a mood board, or whiteboard-style notes. -### Artboard -An area inside a _canvas_ that provides rectangular bounds to the artwork contained within, as well as default bounds for an exported image. The _Artboard tool_ adjusts the bounds and placement of frames in the _document_ and each artboard is stored in a "artboard list" property of the _root layer_. When there is at least one artboard, the infinite _canvas_ area outside any artboard displays a configurable background color. Artwork can be placed outside of a artboard but it will appear mostly transparent. The purpose of using one artboard is to provide convenient cropping to the edges of the artwork, such as a single digital painting or photograph. The purpose of using multiple frames is to work on related artwork with separate bounds, such as the layout for a book. -### Layer graph -A (directed acyclic) graph structure composed of _layers_ with _connections_ between their input and output _ports_. This is commonly referred to as a "node graph" in other software, but Graphite's layer graph is more suited towards layer-based compositing compared to traditional compositor node graphs. -### Node -A definition of a _layer_. A node is a graph "operation" or "function" that receives input and generates deterministic output. -### Layer -Any instance of a _node_ that lives in the _layer graph_. Layers (usually) take input data, then they transform it or synthesize new data, then they provide it as output. Layers have _properties_ as well as exposed input and output _ports_ for sending and receiving data. -### Root layer -### Group -### Raster -### Vector -### Mask -### Data type -### Subgraph -### Port -### Connection -### Core Libraries -### Graphite Editor (Frontend) -### Graphite Editor (Backend) -### Graphene (Node Graph Engine) -### Trace -### Path -### Shape diff --git a/website/content/volunteer/guide/_unpublished/product-design/product-outline.md b/website/content/volunteer/guide/_unpublished/product-design/product-outline.md deleted file mode 100644 index 7e1e8365..00000000 --- a/website/content/volunteer/guide/_unpublished/product-design/product-outline.md +++ /dev/null @@ -1,108 +0,0 @@ -+++ -title = "Product outline" - -[extra] -order = 1 # Page number after chapter intro -+++ - -**NOTE: This is old. Some parts may not match current usage.** - -- Interface - - Title bar - - Menu bar - - Document title - - Window buttons - - Workspace - - Panel interface (tab, pin, control bar, left menu) - - Arrangement and docking - - Status bar - - Multiple windows -- Panels - - Document - - Canvas and frames - - Rulers - - Tool menu - - Control bar - - Properties - - Blending - - Origin - - Transform - - Node-specific properties - - Layers - - Interface - - Compositing flow - - Groups - - Masks - - Isolation - - Graph - - Interface - - Layer/node equivalence - - Compositing flow - - Groups - - Masks - - Isolation - - Connection Matrix - - Spreadsheet - - Node Catalog - - Asset Manager - - Embedded assets - - Linked assets - - Local assets - - Remote assets - - Store assets - - Out-of-the-box assets - - Extension-provided assets - - Palettes - - Each color palette is an asset - - Minimap - - Histogram - - Timeline -- Documents and assets - - Node groups, layer groups, and document tabs are equivalent - - Groups/documents are assets - - Assets are embedded or linked - - The document canvas is a group output - - Art boards and dimensions - - Reusable assets -- Tools - - Overview - - Tools add and update assets - - General tool group - - Vector tool group - - Raster tool group -- Masking - - Mask mode -- Vector editing - - Data types - - Data flow - - Rasterization -- Raster editing - - Adaptive resolution sampling - - Compositing and data flow - - Caching - - Predictive caching - - Progressive enhancement - - Generator nodes - - Rasterizer nodes - - Adjustment nodes - - Filter nodes - - Sample transformer nodes - - Historical sampling -- Text editing -- Data types -- Node types -- Node library -- File format interoperability - - Import/export: JPG, PNG, APNG, GIF, TIFF, TGA, BMP, JPEG 2000, WebP, HEIF, ICO - - Import/export: EXR - - Import: digital camera raw formats - - Import/export: SVG - - Import/export: EPS - - Import/export: PDF - - Import: PSD/PSB, AI, INDD? - - Export: G-code formats for laser and vinyl cutters? -- Extensions -- Color management -- Units, measurement, and scale -- Headless and integrations -- Animation diff --git a/website/content/volunteer/guide/_unpublished/product-design/uses-and-workflows.md b/website/content/volunteer/guide/_unpublished/product-design/uses-and-workflows.md deleted file mode 100644 index c05356a9..00000000 --- a/website/content/volunteer/guide/_unpublished/product-design/uses-and-workflows.md +++ /dev/null @@ -1,115 +0,0 @@ -+++ -title = "Uses and workflows" - -[extra] -order = 2 # Page number after chapter intro -+++ - -**NOTE: This is old. Some parts may not match current usage.** - -This list describes some long-term aspirational goals and ideas. It represents an incomplete brainstorm idea dump, not a roadmap. - -## Use cases - -General goals for product capabilities are categorized by discipline below. - -### Photography -- RAW photo editing/processing -- Batch processing pipeline - -### Motion graphics -- Title sequences/kinetic typography/etc. - -### Live broadcast/streaming -- Live video compositing/overlays -- Interactive exhibits (rendering live content for museum/art/festival exhibits) - -### Web -- SVG design -- Animated or interactive SVG design - -### Automation -- Batch multimedia editing/conversion - -### Graphic design -- Print design -- Web/digital-focused graphics (marketing, branding, infographics, ads, etc.) -- Templates filled with file/spreadsheet data (e.g. prototyping/iterating components of a board/card game) - -### Illustration -- Digital painting -- Logo and icon design - -### Desktop publishing -- Templates filled with Markdown/HTML content with export to PDF - -### Video compositing - -### Data Visualization -- Data-powered graphs/charts/etc. -- Automated rendering with live/often-updated data - -### 3D/Gamedev -- PBR procedural material authorship -- 3D model UV map texturing - -### AI-assisted tools - -### HDR processing - -### 360Β° and panoramic stitching and spherical editing - -## User stories - -Example user workflows are categorized by discipline below. - -### Photography -- Using a face detection node to sort photos into the correct folders upon export -- Using a face detection node to place a watermark near every face to prevent customers from cropping out watermarks placed in less important areas of some photos -- Shadow removal from part of an image by masking the location of a shadow and using the texture details of the darker area and the lighting and color context of the illuminated area -- Lightening or contextually infilling all the areas where a camera sensor has dust specks -- Isolating clipping highlights (that have expanded into neighboring pixels) in one or more color channels from point light sources like city lights and rendering smoother bright gradient point lights in place of them -- Advanced, intuitive blending with a node that lets you create a "custom blend mode" by specifying something like "Anything that's white: display as is. Anything else further from white by luminosity: fade out the saturation and opacity." - -### Image editing -- Removing translucent watermarks that were applied in the same location to a batch of photos by finding their shared similarities and differences and using that as a subtraction diff - -### Game development -- Design a GUI for the game and use Graphene to render the in-game GUI textures at runtime at the desired resolution without scaling problems, or even render it live as data updates its state -- Authoring procedural noise-based textures and PBR materials - -### Data visualization -- Creating a chart from a CSV -- Rendering an always-up-to-date chart powered by real-time updates from a database -- Data-driven infographics like an org chart that can be updated with text instead of manual design work -- Rendering a timelapse video of every operation done in the history of a document - -### Digital painting -- Creating a digital acrylic or oil painting using various brushes -- Preventing mixing/smearing of previous wet paint layers by drying it with a hair dryer tool -- Smearing wet paint colors together on a simulated paint palette and then sampling paint colors from that palette to paint with - -### Graphic design -- Prototyping cards for board games fed with data in a spreadsheet which generates the cards from a template -- Creating an image that has been shredded but pieced back together, where the image can be updated then return to the shredded one without having to redo the editing steps to shred it - -### Broadcast, interactive exhibits, and digital signage -- Rendering overlays for live streams or television broadcasts based on live input data, for example somebody donates and leaves a comment on a live stream and this web hook could trigger an animated display containing the user and their comment, or live telemetry for a rocket launch streams in and gets rendered as graphical overlays for a webcast. -- Rendering a custom live clockface with hour/minute/second hands based on an input of the current time, then showing them fullscreen on a display -- Request the weather from an API and render live visualizations which gets displayed on a monitor in your house or a museum (export to a Windows screen saver?) -- Data from sensors can render interactive 2D graphics at a museum art installation -- A storefront can have a monitor set up showing daily or hourly sales items based on web hooks or polling from the company’s website -- Polling an API for content like Twitter or an RSS feed and displaying the tweet or headline when it arrives on screen, styled as desired - -### Print and publishing -- Formatting Markdown documents into PDF print layouts -- Laying out book covers for proper PDF export to a printer -- Typesetting and formatting all the interior pages of a book with manual control where needed - -### Automation -- Laser cutter artwork processing for automating custom Etsy orders -- Running on a server to let users upload images for a custom T-shirt printing website, and it renders their graphic on the model’s shirt (or other custom printing online stores) -- Generating a PDF invoice based on data in a pipeline on a server - -### Computer vision and industrial control -- Factory line is examining its fruit for defects. In order to verify the quality, they need to enhance the contrast, automatically orient the image and correct the lighting. They then pass the results into a machine learning algorithm to classify, and sometimes need to snoop on the stream of data to manually do quality control (ImageMagick or custom Python scripts are often used for this right now) diff --git a/website/content/volunteer/guide/_unpublished/project-setup/knowing-your-tooling.md b/website/content/volunteer/guide/_unpublished/project-setup/knowing-your-tooling.md deleted file mode 100644 index 97869a2c..00000000 --- a/website/content/volunteer/guide/_unpublished/project-setup/knowing-your-tooling.md +++ /dev/null @@ -1,20 +0,0 @@ -+++ -title = "Knowing your tooling" - -[extra] -order = 2 # Page number after chapter intro -+++ - -## First time builds - -Slower. - -## Troubleshooting - -Delete the `target`, `pkg`, `node_modules`, and `dist` directories. - -## Slow builds - -If you're seeing the terminal spend several seconds installing wasm-opt every recompilation, reinstall the exact version of `wasm-bindgen-cli` that matches the `wasm-bindgen` dependency in [`Cargo.toml`](https://github.com/GraphiteEditor/Graphite/blob/master/Cargo.toml). - -## Rust Analyzer speed tips diff --git a/website/content/volunteer/guide/codebase-overview/_index.md b/website/content/volunteer/guide/codebase-overview/_index.md index c28ac782..c3b8cd6d 100644 --- a/website/content/volunteer/guide/codebase-overview/_index.md +++ b/website/content/volunteer/guide/codebase-overview/_index.md @@ -32,190 +32,28 @@ Graphite is built from several main software components. New developers may choo ### Frontend -*Location: `/frontend/src`* +*Location: [`/frontend/src`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/src)* -The frontend is the interface for Graphite which users see and interact with. It is built using web technologies with TypeScript and Svelte (HTML and SCSS). The frontend's philosophy is to be as lightweight and minimal as possible. It acts as the entry point for user input and then quickly hands off its work to the WebAssembly editor backend via its Wasm wrapper API. That API is written in Rust but has TypeScript bindings generated by the [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) tooling that is part of the Vite-based build chain. The frontend is built of many components that recursively form the window, panels, and widgets that make up the user interface. +The frontend is the GUI for Graphite which users see and interact with. It is built using web technologies with TypeScript and Svelte (HTML and SCSS). The frontend's philosophy is to be as lightweight and minimal as possible. It acts as the entry point for user input and then quickly hands off its work to the WebAssembly editor backend via its [Wasm wrapper API](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/wasm). That API is written in Rust but has TypeScript bindings generated by the [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) tooling that is part of the Vite-based build toolchain. The frontend is built of many [components](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/src/components) that recursively form the window, panels, and widgets that make up the user interface. ### Editor -*Location: `/editor`* +*Location: [`/editor`](https://github.com/GraphiteEditor/Graphite/tree/master/editor)* -The editor is the core of the Graphite application, and it's where all the business logic occurs for the tooling and user interaction. It is written in Rust and compiled to WebAssembly. At its heart is the message system described below. It is responsible for communicating with Graphene as well as handling the actual logic, state, tooling, and responsibilities of the interactive application. +[The editor](./editor-structure) is the core of the Graphite application, and it's where all the business logic occurs for the tooling and user interaction. It is written in Rust and compiled to WebAssembly. At its heart is the message system. It is responsible for communicating with Graphene as well as handling the actual logic, state, tooling, and responsibilities of the interactive application. ### Graphene -*Location: `/node-graph`* +*Location: [`/node-graph`](https://github.com/GraphiteEditor/Graphite/tree/master/node-graph)* [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. ## Frontend/backend communication -Frontend-to-backend communication is achieved through a thin Rust translation layer in `/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. -Backend-to-frontend communication happens by sending a queue of messages to the frontend message dispatcher. After the TS has called any wrapper API function to get into backend code execution, the editor's business logic runs and queues up `FrontendMessage`s (defined in `/editor/src/messages/frontend/frontend_message.rs`) which get mapped from Rust to TS-friendly data types in `/frontend/src/messages.ts`. Various TS code subscribes to these messages by calling `subscribeJsMessage(MessageName, (messageData) => { /* callback code */ });`. - -## The message system - -The Graphite editor backend is organized into a hierarchy of subsystems, called *message handlers*, which talk to one another through message passing. Messages are pushed to the front or back of a queue and each one is processed sequentially by the backend's dispatcher. - -The dispatcher lives at the root of the application hierarchy and it owns its message handlers. Thus, Rust's restrictions on mutable borrowing are satisfied because only the dispatcher mutably borrows its message handlers, one at a time, while each message is processed. - -### Messages - -Messages are enum variants that are dispatched to perform some intended activity within their respective message handlers. Here are two `DocumentMessage` definitions: -```rs -pub enum DocumentMessage { - ... - // A message that carries one named data field - DeleteLayer { - id: NodeId, - } - // A message that carries no data - DeleteSelectedLayers, - ... -} -``` - -As shown above, additional data fields can be included with each message. But as a special case denoted by the `#[child]` attribute, that data can also be a sub-message enum, which enables hierarchical nesting of message handler subsystems. - -
-To view the hierarchical subsystem file structure: click here - - - -``` -messages -β”œβ”€β”€ broadcast -β”‚Β Β  β”œβ”€β”€ broadcast_message.rs -β”‚Β Β  └── broadcast_message_handler.rs -β”œβ”€β”€ debug -β”‚Β Β  β”œβ”€β”€ debug_message.rs -β”‚Β Β  └── debug_message_handler.rs -β”œβ”€β”€ dialog -β”‚Β Β  β”œβ”€β”€ dialog_message.rs -β”‚Β Β  β”œβ”€β”€ dialog_message_handler.rs -β”‚Β Β  β”œβ”€β”€ export_dialog -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ export_dialog_message.rs -β”‚Β Β  β”‚Β Β  └── export_dialog_message_handler.rs -β”‚Β Β  β”œβ”€β”€ new_document_dialog -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ new_document_dialog_message.rs -β”‚Β Β  β”‚Β Β  └── new_document_dialog_message_handler.rs -β”‚Β Β  └── preferences_dialog -β”‚Β Β  β”œβ”€β”€ preferences_dialog_message.rs -β”‚Β Β  └── preferences_dialog_message_handler.rs -β”œβ”€β”€ frontend -β”‚Β Β  └── frontend_message.rs -β”œβ”€β”€ globals -β”‚Β Β  β”œβ”€β”€ globals_message.rs -β”‚Β Β  └── globals_message_handler.rs -β”œβ”€β”€ input_mapper -β”‚Β Β  β”œβ”€β”€ input_mapper_message.rs -β”‚Β Β  β”œβ”€β”€ input_mapper_message_handler.rs -β”‚Β Β  └── key_mapping -β”‚Β Β  β”œβ”€β”€ key_mapping_message.rs -β”‚Β Β  └── key_mapping_message_handler.rs -β”œβ”€β”€ input_preprocessor -β”‚Β Β  β”œβ”€β”€ input_preprocessor_message.rs -β”‚Β Β  └── input_preprocessor_message_handler.rs -β”œβ”€β”€ layout -β”‚Β Β  β”œβ”€β”€ layout_message.rs -β”‚Β Β  └── layout_message_handler.rs -β”œβ”€β”€ portfolio -β”‚Β Β  β”œβ”€β”€ document -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ document_message.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ document_message_handler.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ graph_operation -β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ graph_operation_message.rs -β”‚Β Β  β”‚Β Β  β”‚Β Β  └── graph_operation_message_handler.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ navigation -β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ navigation_message.rs -β”‚Β Β  β”‚Β Β  β”‚Β Β  └── navigation_message_handler.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ node_graph -β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ node_graph_message.rs -β”‚Β Β  β”‚Β Β  β”‚Β Β  └── node_graph_message_handler.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ overlays -β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ overlays_message.rs -β”‚Β Β  β”‚Β Β  β”‚Β Β  └── overlays_message_handler.rs -β”‚Β Β  β”‚Β Β  └── properties_panel -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ properties_panel_message.rs -β”‚Β Β  β”‚Β Β  └── properties_panel_message_handler.rs -β”‚Β Β  β”œβ”€β”€ menu_bar -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ menu_bar_message.rs -β”‚Β Β  β”‚Β Β  └── menu_bar_message_handler.rs -β”‚Β Β  β”œβ”€β”€ portfolio_message.rs -β”‚Β Β  └── portfolio_message_handler.rs -β”œβ”€β”€ preferences -β”‚Β Β  β”œβ”€β”€ preferences_message.rs -β”‚Β Β  └── preferences_message_handler.rs -β”œβ”€β”€ tool -β”‚Β Β  β”œβ”€β”€ tool_message.rs -β”‚Β Β  β”œβ”€β”€ tool_message_handler.rs -β”‚Β Β  β”œβ”€β”€ tool_messages -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ artboard_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ brush_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ ellipse_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ eyedropper_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ fill_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ freehand_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ gradient_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ line_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ navigate_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ path_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ pen_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ polygon_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ rectangle_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ select_tool.rs -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ spline_tool.rs -β”‚Β Β  β”‚Β Β  └── text_tool.rs -β”‚Β Β  └── transform_layer -β”‚Β Β  β”œβ”€β”€ transform_layer_message.rs -β”‚Β Β  └── transform_layer_message_handler.rs -└── workspace - β”œβ”€β”€ workspace_message.rs - └── workspace_message_handler.rs -``` - -
- -By convention, regular data must be written as struct-style named fields (shown above), while a sub-message enum must be written as a tuple/newtype-style field (shown below). The `DocumentMessage` enum of the previous example is defined as a child of `PortfolioMessage` which wraps it like this: +Backend-to-frontend communication happens by sending a queue of messages to the frontend message dispatcher. After the TS has called any wrapper API function to get into backend code execution, the editor's business logic runs and queues up each [`FrontendMessage`](https://github.com/GraphiteEditor/Graphite/tree/master/editor/src/messages/frontend/frontend_message.rs) which get mapped from Rust to JavaScript data structures in [`/frontend/src/messages.ts`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/src/messages.ts). Various TS code subscribes to these messages by calling: ```rs -pub enum PortfolioMessage { - ... - // A message that carries the `DocumentMessage` child enum as data - #[child] - Document(DocumentMessage), - ... -} +subscribeJsMessage(MessageName, (messageData) => { /* callback code */ }); ``` - -Likewise, the `PortfolioMessage` enum is wrapped by the top-level `Message` enum. The dispatcher operates on the queue of these base-level `Message` types. - -So for example, the `DeleteSelectedLayers` message mentioned previously will look like this as a `Message` data type: - -```rs -Message::Portfolio( - PortfolioMessage::Document( - DocumentMessage::DeleteSelectedLayers - ) -) -``` - -Writing out these nested message enum variants would be cumbersome, so that `#[child]` attribute shown earlier invokes a proc macro that automatically implements the `From` trait, letting you write this instead to get a `Message` data type: - -```rs -DocumentMessage::DeleteSelectedLayers.into() -``` - -Most often, this is simplified even further because the `.into()` is called for you when pushing a message to the queue with `.add()` or `.add_front()`. So this becomes as simple as: - -```rs -responses.add(DocumentMessage::DeleteSelectedLayers); -``` - -The `responses` message queue is composed of `Message` data types, and thanks to this system, child messages like `DocumentMessage::DeleteSelectedLayers` are automatically wrapped in their ancestor enum variants to become a `Message`, saving you from writing the verbose nested form. diff --git a/website/content/volunteer/guide/codebase-overview/debugging-tips.md b/website/content/volunteer/guide/codebase-overview/debugging-tips.md index 2e793326..581f9ea1 100644 --- a/website/content/volunteer/guide/codebase-overview/debugging-tips.md +++ b/website/content/volunteer/guide/codebase-overview/debugging-tips.md @@ -2,7 +2,7 @@ title = "Debugging tips" [extra] -order = 4 # Page number after chapter intro +order = 2 # Page number after chapter intro +++ The Wasm-based editor has some unique limitations about how you are able to debug it. This page offers tips and best practices to get the most out of your problem-solving efforts. diff --git a/website/content/volunteer/guide/codebase-overview/editor-structure.md b/website/content/volunteer/guide/codebase-overview/editor-structure.md new file mode 100644 index 00000000..d07d7e23 --- /dev/null +++ b/website/content/volunteer/guide/codebase-overview/editor-structure.md @@ -0,0 +1,98 @@ ++++ +title = "Editor structure" + +[extra] +order = 1 # Page number after chapter intro +css = ["/page/developer-guide-editor-structure.css"] +js = ["/js/developer-guide-editor-structure.js"] ++++ + +The Graphite editor is the application users interact with to create documents. Its code is one Rust crate sandwiched between the frontend and Graphene, the node-based graphics engine. The main business logic of all visual editing is handled by the editor backend. When running in the browser, it is compiled to WebAssembly and passes messages to the frontend. + +## Message system + +The Graphite editor backend is organized into a hierarchy of subsystems which talk to one another through message passing. Messages are pushed to the front or back of a queue and each one is processed sequentially by the editor's dispatcher. + +The dispatcher lives at the root of the editor hierarchy and acts as the owner of all its top-level message handlers. This satisfies Rust's restrictions on mutable borrows because only the dispatcher may mutate its message handlers, one at a time, while each message is processed. + +## Editor outline + +Click to explore the outline of the editor subsystem hierarchy which forms the structure of the editor's subsystems, state, and interactions. Bookmark this page to reference it later. + +
+ +
+ +### Parts of the hierarchy + +Subsystem components + +- A *Message enum is the component of an editor subsystem that defines its message interfaces as enum variants. Messages are used for passing a request from anywhere in the application, optionally with some included data, to have a particular block of code be run by its respective message handler. + +- A *MessageHandler struct is the component of an editor subsystem that has ownership over its persistent editor state and its child message handlers for the lifetime of the application. It also defines the logic for handling each of its messages that it receives from the dispatcher. Those blocks of logic may further enqueue additional messages to be processed by itself or other message handlers during the same dispatch cycle. + +- A *MessageContext struct is the component of an editor subsystem that defines what data is made available from other subsystems when running the logic to handle a dispatched message. It is a struct that is passed to the message handler when processing a message, and it gets filled in with data (owned, borrowed, or mutably borrowed) from its parent message handler. Intermediate subsystem layers may forward data from their parent to their child contexts to make state available from further up the hierarchy. + +Sub-messages + +- A #[child] * attribute-decorated message enum variant is a special kind of message that encapsulates a nested subsystem. As with all messages, its handler has a manually written code block. But that code must call its corresponding child message handler's `process_message` method. The child message handler is a field of this parent message handler's state struct. + +`Messages` + +- A `*` message enum variant is used throughout the editor to request that a certain subsystem performs some action, potentially given some data. In that sense, it resembles a function call, but a key difference is that messages are queued up and processed sequentially in a flat order, always invoked by the dispatcher. + +## How messages work + +Messages are enum variants that are dispatched to perform some intended activity within their respective message handlers. Here are two DocumentMessage definitions: +```rs +pub enum DocumentMessage { + ... + // A message that carries one named data field + DeleteLayer { + id: NodeId, + } + // A message that carries no data + DeleteSelectedLayers, + ... +} +``` + +As shown above, additional data fields can be included with each message. But as a special case denoted by the #[child] attribute, that data can also be a sub-message enum, which enables hierarchical nesting of message handler subsystems. + +By convention, regular data must be written as struct-style named fields (shown above), while a sub-message enum must be written as a tuple/newtype-style field (shown below). The DocumentMessage enum of the previous example is defined as a child of PortfolioMessage which wraps it like this: + +```rs +pub enum PortfolioMessage { + ... + // A message that carries the `DocumentMessage` child enum as data + #[child] + Document(DocumentMessage), + ... +} +``` + +Likewise, the PortfolioMessage enum is wrapped by the top-level Message enum. The dispatcher operates on the queue of these base-level Message types. + +So for example, the `DeleteSelectedLayers` message mentioned previously will look like this as a Message data type: + +```rs +Message::Portfolio( + PortfolioMessage::Document( + DocumentMessage::DeleteSelectedLayers + ) +) +``` + +Writing out these nested message enum variants would be cumbersome, so that #[child] attribute shown earlier invokes a proc macro that automatically implements the `From` trait, letting you write this instead to get a Message data type: + +```rs +DocumentMessage::DeleteSelectedLayers.into() +``` + +Most often, this is simplified even further because the `.into()` is called for you when pushing a message to the queue with `.add()` or `.add_front()`. So this becomes as simple as: + +```rs +responses.add(DocumentMessage::DeleteSelectedLayers); +``` + +The `responses` message queue is composed of Message data types, and thanks to this system, child messages like `DocumentMessage::DeleteSelectedLayers` are automatically wrapped in their ancestor enum variants to become a Message, saving you from writing the verbose nested form. diff --git a/website/other/editor-structure/generate.js b/website/other/editor-structure/generate.js new file mode 100644 index 00000000..b9ce6d99 --- /dev/null +++ b/website/other/editor-structure/generate.js @@ -0,0 +1,120 @@ +const fs = require("fs"); +const path = require("path"); + +/** + * Escapes characters that have special meaning in HTML. + * @param {string} text The text to escape. + * @returns {string} The escaped text. + */ +function escapeHtml(text) { + return text.replace(//g, ">"); +} + +/** + * Parses a single line of the input text. + * @param {string} line The line to parse. + * @returns {{ level: number, text: string, link: string | undefined }} + */ +function parseLine(line) { + 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. + * @param {Array} nodes The array of parsed node objects. + * @param {number} currentIndex The current index in the nodes array. + * @param {number} currentLevel The current indentation level. + * @returns {{html: string, nextIndex: number}} + */ +function buildHtmlList(nodes, currentIndex, currentLevel) { + if (currentIndex >= nodes.length) { + return { html: "", nextIndex: currentIndex }; + } + + let html = "\n"; + return { html, nextIndex: i }; +} + +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.js "); + 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); +} diff --git a/website/sass/base.scss b/website/sass/base.scss index d938d26a..786f22d3 100644 --- a/website/sass/base.scss +++ b/website/sass/base.scss @@ -11,17 +11,17 @@ --color-walnut: #473a3a; --color-slate: #3a4047; --color-crimson: #803847; - --color-lilac: #cabdc8; + --color-lilac: #e5e0eb; // --color-lime: #c5e0af; --color-lemon: #efe2b2; --color-peach: #ebb29f; --color-ale: #cd8f7a; - // --color-flamingo: #d2697c; + --color-flamingo: #d2697c; --color-seaside: #b0d6cb; --color-seaside-rgb: 176, 214, 203; // --color-cove: #83c0b9; // --color-sage: #91b99a; - // --color-storm: #627088; + --color-storm: #495875; --max-width: 1200px; --max-width-plus-padding: calc(var(--max-width) + 40px * 2); @@ -313,7 +313,6 @@ body > .page { } details[open] summary::before { - // content: "β–Ό"; transform: rotate(90deg); } } diff --git a/website/sass/page/developer-guide-editor-structure.scss b/website/sass/page/developer-guide-editor-structure.scss new file mode 100644 index 00000000..3e649aa6 --- /dev/null +++ b/website/sass/page/developer-guide-editor-structure.scss @@ -0,0 +1,106 @@ +.structure-outline { + font-family: monospace; + font-size: 18px; + line-height: 1.5; + margin-top: 20px; + + ul { + list-style-type: none; + padding-left: 20px; + + li { + margin-top: 0; + + span { + line-height: 1.5; + display: inline-block; + } + } + } + + .tree-node { + padding-left: calc(10px + 8px); + position: relative; + user-select: none; + cursor: pointer; + + &::before { + content: ""; + background: url('data:image/svg+xml;utf8,\ + \ + '); + position: absolute; + margin: auto; + top: 0; + bottom: 0; + left: 0; + width: 10px; + height: 10px; + } + + &.expanded::before { + transform: rotate(90deg); + } + + a { + margin-left: 12px; + color: var(--color-crimson); + font-size: 12px; + font-family: Arial, sans-serif; + position: relative; + + &:hover::after { + content: "β†—"; + margin-left: 4px; + position: absolute; + } + } + } + + .tree-leaf { + margin-left: calc(10px + 8px); + + &.field { + padding-left: 4px; + color: var(--color-storm); + } + + &:not(.field) { + padding: 0 4px; + background: var(--color-fog); + } + } + + .nested { + display: none; + } + + .active { + display: block; + } + + .warn { + margin-left: 12px; + color: var(--color-flamingo); + font-family: Arial, sans-serif; + font-size: 12px; + text-decoration: none; + font-style: italic; + } +} + +.subsystem, +.submessage { + font-family: monospace; + line-height: 1.5; + padding: 0 4px; +} + +.subsystem { + color: #ffffff; + background: var(--color-storm); +} + +.submessage { + background: var(--color-lilac); +} diff --git a/website/static/js/developer-guide-editor-structure.js b/website/static/js/developer-guide-editor-structure.js new file mode 100644 index 00000000..91768566 --- /dev/null +++ b/website/static/js/developer-guide-editor-structure.js @@ -0,0 +1,17 @@ +document.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll(".tree-node").forEach((toggle) => { + toggle.addEventListener("click", (event) => { + // Prevent link click from also toggling parent + if (event.target.tagName === "A") return; + + const nestedList = toggle.parentElement.querySelector(".nested"); + if (nestedList) { + toggle.classList.toggle("expanded"); + nestedList.classList.toggle("active"); + } + }); + }); + + // Expand the first level by default + document.querySelector(".structure-outline > ul > li > .tree-node")?.click(); +}); diff --git a/website/templates/base.html b/website/templates/base.html index d3ed68c4..29c6c1fa 100644 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -36,9 +36,9 @@ {%- set global_linked_css = [] -%} {%- set global_js = ["/js/text-justification.js", "/js/navbar.js"] -%} {%- set global_css = ["/base.css", "/fonts/common.css"] -%} - {%- set fonts_loaded = load_data(path="static/fonts/common.css", format="plain", required=false) -%} + {%- set fonts_loaded = load_data(path = "static/fonts/common.css", format = "plain", required = false) -%} {%- if not fonts_loaded -%} - {{ throw(message="------------------------------------------------------------> FONTS ARE NOT INSTALLED! Before running Zola, execute `npm run install-fonts` from the `/website` directory.") }} + {{ throw(message = "------------------------------------------------------------> FONTS ARE NOT INSTALLED! Before running Zola, execute `npm run install-fonts` from the `/website` directory.") }} {%- endif -%} {#- RETRIEVE FROM TEMPLATES AND PAGES: CSS AND JS TO LOAD EITHER AS A LINK OR INLINE -#} @@ -125,9 +125,11 @@
{%- filter replace(from = "", to = replacements::blog_posts(count = 2)) -%} {%- filter replace(from = "", to = replacements::text_balancer()) -%} + {%- filter replace(from = "", to = replacements::hierarchical_message_system_tree()) -%} {%- block content -%}{%- endblock -%} {%- endfilter -%} {%- endfilter -%} + {%- endfilter -%}