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 0000000..423ca3f Binary files /dev/null and b/src/selium.png differ diff --git a/theme/css/chrome.css b/theme/css/chrome.css new file mode 100644 index 0000000..55cb62e --- /dev/null +++ b/theme/css/chrome.css @@ -0,0 +1,649 @@ +/* CSS for UI elements (a.k.a. chrome) */ + +@import "variables.css"; + +html { + scrollbar-color: var(--scrollbar) var(--bg); +} +#searchresults a, +.content a:link, +a:visited, +a > .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 0000000..5b874f5 Binary files /dev/null and b/theme/favicon.png differ diff --git a/theme/head.hbs b/theme/head.hbs new file mode 100644 index 0000000..bc30e59 --- /dev/null +++ b/theme/head.hbs @@ -0,0 +1,9 @@ + + + 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}} + +
+ +