Fallible
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # Declarative Plugins / "Plugins as Data" for Bevy ## Isolating `&mut App` and `&mut World` from Plugin registration **Tl;dr: I'm arguing we introduce the invariant "plugin registration does not touch `&mut App` or `&mut World` directly"** so we can make a bunch of things work Just Fine eventually such as **plugin dependencies**, re-registration during runtime, more information for the editor, and better hot-patching. Reasoning about plugins that have direct access to `&mut World` will only continue to cause headaches over time as the possibilities of plugins keep being tied back to "this has `&mut App`/`&mut World` access." Plugins should be about building up a database and configuration of systems, resources, observers, etc. rather than sequential, potentially arbitrary computation on `&mut App` or `&mut World`. --- ## 1. Design Goals - Move plugin registration in the direction of being "data" / configuration. - Restricted operations on an opaque type. - Operations that genuinely require World access get moved to startup/pre-startup systems. - The final set of plugin data structures get "solved" between the end of plugin registration and the start of the app running. - Make users worry less about the details of plugin development and registration. - Expose data about game setup in a standard, reliable way to Editors. - Provide users with a way to resolve contradictions between plugins on their own without necessarily needing to patch or vendor dependencies. --- Bevy currently registers plugins in a procedural way by handing off a `&mut App` to all plugin authors. This is great for plugin author flexibility, but conflicts with both hot patching workflows and future potential for having plugins expose information in a consistent enough way to make an Editor-App split trivial to reason about. Additionally, we run into problems when two plugins add the same sub-plugin. A plugin system where **we care about the information the user is providing** rather than the user caring about what they are being passed will let us reflect the needs of users, plugin developers, and editor developers better. Few users actually break into using `SubApps` etc. The end result of this kind of API should look largely the same to most users, but being able to say "the registration function can only do certain operations" and "contradictions between registrations can be resolved by end users" will do a lot to make plugins closer to what people expect from the plugin infrastructure of other tools. ## 2. Why change? We need a more consistent, repeatable way of reasoning about what we expose to bevy through the Plugin API as Bevy matures and projects grow. People will want access to a lot of information about running programs, as well as the ability to toggle what's getting run without touching their codebase. ### 2.1 Treating Registrations as Data Registration is an act that introduces knowledge to Bevy. By registering an item we increase the number of systems, messages, observers etc. that Bevy knows about. By registering something, we say "This thing exists, I want you to know about it, here is its relevant information." This is the core of what we do in `Plugins`, we provide type information, system information, observer information, relationship information etc. This is what the Plugin framework is for. A declarative plugin architecture lets us put a barrier between the `App` (and therefore `World`) and the act of registration. There is a major knowledge management benefit from this: we establish the invariant "**registration does not touch a `App` or `World` directly, it just produces a database[^database] of types, systems, and configurations**." This lets us extend the purpose of plugin registration past "set up the Bevy application" and into "**produce a database of Bevy-relevant objects and reason about how to turn that into a running app.**" [^database]: I don't mean this in the sense of SQL but in the sense of "the relevant data is together in one place." We're not serializing functions :) ### 2.2 Expose more to the Editor, and more reliably. We're reaching a point where "the editor" is becoming real. Once we have a BSN asset format, people will be writing editors for it (and some already are, see: [jackdaw](https://github.com/jbuehler23/jackdaw).) With the existence of an editor, expectations wills grow. If common features for analysis and extensibility don't exist early, users will at the very least want to know if there is a path to implementation. Declarative plugins will help us expose an API surface that will remove a lot of pain by eliminating an escape hatch that practically nobody uses, and few should be using. Being able to reliably construct a database existing Bevy program and expose that data to an Editor without assuming deep integration is where we want to be. We can solve the same graph differently depending on if we're in Editor logic or App logic. What's stopping us from doing this right now? We expose the same information, but we don't have the invariants that make this easy to reason about with confidence. There is no split between "Editor space" items and "App space" items because we do not have such a split. Introducing such a split is possible under current infrastructure, but we would always be stuck reasoning about things that can touch a `&mut World`! #### 2.2.1 Editor Plugins in Plugin Registration Given an Editor is an application that reasons about Bevy data for a Bevy program in a specific way without running the whole schedule of program, and users will want to extend editors with purpose-built design tooling, having a way to reliably separate things that matter to the editor from things that matter to the program without introducing a whole new plugin API would be reasonable. Having an intermediary step between registration and application of items to the world lets us do things like select items to actually add to the world that are relevant to the execution context, such as _editor-specific behaviors_. Editors have extra things to care about, the [Jackdaw Wing RFC](https://github.com/jbuehler23/jackdaw/issues/38) exposes an extra set of Editor Items that would be relevant to the context of viewing information in the editor but not in the context of running a Game/Program/etc. We can imagine how this slots into this API: the Editor-only information is discarded before the graph is applied to the App in a Game/Program setting, or the Game/Program information is discarded before the graph is applied in an Editor-only setting. Items are inspected for there relevance for a given context, and for Game/Programs vs Editors there is a relatively easy split. This was published today (2026/04/14), so I've not turned it around in my head too much ### 2.3 Solve dependencies One of the big problems in the current plugin infrastructure is Dependencies. This isn't a big issue right now because the Bevy ecosystem is _shallow and wide_, but this risks being a chicken-and-egg situation. If dependencies between plugins isn't solved, then how do we author plugins that depend on other plugins? A plugin becomes a graph with the following items: - **Items** (anything we register) - **Dependencies** ("We want plugin X" or "We want resource Y") - **Resolution callbacks** to choose between defaults With this we have enough information to construct a full dependency graph of a program by merging these individual plugins and solving it through **topological sorting**, panicking only in the case where plugins have deeply contradictory and highly opinionated needs. ### 2.4 Allow overriding AKA Data driven system ordering Some of the items we introduce to Bevy are _Defaults_ about a piece of information, such as system schedule labels. We do not need to treat some of this _default information_ as the source of truth all the time. In the case of System schedules, we get to map out an entire Bevy application's system graph through the information exposed in plugin registration calls. Then, we have a an opportunity between solving that graph and running the Bevy app to read data not defined in code that lets us re-order systems however the designer etc. sees fit. ### 2.5 Better hot-patching experience AKA Idempotent plugin registration We got hot-patching in Bevy 0.17 via subsecond + dioxus's `dx` tooling. This has been a game changer for development iteration times in some areas, but it's lacking in others. Plugin registration functions are not re-run on hot-patching. This means that new types, systems, global observers etc. are not made visible to a running Bevy application during these processes. We cannot rerun plugin registration functions as it is. Re-registering something that already exists either 1. duplicates existing information (a system gets registered twice) or 2. causes a panic (plugin re-registering). We are encouraged to create plugins in a tree-like hierarchy. This means that higher-level plugins often end up causing many smaller plugins to be registered too. This means plugin re-registration being Being able to safely re-run a plugin registration function by having the full dependency graph and its diff between hot-patches available to us puts us in a position where we have enough information to rebuild and replace / disable everything we need to with a higher degree of safety. Or, as much as we can in a code hot-patching context. ### 2.6 ...Dynamic plugins? If we have a general data structure that can hold arbitrary registration information (types, reflected type information, systems, observers, schedule labels, etc.) and a consistent entry point for getting that information (`extern "C"` a named plugin registration function) then we have something that, if you squint, looks like a Bevy-specific ABI. But! Don't we already have these prerequisites with the existing plugin infrastructure? Kind of. Existing infrastructure is fragile and its purpose ends up more procedural. There's always the potential for access to `World`! This makes registration non-idempotent. An entry point that is "this function will establish a database for information about a plugin and throw it back to you to solve" is a more stable foundation to build dynamic plugins on than what we have currently, and is much closer to how ABIs work across language boundaries. While not a perfect solution this still puts us in a position where we can produce dynamic libraries with an obvious entry point for Bevy applications. TBD TBH, this is not my strong suit. I imagine that there will be a lot of ignoring whatever Rust type ID information exists for items and instead referring to whatever Bevy's reflect information comes to, as well as some complication surrounding how the VTables of these things will work across a FFI boundary. Dynamic plugins are still a solid "want this" to a lot of people. People who want to make proprietary Bevy apps extensible with rust code or make proprietary extensions for things like the editor, modders who want "full access" to inserting new information to bevy programs etc. ### 2.7 Plugin groups are just plugins that have dependencies and register no new information. Plugin groups serve as the existing solution to "a bunch of plugins depend on each other." ## 3. Existing / Possible Solutions 1. Don't Do Anything - Plugins work fine - A major change would touch every single part of bevy after all, and its entire ecosystem. 2. Async plugins - Solves Plugin dependencies, but doesn't step away from "procedural" APIs - This is a bit too imperative for my liking - Solves 1 problem, but doesn't address others. - Assumes a context where we still want to do arbitrary operations on `App` or an async-focused indirection of `App`, which may be true for some plugins. 3. Keep everything the same but make adding plugins & other registration operations idempotent. - Puts extra checking-if-present logic in registration operations. - Doesn't expose trivial & extra control to alternative execution contexts (like the editor) - Maintains the current reasoning model. ### 3.1 Don't do anything ### 3.2 Async plugins This solves plugin dependencies but maintains the procedural model of plugin registration. This also maintains the idea that plugins need access to `&mut App` et al. ## 4. Potential Future API 1. `Plugin` - `#[deprecated]` alias for `ProceduralPlugin`/`FoundationalPlugin`. 2. `ProceduralPlugin`/`FoundationalPlugin` - We **cannot** rerun this on hot-patching - This is what the current plugin API is. - We can do anything here, we get a `&mut App` - Still useful in some circumstances! - Could still be `async` - We should, over time, make this looks more like a Scary Engine Internal rather than the preferred entry point for users. 3. `DeclarativePlugin` - We **can** rerun this on hot-patching (hypothetically) - A plugin system that makes bevy "aware" of functions, methods, types at run time. - Limited operations: - Register systems with a schedule label _preference_ or _default_ - Register schedule labels - Register messages - Ask for resources - Add observers - Register a dependency on another plugin, with a preferred "plugin state" input. - Focuses on handing a coherent data structure to bevy to "solve" - We can perform "dry runs" on the information this gives to bevy in contexts like i.e. the editor. - **Produces a graph-like data structure** that can be merged with other outputs of other `DeclarativePlugin` implementations. ## 5. In Practice This is a sketch of what could be possible from a declarative API. This isn't meant to be much different from the existing API, but the semantics are closer to suggesting defaults than setting values. ```rust pub struct MyDeclrPlugin; impl DeclarativePlugin for MyDeclrPlugin { fn build(&self, &app: &mut DeclarativePluginOutput) { // Identical to current system add/registration API app.add_systems(Update, my_normal_system); app.register_system(my_other_normal_system); // Registering systems, only adding some of them to schedules. app.add_systems_but_more_datay(( // Let the ECS know about system_1, don't put it in any given // schedule. system_1, // Let the ECS know about system_2 + put it in the update // schedulelabel. (system_2, Update), system_3, system_4, (system_5, Last), // Let the ECS know about system_6, and put it after system_2 (system_6, After(system_2)), )); // Adding schedule labels app.add_schedules(( // Schedule labels with a default of happening after startup After(Startup, ( MyCoolStartup, MyCoolPostStartup )), // Schedule labels with a default of happening before each // update Before(Update, ( MyPreUpdate, MyPostPreUpdate, )), // A schedule label that isn't assigned to anywhere in the // existing schedule. MyUnplacedSheduleLabel )); // Add an observer and default to it being a global observer app.add_observer(my_observer); // Register an observer, but don't put it anywhere and give it a // different name than its function name for i.e editors app.register_observer( // Observer my_named_observer, // Name it's registered under Some("giving_it_a_special_name".into()) ); // Register a message type app.add_message::<MyMessageType>(); // Register a plugin dependency app.require_plugin::<OtherPlugin>(); // Register a plugin dependency, offering up an initialization value // for it, and requiring that its `internal` value be exactly 7. app.require_plugin_with_value( // Proposed init value AnotherPlugin{ internal: 7 }, // Conflict resolution function |another_plugin| another_plugin.internal == 7 ); // Require a resource, no matter what its value is. app.require_resource::<SomeResource>(); // Require a resource, but its `another_resource` should be 10 or less. app.require_resource_with_value::<AnotherResource>( // Proposed init value AnotherResource{ value: 4.0 }, // Conflict resolution function |another_resource| another_resource.value < 10.0 ); } } ``` The API we want to establish internally is elaborated on a bit further down at [Design Data Structures](#2-Design-data-structures-and-Operations) ### 5.1 Non-Contradicting Cycles One of the benefits of a more "deferred" plugin registration process is that we get to deal with duplications and cycles in some categories of registration. Systems can be registered in multiple schedules. Components only need to be registered once. Messages are a trivial thing to register. ### 5.2 True Contradiction Points One way we can tackle deadlocks is by giving the plugin authors a way to evaluate, via callback, if a resource, plugin, or schedule deviates too far from their assumptions. We expect most conflict resolution callbacks to be of the form "`|_| true`" – they do not care what their dependency is initialized to, only that it exists. For these "everything's fine as long as it exists" dependencies, conflict resolution is trivial. The other ways we could solve this is with a static "preference precedence" value, or we could solve this simply by crashing. We will want some kind of precedence, as we want to be able to "override" things like resources the closer we are to the true running app logic. We can maybe consider depth-of-dependency here. ### 5.3 Resource Contradictions What happens if two plugins both have opinions about something like a `Resource`? Multiple plugins could register them There's a couple of ways to solve this 1. An additional callback of type `&R -> bool` when registering a "sensitive" type like a resource that "asks" if a resource with a specific config is "okay" and go through all the different potential `Resource` values one by one until all plugins return "true" on one of the resources - If we can't find a suitable resource for all plugins that want that resource, crash! - Problems: requires exposing resource internals for users to evaluate. But this is unlikely to be a super frequent case, as plugins from different crates don't usually have strong opinions on common resources. - It could also be that a plugin would want to evaluate resources in n-tuples i.e. "if this database exists and looks like this AND if this other resources matches my expectations, then we're all good." But this could also be the kind of logic that exists at Startup system time. 3. Pick one and let the program report errors, or crash if bevy is configured to crash on this kind of thing. 4. Each registration of a resource comes with an `Ord` "precedence" value and the one with the highest precedence wins out. This would have `O(N * M)` complexity where N is the number of resources registered and M is the number of plugins that registered that resource. These numbers are likely to be low, but it's something to keep in mind. ### 5.4 Plugin Dependency Contradictions One can imagine a circular dependency between three plugins. `Game` wants `A` who wants `B` wants `C` wants `A`, and maybe the initialization of plugin `A` from `Game` is different from the one `C` prefers! Once again, we have a couple of ways to solve this: 1. Like with resources, for a clash in dependencies between plugins of type `A` we have a callback of type `&A -> Bool` that takes the plugin with conflicting values and evaluates if a plugin will work just fine with that value. 3. Pick one and let the program report errors, or crash if bevy is configured to crash on this kind of thing. 4. Each registration of a plugin comes with an `Ord` "precedence" value and the one with the highest precedence wins out. Plugins in a declarative plugin "dependency graph" can be expanded to the point where we can solve for the particular items registered in each plugin. ### 5.5 Schedule Contradictions I personally do not know if you can place a `ScheduleLabel` into two places in the overall `Schedule`. If you can't, then two plugins registering the same schedule label at different points is something to consider having the program crash at. ## 6 Path to Implementation ### 6.1. Change the input type of `Plugin` to a newtype over `App` Most of the internal changes don't need to happen right away. The simplest change is newtyping `App` before passing it to plugin `build` methods for the declarative plugin system. ```rust #[deprecated("Use `DeclarativePlugin`, or `FoundationalPlugin`")] pub trait Plugin { /* Old API */ } // Newtype App to change the reasoning model for users. pub struct DeclarativePluginOutput(App); impl DeclarativePluginOutput { // Expose a similar/identical API as App wrt Plugins, but don't // expose `App`, `SubApp`, or `World`. } pub trait DeclarativePlugin: ... { fn build(&self, &mut DeclarativePluginOutput) { // Identical API usage to current model, for most users. } } ``` This establishes the main invariant we want, but only doing this wouldn't make plugins declarative. What it does do is prompt developers to figure out if they _really do need access to internals during registration_ and punts them to the "more internal" plugin system if they do. In this position, we've hidden `App` but a lot of operations still aren't idempotent. Additionally, nothing has changed wrt the ability to hot-patch registration. But because we've set up the invariant we need, replacing `App` with an opaque-to-the-user type, we can start to build up the declarative properties. ### 6.2. Design data structures and Operations We need the following data structures (names illustrative, not final): 1. `DeclarativePluginOutput`: Plugin Output 2. `PluginOutputSet`: Set of Plugin Output 3. `PluginGraph`: Graph of merged Plugin Outputs 4. `PluginGraphDiff`: Diff on Graph of merged Plugin Outputs So we can have the following operations: ```rust fn build_declr(&self: impl DeclarativePlugin, out: &mut DeclarativePluginOutput); // Plugin output can be added to the Plugin Set easily. fn add_plugin(self: PluginOutputSet, output: impl DeclarativePlugin) -> PluginOutputSet; // The plugin output set can be transformed into a graph. fn solve_set(self: &PluginOutputSet) -> Result<PluginGraph, Err>; // The graph can be diffed. fn graph_diff(self: &PluginGraph, other: &PluginGraph) -> Option<PluginGraphDiff>; // The graph can be applied to an app, directly replacing existing items. fn apply_graph(self: &mut App, graph: &PluginGraph) -> Result<(), Err>; ``` This could be missing things, as always. To be iterated on. Once these data structures are built we can replace the `App` in the newtype with a data structure that holds these pieces of data directly. #### 6.2.1 Individual plugin data structure (`DeclarativePluginOutput`) `DeclarativePluginOutput` is the "unit" of data that declarative plugin build functions output. It manages the following for **single plugin build functions**: 1. Plugin data structure init data. 2. Plugin dependencies. 3. Resources and the default preferences of those resources. 4. Conflict resolution callbacks for plugins, resources (and maybe schedule labels?) 5. Schedule labels and where to put them. 6. Message types. 7. Reflect information? TypeId info? We already store most of this information in Bevy apps somewhere. #### 6.2.2 Intermediary structure (`PluginSet`) This just needs to hold many instances of `DeclarativePluginOutput` and associate them with type information or any other uniquely-identifying information. #### 6.2.3 Wider data structure (`PluginGraph`) Once we have the data structure from individual plugins, we can build a more broad graph-like structure that contains: 1. Plugins (the rust data structure) and the plugins that it depends on / that depend on it. 2. Resources and the plugin source that initialized / requested them. 3. Pointers to systems, their bevy-ecs relevant information, their "default" schedule label, and which plugin inserted them. 4. Pointers to observers, their relevant ecs information, if they're registered under a specific name. 5. Messages that need to be initialized. 6. Schedule labels, and which plugin inserted them. From here we can build a graph, establish where conflicts exist, run conflict resolution callbacks to see if there's a solution, and if a solution is possible we can register & replace information in an `App` in the order we get from a topsort of the final dependency graph. #### 6.2.4 Graph Diff (`PluginGraphDiff`) Needed so we know what has changed, and how to minimally replace items in a running `World`. ### 6.3. Bridge gap in current Bevy API, if it exists. Draw the rest of the owl, etc. There's unstated API needs in this document, and I don't think all of them are currently met by the existing internal Bevy API. Downside of writing a technical document instead of a prototype. I don't think there's much to bridge, but this will depend on specific missing components from Bevy's internals. More here TBD. --- <details> <summary>Glossary</summary> - Items: Things that are registered by a plugin. Systems, global observers, messages, dependencies, etc. - Hot-patching / hot-patching: Code recompilation & re-injection at runtime, via the dioxus toolchain. - Idempotent/Idempotence: The ability to do something more than once without the duplicate action changing the state of things. i.e. `f(f(x)) == f(x) && f(x) != x`, or "The wall was green before I painted it red. Then I painted it red a second time, this didn't cause the application to crash because painting the wall the color it already was is an idempotent operation." </details>

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully