From 4b3db689cf3f67a62c545bd6e09d6df9ea47757b Mon Sep 17 00:00:00 2001 From: Pete Hayes Date: Wed, 14 Feb 2024 12:22:19 +1100 Subject: [PATCH] Migrate from GitHub Wiki --- .github/workflows/pages.yml | 49 ++ .gitignore | 1 + README.md | 18 +- book.toml | 12 + src/SUMMARY.md | 16 + src/cloud.md | 77 +++ src/getting_started/codecs.md | 87 +++ src/getting_started/first_steps.md | 129 ++++ src/getting_started/request_reply.md | 90 +++ src/getting_started/reusing_connections.md | 37 ++ src/getting_started/streams_and_sinks.md | 75 +++ src/introduction.md | 39 ++ src/selium.png | Bin 0 -> 29595 bytes theme/css/chrome.css | 649 +++++++++++++++++++++ theme/favicon.png | Bin 0 -> 890 bytes theme/head.hbs | 9 + theme/index.hbs | 349 +++++++++++ 17 files changed, 1635 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 .gitignore create mode 100644 book.toml create mode 100644 src/SUMMARY.md create mode 100644 src/cloud.md create mode 100644 src/getting_started/codecs.md create mode 100644 src/getting_started/first_steps.md create mode 100644 src/getting_started/request_reply.md create mode 100644 src/getting_started/reusing_connections.md create mode 100644 src/getting_started/streams_and_sinks.md create mode 100644 src/introduction.md create mode 100644 src/selium.png create mode 100644 theme/css/chrome.css create mode 100644 theme/favicon.png create mode 100644 theme/head.hbs create mode 100644 theme/index.hbs diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..8c752d4 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,49 @@ +name: Deploy book to Pages + +on: + push: + branches: ["main"] + + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + env: + MDBOOK_VERSION: 0.4 + steps: + - uses: actions/checkout@v4 + - name: Install mdBook + run: | + curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh + rustup update + cargo install --version ${MDBOOK_VERSION} mdbook + - name: Setup Pages + id: pages + uses: actions/configure-pages@v4 + - name: Build with mdBook + run: mdbook build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./book + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +book diff --git a/README.md b/README.md index 0fab3d9..71620c5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ -# guide -Selium user guide +# Selium User Guide + +Selium's user guide, built by [`mdBook`](https://github.com/rust-lang/mdBook). + +## Editing + +To render the book whilst making changes, mdBook has a built in web server to help: + +```bash +mdbook serve --open +``` + +## Deployment + +The rendered book is deployed to GitHub Pages using the `pages.yml` workflow. Any push to `main` +will trigger this deployment. diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..66ed946 --- /dev/null +++ b/book.toml @@ -0,0 +1,12 @@ +[book] +authors = ["Selium Developers"] +language = "en" +multilingual = false +src = "src" +title = "Selium User Guide" + +[output.html] +default-theme = "ayu" +preferred-dark-theme = "ayu" +git-repository-url = "https://github.com/seliumlabs/selium" +cname = "guide.selium.com" diff --git a/src/SUMMARY.md b/src/SUMMARY.md new file mode 100644 index 0000000..035076d --- /dev/null +++ b/src/SUMMARY.md @@ -0,0 +1,16 @@ +# Summary +![Selium.com](selium.png) + +[Introduction](./introduction.md) + +# Getting Started + +- [First Steps](./getting_started/first_steps.md) +- [Request/Reply](./getting_started/request_reply.md) +- [Reusing Connections](./getting_started/reusing_connections.md) +- [`Stream` and `Sink` traits](./getting_started/streams_and_sinks.md) +- [Codecs](./getting_started/codecs.md) + +--- + +- [Selium Cloud](./cloud.md) diff --git a/src/cloud.md b/src/cloud.md new file mode 100644 index 0000000..3bcaca9 --- /dev/null +++ b/src/cloud.md @@ -0,0 +1,77 @@ +# Selium Cloud + +Selium Cloud (Beta) consists of a managed Selium Server, authentication, certificate +management, dashboard and technical support. If you want a hands-free, production-ready +solution for all of your software comms, backed by the people that made Selium, this is +for you. + +## Getting Started + +#### 1. Sign up +To get started with Selium Cloud, you'll [need an account!](https://www.selium.com/#CTA) +Selium Cloud is free for life, and super affordable as you grow with us. + +#### 2. Create your first certificate +Once you've got your account, login to the Selium Cloud dashboard at +[cloud.selium.io](https://cloud.selium.io). From here, you can create TLS certificates +for each client on your account. + +After logging in, you should see a screen like this: + +Dashboard + +To create a certificate, fill in a name for your client, e.g. "web1.example.com", then +click the submit button. After a moment you should see a dialog box like this: + +Add client + +**Make sure you download the private key as you cannot download it again!** + +Add the public and private keys to your project and you're ready to start using Selium. + +## Basic Usage + +Selium Cloud's API feels just like running your own Selium server. The main difference is +when establishing a connection to the server. Instead of using `selium::custom()` to +connect to your own server, use `selium::cloud()` to connect to the cloud: + +```rust +let connection = selium::cloud() + .with_cert_and_key( + "./web1.example.com.der", + "./web1.example.com.key.der", + )? + .connect() + .await?; +``` + +Note that when using `selium::cloud()`, you won't need to specify a CA path or endpoint. +These details are baked into the Selium client for your convenience. + +#### Namespaces + +Each Selium Cloud account is linked to a unique _namespace_. This is used to distinguish +your data from other accounts, and is linked to every certificate you create. You can find +your namespace on the Selium Cloud dashboard: + +Namespace + +To use your namespace, simply prepend it to every topic name. For example, to publish the +topic "retail-transactions", you would code the following: + +```rust +let mut publisher = connection + .publisher("/example/retail-transactions") + ... + .await?; +``` + +### IMPORTANT NOTE! + +**You must compile your code with the `--release` flag for both testing and production +use. This is so the Selium client knows to use the _production_ Certificate Authority!** + +If you don't do this, you will not be able to connect to Selium. + +> N.B. We know this isn't ideal, and likely we'll be removing this restriction in future +> versions. We'd love your feedback on this too! diff --git a/src/getting_started/codecs.md b/src/getting_started/codecs.md new file mode 100644 index 0000000..f3b00a3 --- /dev/null +++ b/src/getting_started/codecs.md @@ -0,0 +1,87 @@ +# Codecs +Looking back at the previous chapters, you probably noticed lines like these ones: + +```rust +let mut publisher = connection + ... + .with_encoder(StringCodec) // allows you to exchange string messages between clients + +let mut subscriber = connection + ... + .with_decoder(StringCodec) // use the same codec as the publisher +``` + +If you've not worked with data streaming before, codecs might seem like a very strange +concept indeed. However the answer is quite simple. The term _codecs_ is a portmanteau of +_encoder_ and _decoder_. Thus a codec is simply a structure that can _encode_ frames of +data on one side of a connection, and _decode_ them on the other. + +In the example above, we use `StringCodec`, which does exactly what it says on the tin - +it _encodes_ strings to bytes and then _decodes_ those bytes back to strings. + +## Why do we need codecs? + +We need codecs because at the network level, computers only support sending raw bytes. +However most data we work with are not raw bytes - they're strings, integers, booleans, +enumerators, structures etc. In order to abstract away the pain of converting these types +to bytes and back again, we use a codec that knows how to do it for us. + +## Can I send more than just strings? + +Yep! With the help of the `serde` crate, we support sending all manner of Rust types. +In the example below, we use the `BincodeCodec` to send a stream of `StockEvent`s. + +You'll need to install `serde` with the _derive_ feature to follow this example: +```bash +$ cargo add -F derive serde +``` + +```rust +use futures::SinkExt; +use selium::{prelude::*, std::codecs::BincodeCodec}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +struct StockEvent { + ticker: String, + change: f64, +} + +impl StockEvent { + pub fn new(ticker: &str, change: f64) -> Self { + Self { + ticker: ticker.to_owned(), + change, + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let connection = selium::custom() + .endpoint("127.0.0.1:7001") + .with_certificate_authority("certs/client/ca.der")? + .with_cert_and_key( + "certs/client/localhost.der", + "certs/client/localhost.key.der", + )? + .connect() + .await?; + + let mut publisher = connection + .publisher("/some/topic") + .with_encoder(BincodeCodec::default()) + .open() + .await?; + + publisher.send(StockEvent::new("INTC", -9.0)).await?; + publisher.finish().await?; + + Ok(()) +} +``` + +## Can I build my own codec? + +Yes, and you can use third party codecs like protocol buffers, SBE etc. We'll have a chapter on +this coming soon. diff --git a/src/getting_started/first_steps.md b/src/getting_started/first_steps.md new file mode 100644 index 0000000..0a89e24 --- /dev/null +++ b/src/getting_started/first_steps.md @@ -0,0 +1,129 @@ +# First Steps + +Selium comprises a client library and a server binary. In order to use Selium, there are 3 basic +steps: +1. Create TLS certificates for your client and server +2. Run the server binary +3. Integrate the client library into your project + +## Step 1 - TLS Certificates + +First we need some certs to validate the client and server. Selium uses mutual TLS (_mTLS_) to +validate both parties cryptographically, making things nice and secure. + +We've built a tool to make this easy, so let's install that, then mint our certs: + +```bash +# Install the selium-tools CLI +$ cargo install selium-tools +# Use this CLI to create our certificates +$ selium-tools gen-certs +``` + +You should now have a directory in the current path called `certs/`. Inside we have certs for +the client and server, which you can move to a more convenient location if you like - the paths are +configurable in code. Both the `client/` and `server/` directories include a copy of the +certificate authority, which you'll need if you want to create more client certificates later. + +## Step 2 - Start the Selium Server + +The Selium server allows us to exchange messages between clients. For this example we'll grab a +copy from [crates.io](https://crates.io/crates/selium-server). + +> For production deployments you can also download prebuilt binaries from +> [GitHub](https://github.com/orgs/seliumlabs/packages?repo_name=selium). + +Let's start a new server with our freshly minted certs. In the same directory as your `certs/` +folder, open a new terminal and run the following commands: + +```bash +# Install Selium server +$ cargo install selium-server +# Run the server +$ selium-server --bind-addr=127.0.0.1:7001 +``` + +The `selium-server` command will not produce any output by default, but that doesn't mean it +isn't working! You can increase logging using the verbosity flag: + +```bash +$ selium-server -v # Warnings only +$ selium-server -vv # Info +$ selium-server -vvv # Debug +$ selium-server -vvvv # Trace +``` + +## Step 3 - Implement the Selium Client + +Selium Client is a composable library API for the Selium Server. Let's have a look at a minimal +example: + +```rust +use futures::{SinkExt, StreamExt}; +use selium::{prelude::*, std::codecs::StringCodec}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let connection = selium::custom() // connect to your own Selium server + .endpoint("127.0.0.1:7001") // your Selium server's address + .with_certificate_authority("certs/client/ca.der")? // your Selium cert authority + .with_cert_and_key( + "certs/client/localhost.der", + "certs/client/localhost.key.der", + )? // your client certificates + .connect() + .await?; + + let mut publisher = connection + .publisher("/some/topic") // choose a topic to group similar messages together + .with_encoder(StringCodec) // allows you to exchange string messages between clients + .open() // opens a new stream for sending data + .await?; + + let mut subscriber = connection + .subscriber("/some/topic") // subscribe to the publisher's topic + .with_decoder(StringCodec) // use the same codec as the publisher + .open() // opens a new stream for receiving data + .await?; + + // Send a message and close the publisher + publisher.send("Hello, world!".into()).await?; + publisher.finish().await?; + + // Receive the message + if let Some(Ok(message)) = subscriber.next().await { + println!("Received message: {message}"); + } + + Ok(()) +} +``` + +There's a lot to take in here, but for the moment let's just get this baby running! + +```bash +# Create a new Cargo project +$ cargo new --bin hello-selium + +# Move into our project +$ cd hello-selium + +# Add the crate dependencies +$ cargo add futures +$ cargo add -F std selium +$ cargo add -F macros,rt tokio +``` + +Now copy and paste the code above into `hello-selium/src/main.rs`. + +Now let's execute the project and start exchanging some messages! Make sure your server is still +running from the previous step. + +```bash +$ cargo run +Received message: Hello, world! +``` + +## Next Steps + +We've just setup a working Selium publish/subscribe project, but [you can also use RPC too.](./request_reply.md) diff --git a/src/getting_started/request_reply.md b/src/getting_started/request_reply.md new file mode 100644 index 0000000..1a81bdc --- /dev/null +++ b/src/getting_started/request_reply.md @@ -0,0 +1,90 @@ +# Request/Reply (RPC pattern) + +In the previous chapter we discussed our first steps with the publish/subscribe pattern. In this +chapter we will explore Selium's other major communication pattern - request/reply (otherwise known +as RPC). + +Request/reply is useful in the same way that a REST API is. For instance, you can use Selium to +replace your internal APIs: + +```rust +#[derive(Deserialize)] +enum Request { + LookupCustomer(u64), + // Other requests +} + +async fn main() -> Result<(), Box> { + let connection = selium::custom() + ... + + // Setup a database connection to demonstrate state sharing + let mut db = create_db()?; + + let mut replier = connection + .replier("/customers/api") // choose a topic to serve your API on + .with_request_decoder(StringCodec) // let's use a StringCodec to interface with JSON clients + .with_request_encoder(StringCodec) // let's use a StringCodec to interface with JSON clients + .with_handler(move |json| { + let req: Request = serde_json::from_str(&json)?; + match req { + Request::LookupCustomer(id) => lookup_customer(&mut db, r.client_id), + } + }) + .open() + .await?; + + replier.listen().await?; + + Ok(()) +} + +async fn lookup_customer(db: &mut Database, id: u64) -> Result { + // Lookup request... + + // Reply with a JSON response + Ok(r#" + { + "name": "John Doe", + "age": 43, + "phones": [ + "+44 1234567", + "+44 2345678" + ] + }"#) +} +``` + +And now for the requestor: + +```rust +async fn main() -> Result<(), Box> { + let connection = selium::custom() + ... + + let requestor = connection + .requestor("/some/endpoint") + .with_request_encoder(StringCodec) + .with_reply_decoder(StringCodec) + .with_request_timeout(Duration::from_secs(1))? + .open() + .await?; + + let customer = requestor.request(r#"{"LookupCustomer": 85028}"#).await.unwrap(); + + Ok(()) +} +``` + +### Benefits over HTTP + +1. No web servers +2. Built in mutual TLS +3. Ability to exchange pure Rust types over the wire +4. Transport much larger amounts of data, much faster +5. Multiple messaging patterns at your fingertips +6. 100% Rust toolchain + +## Next Steps + +Nice work, we've got all kinds of data flowing! Now it's time to learn about [reusing Selium connections.](./reusing_connections.md) diff --git a/src/getting_started/reusing_connections.md b/src/getting_started/reusing_connections.md new file mode 100644 index 0000000..fa2670c --- /dev/null +++ b/src/getting_started/reusing_connections.md @@ -0,0 +1,37 @@ +# Reusing Connections + +Selium supports reusing connections to the Selium server, otherwise known as _multiplexing_. This +allows you to maintain a single connection that publishes and/or subscribes to multiple topics +simultaneously. + +Let's have another look at that code from the previous chapter: + +```rust +async fn main() -> Result<(), Box> { + let connection = selium::custom() + ... + + let mut publisher = connection + .publisher("/some/topic") // choose a topic to group similar messages together + .with_encoder(StringCodec) // allows you to exchange string messages between clients + .open() // opens a new stream for sending data + .await?; + + let mut subscriber = connection + .subscriber("/some/topic") // subscribe to the publisher's topic + .with_decoder(StringCodec) // use the same codec as the publisher + .open() // opens a new stream for receiving data + .await?; + + ... + + Ok(()) +} +``` + +We can see that each time a new behaviour is required, we create a new stream using +`Client::publisher` and `Client::subscriber`. For each new stream, the full range of builder +options is available, uniquely for that stream. + +Next, let's see how we can send/receive data more ergonomically with +[`Stream` and `Sink` traits](./streams_and_sinks.md). diff --git a/src/getting_started/streams_and_sinks.md b/src/getting_started/streams_and_sinks.md new file mode 100644 index 0000000..b2fb8a1 --- /dev/null +++ b/src/getting_started/streams_and_sinks.md @@ -0,0 +1,75 @@ +# `Stream` and `Sink` traits + +If you've ever used Rust's `futures` or `tokio` crates, you'll almost certainly be aware +of the [`Stream`](https://docs.rs/futures/latest/futures/stream/trait.Stream.html) and +[`Sink`](https://docs.rs/futures/latest/futures/sink/trait.Sink.html) traits. These +traits govern the sending and receiving of messages between components, and are arguably +a de facto standard in Rust. + +> If you've never used these traits before, don't worry! Not only are they quite +> intuitive, but the [`futures` crate](https://docs.rs/futures/latest/futures/index.html) +> has great documentation explaining how they work. + +The good news for Selium users, is that Selium's `Publisher` and `Subscriber` both +implement `Sink` and `Stream` respectively. Thus they should fit hand in glove with your +existing streaming applications. + +You can also make use of the `futures` crate's +[`StreamExt`](https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html) and +[`SinkExt`](https://docs.rs/futures/latest/futures/sink/trait.SinkExt.html) extensions. +These traits are implemented by default on any `Stream`/`Sink` implementations (like +Selium's) and contain lots of helpful tools. + +Let's go back to our example from previous chapters and see what we can do with `SinkExt` +on a `Publisher`: + +```rust +use futures::{future, stream, SinkExt, Stream, StreamExt}; +use selium::{prelude::*, std::codecs::StringCodec}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let connection = selium::custom() + .endpoint("127.0.0.1:7001") + .with_certificate_authority("certs/client/ca.der")? // your Selium cert authority + .with_cert_and_key( + "certs/client/localhost.der", + "certs/client/localhost.key.der", + )? // your client certificates + .connect() // your Selium server's address + .await?; + + let publisher = connection + .publisher("/some/topic") // choose a topic to group similar messages together + .with_encoder(StringCodec) // allows you to exchange string messages between clients + .open() // opens a new stream for sending data + .await?; + + let mut sink = publisher.with(|item: String| { + if item.contains('@') { + future::ok(item) + } else { + future::ok("default_email@company.org".into()) + } + }); + sink.send_all(&mut some_stream()).await?; + + Ok(()) +} + +fn some_stream() -> impl Stream>> { + stream::iter(vec![ + "hello@world.com".into(), + "some@example.net".into(), + "notanemail.com".into(), + ]) + .map(|email| Ok(email)) +} +``` + +In this example, we take a stream of email addresses and optionally map invalid email addresses to +a default email address. Of course, this is not a very useful example, but the Rust ecosystem is +_full_ of `Stream`s and `Sink`s. Selium's native support for these traits allows you to slot Selium +client into your libraries and applications with ease. + +Next, let's get to grips with [codecs](./codecs.md). diff --git a/src/introduction.md b/src/introduction.md new file mode 100644 index 0000000..1df949e --- /dev/null +++ b/src/introduction.md @@ -0,0 +1,39 @@ +# Welcome to Selium's user guide + +Ahoy, and thanks for being part of the community! In this wiki you'll learn about Selium, its +components and how to use them. + +If at any point you find yourself thinking "I wish they'd cover ...", please +[start a new discussion](https://github.com/seliumlabs/selium/discussions/new?category=ideas). We would really value +your feedback and ideas. Thanks! + +## What is Selium? + +You may have seen this description of Selium on the organisation readme: "Selium is an extremely +developer friendly composable messaging platform with zero build time configuration." This is a +nice statement, but lacks any depth. So, let's add some, shall we? + +**_"Selium is a messaging platform."_** Messaging is simply a method of exchanging data in a +structured way. You may have come across the term "Pub-Sub", which is a method of sending +(_publishing_) the same message to lots of receivers (_subscribers_). That may be a bit of a grey +area to people new to messaging, but you'll definitely know this one: "HTTP". Yep that's right, web +requests are a form of messaging too. It's called an "RPC", or more colloquially "request-reply". + +**_"Selium is composable."_** We've built Selium as a collection of parts that you can stick +together to aggregate, manipulate and disseminate your data however you require, much like +everyone's favourite childrens' toy that rhymes with "Pego". You guessed it - Nanoblocks! + +**_"Selium is extremely developer friendly."_** Selium is designed from the ground up for developer +ergonomics. Whatever requirements you have for your services and data, you can compose them in at +runtime using our functional API. In other words, Selium has _"zero build time configuration"_. + +## Who should use Selium? + +_(Anyone with ops-related trauma)_ + +Selium is designed for software developers. If your project needs to move data around, expose +services, discover services, introduce resiliency, scale out, speed up, produce or consume events, +or otherwise run screaming from HTTP, Selium is for you. Liberate your stack from DevOps, simplify +your deployment pipeline and never use the word "idempotent" again. + +Let's [get started!](./getting_started/first_steps.md) diff --git a/src/selium.png b/src/selium.png new file mode 100644 index 0000000000000000000000000000000000000000..423ca3f8008a5f047bdc54020462bdf63b356638 GIT binary patch literal 29595 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 .hljs { + color: var(--links); +} + +/* + body-container is necessary because mobile browsers don't seem to like + overflow-x on the body tag when there is a tag. +*/ +#body-container { + /* + This is used when the sidebar pushes the body content off the side of + the screen on small screens. Without it, dragging on mobile Safari + will want to reposition the viewport in a weird way. + */ + overflow-x: clip; +} + +/* Menu Bar */ + +#menu-bar, +#menu-bar-hover-placeholder { + z-index: 101; + margin: auto calc(0px - var(--page-padding)); +} +#menu-bar { + position: relative; + display: flex; + flex-wrap: wrap; + background-color: var(--bg); + border-block-end-color: var(--bg); + border-block-end-width: 1px; + border-block-end-style: solid; +} +#menu-bar.sticky, +.js #menu-bar-hover-placeholder:hover + #menu-bar, +.js #menu-bar:hover, +.js.sidebar-visible #menu-bar { + position: -webkit-sticky; + position: sticky; + top: 0 !important; +} +#menu-bar-hover-placeholder { + position: sticky; + position: -webkit-sticky; + top: 0; + height: var(--menu-bar-height); +} +#menu-bar.bordered { + border-block-end-color: var(--table-border-color); +} +#menu-bar i, +#menu-bar .icon-button { + position: relative; + padding: 0 8px; + z-index: 10; + line-height: var(--menu-bar-height); + cursor: pointer; + transition: color 0.5s; +} +@media only screen and (max-width: 420px) { + #menu-bar i, + #menu-bar .icon-button { + padding: 0 5px; + } +} + +.icon-button { + border: none; + background: none; + padding: 0; + color: inherit; +} +.icon-button i { + margin: 0; +} + +.right-buttons { + margin: 0 15px; +} +.right-buttons a { + text-decoration: none; +} + +.left-buttons { + display: flex; + margin: 0 5px; +} +.no-js .left-buttons button { + display: none; +} + +.menu-title { + display: inline-block; + font-weight: 200; + font-size: 2.4rem; + line-height: var(--menu-bar-height); + text-align: center; + margin: 0; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.js .menu-title { + cursor: pointer; +} + +.menu-bar, +.menu-bar:visited, +.nav-chapters, +.nav-chapters:visited, +.mobile-nav-chapters, +.mobile-nav-chapters:visited, +.menu-bar .icon-button, +.menu-bar a i { + color: var(--icons); +} + +.menu-bar i:hover, +.menu-bar .icon-button:hover, +.nav-chapters:hover, +.mobile-nav-chapters i:hover { + color: var(--icons-hover); +} + +/* Nav Icons */ + +.nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + + position: fixed; + top: 0; + bottom: 0; + margin: 0; + max-width: 150px; + min-width: 90px; + + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + + transition: + color 0.5s, + background-color 0.5s; +} + +.nav-chapters:hover { + text-decoration: none; + background-color: var(--theme-hover); + transition: + background-color 0.15s, + color 0.15s; +} + +.nav-wrapper { + margin-block-start: 50px; + display: none; +} + +.mobile-nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + width: 90px; + border-radius: 5px; + background-color: var(--sidebar-bg); +} + +/* Only Firefox supports flow-relative values */ +.previous { + float: left; +} +[dir="rtl"] .previous { + float: right; +} + +/* Only Firefox supports flow-relative values */ +.next { + float: right; + right: var(--page-padding); +} +[dir="rtl"] .next { + float: left; + right: unset; + left: var(--page-padding); +} + +/* Use the correct buttons for RTL layouts*/ +[dir="rtl"] .previous i.fa-angle-left:before { + content: "\f105"; +} +[dir="rtl"] .next i.fa-angle-right:before { + content: "\f104"; +} + +@media only screen and (max-width: 1080px) { + .nav-wide-wrapper { + display: none; + } + .nav-wrapper { + display: block; + } +} + +/* sidebar-visible */ +@media only screen and (max-width: 1380px) { + #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { + display: none; + } + #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { + display: block; + } +} + +/* Inline code */ + +:not(pre) > .hljs { + display: inline; + padding: 0.1em 0.3em; + border-radius: 3px; +} + +:not(pre):not(a) > .hljs { + color: var(--inline-code-color); + overflow-x: initial; +} + +a:hover > .hljs { + text-decoration: underline; +} + +pre { + position: relative; +} +pre > .buttons { + position: absolute; + z-index: 100; + right: 0px; + top: 2px; + margin: 0px; + padding: 2px 0px; + + color: var(--sidebar-fg); + cursor: pointer; + visibility: hidden; + opacity: 0; + transition: + visibility 0.1s linear, + opacity 0.1s linear; +} +pre:hover > .buttons { + visibility: visible; + opacity: 1; +} +pre > .buttons :hover { + color: var(--sidebar-active); + border-color: var(--icons-hover); + background-color: var(--theme-hover); +} +pre > .buttons i { + margin-inline-start: 8px; +} +pre > .buttons button { + cursor: inherit; + margin: 0px 5px; + padding: 3px 5px; + font-size: 14px; + + border-style: solid; + border-width: 1px; + border-radius: 4px; + border-color: var(--icons); + background-color: var(--theme-popup-bg); + transition: 100ms; + transition-property: color, border-color, background-color; + color: var(--icons); +} +@media (pointer: coarse) { + pre > .buttons button { + /* On mobile, make it easier to tap buttons. */ + padding: 0.3rem 1rem; + } + + .sidebar-resize-indicator { + /* Hide resize indicator on devices with limited accuracy */ + display: none; + } +} +pre > code { + display: block; + padding: 1rem; +} + +/* FIXME: ACE editors overlap their buttons because ACE does absolute + positioning within the code block which breaks padding. The only solution I + can think of is to move the padding to the outer pre tag (or insert a div + wrapper), but that would require fixing a whole bunch of CSS rules. +*/ +.hljs.ace_editor { + padding: 0rem 0rem; +} + +pre > .result { + margin-block-start: 10px; +} + +/* Search */ + +#searchresults a { + text-decoration: none; +} + +mark { + border-radius: 2px; + padding-block-start: 0; + padding-block-end: 1px; + padding-inline-start: 3px; + padding-inline-end: 3px; + margin-block-start: 0; + margin-block-end: -1px; + margin-inline-start: -3px; + margin-inline-end: -3px; + background-color: var(--search-mark-bg); + transition: background-color 300ms linear; + cursor: pointer; +} + +mark.fade-out { + background-color: rgba(0, 0, 0, 0) !important; + cursor: auto; +} + +.searchbar-outer { + margin-inline-start: auto; + margin-inline-end: auto; + max-width: var(--content-max-width); +} + +#searchbar { + width: 100%; + margin-block-start: 5px; + margin-block-end: 0; + margin-inline-start: auto; + margin-inline-end: auto; + padding: 10px 16px; + transition: box-shadow 300ms ease-in-out; + border: 1px solid var(--searchbar-border-color); + border-radius: 3px; + background-color: var(--searchbar-bg); + color: var(--searchbar-fg); +} +#searchbar:focus, +#searchbar.active { + box-shadow: 0 0 3px var(--searchbar-shadow-color); +} + +.searchresults-header { + font-weight: bold; + font-size: 1em; + padding-block-start: 18px; + padding-block-end: 0; + padding-inline-start: 5px; + padding-inline-end: 0; + color: var(--searchresults-header-fg); +} + +.searchresults-outer { + margin-inline-start: auto; + margin-inline-end: auto; + max-width: var(--content-max-width); + border-block-end: 1px dashed var(--searchresults-border-color); +} + +ul#searchresults { + list-style: none; + padding-inline-start: 20px; +} +ul#searchresults li { + margin: 10px 0px; + padding: 2px; + border-radius: 2px; +} +ul#searchresults li.focus { + background-color: var(--searchresults-li-bg); +} +ul#searchresults span.teaser { + display: block; + clear: both; + margin-block-start: 5px; + margin-block-end: 0; + margin-inline-start: 20px; + margin-inline-end: 0; + font-size: 0.8em; +} +ul#searchresults span.teaser em { + font-weight: bold; + font-style: normal; +} + +/* Sidebar */ + +.sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: var(--sidebar-width); + font-size: 0.875em; + box-sizing: border-box; + -webkit-overflow-scrolling: touch; + overscroll-behavior-y: contain; + background-color: var(--sidebar-bg); + color: var(--sidebar-fg); +} +[dir="rtl"] .sidebar { + left: unset; + right: 0; +} +.sidebar-resizing { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} +.no-js .sidebar, +.js:not(.sidebar-resizing) .sidebar { + transition: transform 0.3s; /* Animation: slide away */ +} +.sidebar code { + line-height: 2em; +} +.sidebar .sidebar-scrollbox { + overflow-y: auto; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + padding: 10px 10px; +} +.sidebar .sidebar-resize-handle { + position: absolute; + cursor: col-resize; + width: 0; + right: calc(var(--sidebar-resize-indicator-width) * -1); + top: 0; + bottom: 0; + display: flex; + align-items: center; +} + +.sidebar-resize-handle .sidebar-resize-indicator { + width: 100%; + height: 12px; + background-color: var(--icons); + margin-inline-start: var(--sidebar-resize-indicator-space); +} + +[dir="rtl"] .sidebar .sidebar-resize-handle { + left: calc(var(--sidebar-resize-indicator-width) * -1); + right: unset; +} +.js .sidebar .sidebar-resize-handle { + cursor: col-resize; + width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space)); +} +/* sidebar-hidden */ +#sidebar-toggle-anchor:not(:checked) ~ .sidebar { + transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width))); + z-index: -1; +} +[dir="rtl"] #sidebar-toggle-anchor:not(:checked) ~ .sidebar { + transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width))); +} +.sidebar::-webkit-scrollbar { + background: var(--sidebar-bg); +} +.sidebar::-webkit-scrollbar-thumb { + background: var(--scrollbar); +} + +.sidebar .sidebar-scrollbox .sidebar-book-logo a img { + display: block; + margin-bottom: 2em; + margin-left: auto; + margin-right: auto; + width: 70%; + max-width: max-content; +} + +/* sidebar-visible */ +#sidebar-toggle-anchor:checked ~ .page-wrapper { + transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width))); +} +[dir="rtl"] #sidebar-toggle-anchor:checked ~ .page-wrapper { + transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width))); +} +@media only screen and (min-width: 620px) { + #sidebar-toggle-anchor:checked ~ .page-wrapper { + transform: none; + margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)); + } + [dir="rtl"] #sidebar-toggle-anchor:checked ~ .page-wrapper { + transform: none; + } +} + +.chapter { + list-style: none outside none; + padding-inline-start: 0; + line-height: 2.2em; +} + +.chapter ol { + width: 100%; +} + +.chapter li { + display: flex; + color: var(--sidebar-non-existant); +} +.chapter li a { + display: block; + padding: 0; + text-decoration: none; + color: var(--sidebar-fg); +} + +.chapter li a:hover { + color: var(--sidebar-active); +} + +.chapter li a.active { + color: var(--sidebar-active); +} + +.chapter li > a.toggle { + cursor: pointer; + display: block; + margin-inline-start: auto; + padding: 0 10px; + user-select: none; + opacity: 0.68; +} + +.chapter li > a.toggle div { + transition: transform 0.5s; +} + +/* collapse the section */ +.chapter li:not(.expanded) + li > ol { + display: none; +} + +.chapter li.chapter-item { + line-height: 1.5em; + margin-block-start: 0.6em; +} + +.chapter li.expanded > a.toggle div { + transform: rotate(90deg); +} + +.spacer { + width: 100%; + height: 3px; + margin: 5px 0px; +} +.chapter .spacer { + background-color: var(--sidebar-spacer); +} + +@media (-moz-touch-enabled: 1), (pointer: coarse) { + .chapter li a { + padding: 5px 0; + } + .spacer { + margin: 10px 0; + } +} + +.section { + list-style: none outside none; + padding-inline-start: 20px; + line-height: 1.9em; +} + +/* Theme Menu Popup */ + +.theme-popup { + position: absolute; + left: 10px; + top: var(--menu-bar-height); + z-index: 1000; + border-radius: 4px; + font-size: 0.7em; + color: var(--fg); + background: var(--theme-popup-bg); + border: 1px solid var(--theme-popup-border); + margin: 0; + padding: 0; + list-style: none; + display: none; + /* Don't let the children's background extend past the rounded corners. */ + overflow: hidden; +} +[dir="rtl"] .theme-popup { + left: unset; + right: 10px; +} +.theme-popup .default { + color: var(--icons); +} +.theme-popup .theme { + width: 100%; + border: 0; + margin: 0; + padding: 2px 20px; + line-height: 25px; + white-space: nowrap; + text-align: start; + cursor: pointer; + color: inherit; + background: inherit; + font-size: inherit; +} +.theme-popup .theme:hover { + background-color: var(--theme-hover); +} + +.theme-selected::before { + display: inline-block; + content: "✓"; + margin-inline-start: -14px; + width: 14px; +} diff --git a/theme/favicon.png b/theme/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..5b874f57f77fb8f1cbdb834ddb27a47aef9578e5 GIT binary patch literal 890 zcmV-=1BLvFP)%@|DIiM`vO0=H2#L9t>L`JNyH5c51j~JbgNF8whL&q1A!X8%BAcX%1Oi9V1qkfj z>}6i;nX!#GX`x6cD43t6M=t!xfB*ls0a{vGT3Y_^I4|4q$>hTwpKfe-RF6l4ccVae zy;JoheN_QuRo?1c+x8v2Pe~9ltaw_Y6(dK)#d@BJI%OD#rPc=>*Ki?zy%CQ$ zd6uKQXL$yOg?xd$=!1(NUtOG>SZNKMTW_MrWi>O3Bw&TUe zJoExU$oV`gTnJbf9{nzX7}w^fK?AeVc((d4e6@XqVW=tz zqAgLCcA*KM-vMIq6rkQ!hHfeAqtpm0En|Z0Sv5+FGK0q_PmA42H1NgF%noyL@RqDP;=EH1nU*9};3^qsGR5nY|y^ zraEw);1`PEpvwlZvajD|dxer#B&0fz65#s2Pj>@_xBz`E=$;0TYq7al=jYHaNg-gq0sdNkeM__hrU6) z=byiEXOVn4bYBmZuC@sx*O`O3uEa;fup1H&3y{@qgLQZ&^0|%ft7Vl + + diff --git a/theme/index.hbs b/theme/index.hbs new file mode 100644 index 0000000..7ee5b49 --- /dev/null +++ b/theme/index.hbs @@ -0,0 +1,349 @@ + + + + + + {{ title }} + {{#if is_print }} + + {{/if}} + {{#if base_url}} + + {{/if}} + + + + {{> head}} + + + + + + {{#if favicon_svg}} + + {{/if}} + {{#if favicon_png}} + + {{/if}} + + + + {{#if print_enable}} + + {{/if}} + + + + {{#if copy_fonts}} + + {{/if}} + + + + + + + + {{#each additional_css}} + + {{/each}} + + {{#if mathjax_support}} + + + {{/if}} + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ {{> header}} + + + + {{#if search_enabled}} + + {{/if}} + + + + +
+
+ {{{ content }}} +
+ + +
+
+ + + +
+ + {{#if live_reload_endpoint}} + + + {{/if}} + + {{#if google_analytics}} + + + {{/if}} + + {{#if playground_line_numbers}} + + {{/if}} + + {{#if playground_copyable}} + + {{/if}} + + {{#if playground_js}} + + + + + + {{/if}} + + {{#if search_js}} + + + + {{/if}} + + + + + + + {{#each additional_js}} + + {{/each}} + + {{#if is_print}} + {{#if mathjax_support}} + + {{else}} + + {{/if}} + {{/if}} + +
+ +