├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ ├── publish-crates.yml │ ├── release.yml │ └── web.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── SECURITY.md ├── assets └── favicon.ico ├── build.rs ├── docs ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── assets.md │ ├── building.md │ ├── cli.md │ ├── cli │ ├── build.md │ ├── dev.md │ ├── generate.md │ └── serve.md │ ├── configuration.md │ ├── configuration │ ├── additional-pages.md │ ├── analytics.md │ ├── artifacts.md │ ├── changelog.md │ ├── funding.md │ ├── mdbook.md │ ├── reference.md │ ├── social.md │ ├── theme.md │ ├── theme │ │ └── previews.md │ └── workspaces.md │ ├── contributing.md │ ├── hosting.md │ ├── images │ ├── artifacts-pkgman.png │ ├── quickstart-1.png │ ├── quickstart-2.png │ └── themes │ │ ├── axodark.png │ │ ├── axolight.png │ │ ├── cupcake.png │ │ ├── dark.png │ │ ├── hacker.png │ │ ├── light.jpg │ │ └── light.png │ ├── install.md │ ├── introduction.md │ ├── quickstart.md │ └── tips.md ├── flake.lock ├── flake.nix ├── generate-css ├── Cargo.toml └── src │ ├── errors.rs │ └── lib.rs ├── oranda-css ├── css │ ├── base.css │ ├── buttons.css │ ├── components.css │ ├── main.css │ ├── pages │ │ ├── artifacts.css │ │ ├── changelog.css │ │ └── workspace_index.css │ ├── themes │ │ ├── axo.css │ │ ├── cupcake.css │ │ └── hacker.css │ ├── utilities.css │ └── variables.css ├── mdbook-theme │ ├── book.js │ ├── css │ │ ├── chrome.css │ │ ├── general.css │ │ └── variables.css │ ├── fonts │ │ └── fonts.css │ ├── highlight-js-themes │ │ └── base16-material.css │ ├── index.hbs │ └── oranda-themes │ │ ├── axo.css │ │ ├── cupcake.css │ │ ├── default.css │ │ └── hacker.css ├── package-lock.json ├── package.json └── tailwind.config.js ├── oranda.json ├── rust-toolchain.toml ├── src ├── commands │ ├── build.rs │ ├── dev.rs │ ├── generate.rs │ ├── mod.rs │ ├── print.rs │ └── serve.rs ├── config │ ├── axoproject.rs │ ├── builds.rs │ ├── components │ │ ├── artifacts │ │ │ ├── mod.rs │ │ │ └── package_managers.rs │ │ ├── changelog.rs │ │ ├── funding.rs │ │ ├── mdbooks.rs │ │ └── mod.rs │ ├── marketing │ │ ├── analytics.rs │ │ ├── mod.rs │ │ └── social.rs │ ├── mod.rs │ ├── oranda_config.rs │ ├── project.rs │ ├── style.rs │ └── workspace.rs ├── data │ ├── artifacts │ │ ├── inference.rs │ │ └── mod.rs │ ├── axodotdev │ │ └── mod.rs │ ├── cargo_dist.rs │ ├── funding.rs │ ├── github │ │ └── mod.rs │ ├── mod.rs │ ├── release.rs │ └── workspaces.rs ├── errors.rs ├── formatter.rs ├── generate.rs ├── lib.rs ├── main.rs ├── paths.rs └── site │ ├── artifacts │ └── mod.rs │ ├── changelog.rs │ ├── funding │ └── mod.rs │ ├── layout │ ├── css.rs │ ├── header.rs │ ├── javascript │ │ ├── analytics.rs │ │ ├── artifacts.js │ │ └── mod.rs │ └── mod.rs │ ├── link.rs │ ├── markdown │ ├── mod.rs │ └── syntax_highlight │ │ ├── MaterialTheme.tmTheme │ │ ├── TOML.sublime-syntax │ │ ├── TypeScript.sublime-syntax │ │ ├── mod.rs │ │ ├── syntax_themes.rs │ │ └── syntax_themes.themedump │ ├── mdbook.rs │ ├── mod.rs │ ├── oranda_theme.rs │ ├── page │ ├── mod.rs │ └── source.rs │ ├── rss.rs │ ├── templates.rs │ └── workspace_index.rs ├── templates ├── generate │ └── web.yml.j2 └── site │ ├── artifacts.html.j2 │ ├── changelog_index.html.j2 │ ├── changelog_single.html.j2 │ ├── funding.html.j2 │ ├── icons │ ├── copy.html.j2 │ ├── date.html.j2 │ ├── github.html.j2 │ ├── kofi.html.j2 │ ├── liberapay.html.j2 │ ├── opencollective.html.j2 │ ├── patreon.html.j2 │ ├── rss.html.j2 │ ├── tag.html.j2 │ ├── tidelift.html.j2 │ └── web.html.j2 │ ├── includes │ ├── changelog_release.html.j2 │ ├── install_widget.html.j2 │ ├── installer_run.html.j2 │ └── nav.html.j2 │ ├── index.html.j2 │ ├── layout.html.j2 │ ├── markdown_page.html.j2 │ └── workspace_index │ ├── index.html.j2 │ └── layout.html.j2 └── tests ├── autodetect ├── fixtures │ ├── mod.rs │ └── project_config.rs └── mod.rs ├── integration ├── fixtures │ ├── mod.rs │ └── oranda_config.rs └── mod.rs ├── integration_gallery ├── command.rs ├── errors.rs ├── mod.rs ├── oranda_impl.rs └── repo.rs ├── mod.rs ├── snapshots ├── gal_akaikatana-public.snap ├── gal_axolotlsay-public.snap ├── gal_oranda-public.snap ├── gal_oranda_empty-public.snap └── gal_oranda_inference-public.snap └── utils ├── mod.rs ├── snapshots.rs └── tokio_utils.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_size = 2 7 | indent_style = space 8 | 9 | [*.rs] 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | - cron: '11 7 * * 1,4' 10 | 11 | env: 12 | RUSTFLAGS: -Dwarnings 13 | 14 | jobs: 15 | fmt: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: dtolnay/rust-toolchain@stable 20 | with: 21 | components: rustfmt 22 | - name: Run cargo fmt 23 | run: | 24 | cargo fmt --all -- --check 25 | clippy: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: dtolnay/rust-toolchain@stable 30 | with: 31 | components: clippy 32 | - name: Run cargo clippy 33 | run: | 34 | cargo clippy --workspace --tests --examples 35 | docs: 36 | runs-on: ubuntu-latest 37 | env: 38 | RUSTDOCFLAGS: -Dwarnings 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: dtolnay/rust-toolchain@stable 42 | - name: Run cargo doc 43 | run: | 44 | cargo doc --workspace --no-deps 45 | test: 46 | runs-on: ${{ matrix.os }} 47 | strategy: 48 | matrix: 49 | os: [ubuntu-latest, windows-latest, macOS-latest] 50 | steps: 51 | - uses: actions/checkout@v3 52 | - uses: dtolnay/rust-toolchain@stable 53 | - uses: swatinem/rust-cache@v2 54 | - name: Run cargo test 55 | run: | 56 | cargo test --workspace 57 | -------------------------------------------------------------------------------- /.github/workflows/publish-crates.yml: -------------------------------------------------------------------------------- 1 | # Publishes a release to crates.io 2 | # 3 | # To trigger this: 4 | # 5 | # - go to Actions > PublishRelease 6 | # - click the Run Workflow dropdown in the top-right 7 | # - enter the tag of the release as “Release Tag” (e.g. v0.3.18) 8 | name: PublishCrates 9 | 10 | on: 11 | workflow_call: 12 | inputs: 13 | plan: 14 | required: true 15 | type: string 16 | 17 | jobs: 18 | # publish the current repo state to crates.io 19 | cargo-publish: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout sources 23 | uses: actions/checkout@v4 24 | with: 25 | submodules: recursive 26 | - run: cargo publish -p oranda-generate-css --token ${CRATES_TOKEN} 27 | env: 28 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 29 | - run: cargo publish -p oranda --token ${CRATES_TOKEN} 30 | env: 31 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/web.yml: -------------------------------------------------------------------------------- 1 | # Workflow to build your docs with oranda (and mdbook) 2 | # and deploy them to Github Pages 3 | name: Web 4 | 5 | # We're going to push to the gh-pages branch, so we need that permission 6 | permissions: 7 | contents: write 8 | 9 | # What situations do we want to build docs in? 10 | # All of these work independently and can be removed / commented out 11 | # if you don't want oranda/mdbook running in that situation 12 | on: 13 | # Check that a PR didn't break docs! 14 | # 15 | # Note that the "Deploy to Github Pages" step won't run in this mode, 16 | # so this won't have any side-effects. But it will tell you if a PR 17 | # completely broke oranda/mdbook. Sadly we don't provide previews (yet)! 18 | pull_request: 19 | 20 | # Whenever something gets pushed to main, update the docs! 21 | # This is great for getting docs changes live without cutting a full release. 22 | # 23 | # Note that if you're using cargo-dist, this will "race" the Release workflow 24 | # that actually builds the Github Release that oranda tries to read (and 25 | # this will almost certainly complete first). As a result you will publish 26 | # docs for the latest commit but the oranda landing page won't know about 27 | # the latest release. The workflow_run trigger below will properly wait for 28 | # cargo-dist, and so this half-published state will only last for ~10 minutes. 29 | # 30 | # If you only want docs to update with releases, disable this, or change it to 31 | # a "release" branch. You can, of course, also manually trigger a workflow run 32 | # when you want the docs to update. 33 | push: 34 | branches: 35 | - main 36 | 37 | # Whenever a workflow called "Release" completes, update the docs! 38 | # 39 | # If you're using cargo-dist, this is recommended, as it will ensure that 40 | # oranda always sees the latest release right when it's available. Note 41 | # however that Github's UI is wonky when you use workflow_run, and won't 42 | # show this workflow as part of any commit. You have to go to the "actions" 43 | # tab for your repo to see this one running (the gh-pages deploy will also 44 | # only show up there). 45 | workflow_run: 46 | workflows: [ "Release" ] 47 | types: 48 | - completed 49 | 50 | # Alright, let's do it! 51 | jobs: 52 | web: 53 | name: Build and deploy site and docs 54 | runs-on: ubuntu-latest 55 | steps: 56 | # Setup 57 | - uses: actions/checkout@v3 58 | with: 59 | fetch-depth: 0 60 | - uses: dtolnay/rust-toolchain@stable 61 | - uses: swatinem/rust-cache@v2 62 | 63 | # If you use any mdbook plugins, here's the place to install them! 64 | 65 | # Install and run oranda (and mdbook) 66 | # This will write all output to ./public/ (including copying mdbook's output to there) 67 | - name: Install and run oranda 68 | run: | 69 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/download/v0.4.0/oranda-installer.sh | sh 70 | oranda build 71 | 72 | - name: Prepare HTML for link checking 73 | # untitaker/hyperlink supports no site prefixes, move entire site into 74 | # a subfolder 75 | run: mkdir /tmp/public/ && cp -R public /tmp/public/oranda 76 | 77 | - name: Check HTML for broken internal links 78 | uses: untitaker/hyperlink@0.1.29 79 | with: 80 | args: /tmp/public/ --sources docs/ 81 | 82 | # Deploy to our gh-pages branch (creating it if it doesn't exist) 83 | # the "public" dir that oranda made above will become the root dir 84 | # of this branch. 85 | # 86 | # Note that once the gh-pages branch exists, you must 87 | # go into repo's settings > pages and set "deploy from branch: gh-pages" 88 | # the other defaults work fine. 89 | - name: Deploy to Github Pages 90 | uses: JamesIves/github-pages-deploy-action@v4.4.1 91 | # ONLY if we're on main (so no PRs or feature branches allowed!) 92 | if: ${{ github.ref == 'refs/heads/main' }} 93 | with: 94 | branch: gh-pages 95 | # Gotta tell the action where to find oranda's output 96 | folder: public 97 | token: ${{ secrets.GITHUB_TOKEN }} 98 | single-commit: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | public/ 3 | oranda-debug.log 4 | # banish macos to the depths of hell 5 | .DS_Store 6 | .idea/ 7 | 8 | # oranda-css 9 | oranda-css/dist 10 | oranda-css/.yarn 11 | # weirdo yarn feature that dumps files everywhere 12 | oranda-css/.pnp* 13 | node_modules 14 | 15 | # nix 16 | result/ 17 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Axo Developer Co. 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # oranda 4 | 5 |
6 | 7 | > 🎁 generate beautiful landing pages for your projects 8 | 9 | [![crates.io](https://img.shields.io/crates/v/oranda.svg)](https://crates.io/crates/oranda) 10 | [![CI](https://github.com/axodotdev/oranda/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/axodotdev/oranda/actions/workflows/ci.yml) 11 | [![release](https://github.com/axodotdev/oranda/actions/workflows/release.yml/badge.svg)](https://github.com/axodotdev/oranda/actions/workflows/release.yml) 12 | [![web](https://github.com/axodotdev/oranda/actions/workflows/web.yml/badge.svg?branch=main)](https://github.com/axodotdev/oranda/actions/workflows/web.yml) 13 | 14 | 15 | `oranda` is an opinionated static-site generator that is designed for developers 16 | who are publishing projects and would like a website but don't want to build 17 | one from scratch. 18 | 19 |
20 | 21 | `oranda` uses `oranda` so you can checkout a live example [here][website]! 22 | 23 | ## Installation 24 | 25 | To install `oranda`, please visit the [`oranda` website][website]- which is generated by 26 | `oranda`! 27 | 28 | [website]: https://axodotdev.github.io/oranda 29 | 30 |
31 | 32 | ## Quickstart 33 | 34 | ```sh 35 | # build your site 36 | > oranda build 37 | 38 | # build your site and start a server that rebuilds on file changes 39 | > oranda dev 40 | ``` 41 | 42 | Here's an animated demo: 43 | 44 | ![oranda demo gif](https://github.com/axodotdev/oranda/assets/6445316/439082a6-2caa-477e-93cc-1ff985d9bb21) 45 | 46 | ## Configuration 47 | 48 | First of all: `oranda` is designed to work without a configuration file. For a lot of projects, 49 | you can likely just run `oranda build` and get a site that contains a couple of things that 50 | `oranda` was automatically able to glean about your project. That being said, it also supports 51 | a configuration file that allows you to tweak many things about oranda's behaviour. 52 | 53 | If you'd like to configure `oranda`, place an `oranda.json` file in the root of 54 | your project and fill it with the configuration you'd like. Check out the [docs] 55 | to learn more about your configuration options! 56 | 57 | [docs]: https://opensource.axo.dev/oranda/book/configuration.html 58 | 59 | ## Installers: integrating with `cargo-dist` 60 | 61 | `oranda` has first-class integration with [`cargo-dist`], a tool that builds 62 | distributable artifacts for your Rust applications. If you have `cargo-dist` 63 | configured in your project correctly, `oranda` will be able to automatically 64 | tell. Benefits of integrating with `cargo-dist` include: 65 | 66 | - Installer scripts: `cargo-dist` can generate one-line installer scripts, which 67 | `oranda` will display in your generated page 68 | - Guaranteed platform support: `oranda` tries to support as many platforms as it can, 69 | but if you build something with `cargo-dist`, we guarantee it'll show up correctly 70 | 71 | [`cargo-dist`]: https://github.com/axodotdev/cargo-dist 72 | 73 | ## Contributing 74 | 75 | Feel free to open a new issue or pull request if you notice something off or have a new feature 76 | request! We sometimes tag issues with [good first issue] for issues that we think would make 77 | a good learning experience for new contributors. 78 | 79 | For local development on oranda, we also have a [special docs page][contributing-docs] with some tips. 80 | 81 | [good first issue]: https://github.com/axodotdev/oranda/labels/good%20first%20issue 82 | [contributing-docs]: https://opensource.axo.dev/oranda/book/contributing.html 83 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Axo Developer Co. takes the security of our software products and services seriously. If you believe you have found a security vulnerability in this open source repository, please report the issue to us directly using GitHub private vulnerability reporting or email ashley@axo.dev. If you aren't sure you have found a security vulnerability but have a suspicion or concern, feel free to message anyways; we prefer over-communication :) 2 | 3 | Please do not report security vulnerabilities publicly, such as via GitHub issues, Twitter, or other social media. 4 | 5 | Thanks for helping make software safe for everyone! 6 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/assets/favicon.ico -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use oranda_generate_css::default_css_output_dir; 2 | use std::path::PathBuf; 3 | 4 | fn main() { 5 | // Step 1: Has the user set ORANDA_USE_TAILWIND_BINARY? If so, we set a cfg attribute to build 6 | // the CSS on-demand in the main binary. This is intended to be used by contributors for a faster local 7 | // development cycle. 8 | // Alternatively, a packager can set this for a release build to prebuild the CSS using the 9 | // Tailwind binary. 10 | if std::env::var("ORANDA_USE_TAILWIND_BINARY").is_ok() || cfg!(feature = "build-with-tailwind") 11 | { 12 | if cfg!(debug_assertions) { 13 | println!("cargo:rustc-cfg=css=\"tailwind\""); 14 | } else { 15 | let runtime = tokio::runtime::Builder::new_multi_thread() 16 | .worker_threads(1) 17 | .max_blocking_threads(128) 18 | .enable_all() 19 | .build() 20 | .expect("Initializing Tokio runtime failed"); 21 | let _guard = runtime.enter(); 22 | oranda_generate_css::build_css(&default_css_output_dir()).unwrap(); 23 | println!("cargo:rustc-cfg=css=\"file\""); 24 | } 25 | } else { 26 | // Step 2: Does a CSS file exist at oranda-css/dist/oranda.css? If so, assume the user 27 | // has precompiled oranda CSS, which will cause the main oranda binary to include this 28 | // file. 29 | let path = PathBuf::from("./oranda-css/dist/oranda.css"); 30 | if path.exists() { 31 | println!("cargo:rustc-cfg=css=\"file\""); 32 | } else { 33 | // Step 3: The user doesn't have the CSS locally and doesn't want to compile it from 34 | // scratch. In this case, we let the main binary know that it should always pull CSS 35 | // from GitHub releases. This behaviour is intended as a fallback for `cargo install` 36 | // builds. 37 | println!("cargo:rustc-cfg=css=\"fetch\""); 38 | println!("cargo:warning=This build of oranda will pull CSS directly from GitHub releases! This is probably not what you want."); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["axodotdev"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | 7 | [output.markdown] 8 | [output.html] 9 | edit-url-template = "https://github.com/axodotdev/oranda/edit/main/docs/{path}" 10 | search.use-boolean-and = true 11 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./introduction.md) 4 | - [Install](./install.md) 5 | - [Quickstart](./quickstart.md) 6 | - [Hosting](./hosting.md) 7 | - [Assets](./assets.md) 8 | - [Command Line](./cli.md) 9 | - [build](./cli/build.md) 10 | - [serve](./cli/serve.md) 11 | - [dev](./cli/dev.md) 12 | - [generate](./cli/generate.md) 13 | - [Tips and Tricks](./tips.md) 14 | - [Configuration](./configuration.md) 15 | - [Reference](./configuration/reference.md) 16 | - [Artifacts & `cargo-dist`](./configuration/artifacts.md) 17 | - [Additional Pages](./configuration/additional-pages.md) 18 | - [Analytics](./configuration/analytics.md) 19 | - [Changelogs](./configuration/changelog.md) 20 | - [`mdbook` support](./configuration/mdbook.md) 21 | - [Social](./configuration/social.md) 22 | - [Theming](./configuration/theme.md) 23 | - [Theme Previews](./configuration/theme/previews.md) 24 | - [Funding](./configuration/funding.md) 25 | - [Workspaces](./configuration/workspaces.md) 26 | - [Contributing](./contributing.md) 27 | - [Building oranda](./building.md) 28 | -------------------------------------------------------------------------------- /docs/src/assets.md: -------------------------------------------------------------------------------- 1 | # Adding static assets 2 | 3 | If you reference static assets in your Markdown, you'll need to place them all inside a directory at the same level as 4 | your project manifest file called `static`. This is because Oranda currently doesn't know about each indidivual asset, 5 | and instead just copies the folder where they're contained. 6 | 7 | In your Markdown, you'll need to refer to the assets in this directory. For example: 8 | 9 | ```md 10 | ![An image from my amazing project](./static/project.png) 11 | ``` 12 | 13 | If you want to use a custom-named directory you can configure this in your `oranda.json`, like so: 14 | 15 | ```json 16 | { 17 | "build": { 18 | "static_dir": "assets" 19 | } 20 | } 21 | ``` 22 | 23 | In this case the `assets` directory will be used instead of the default `static` directory. 24 | -------------------------------------------------------------------------------- /docs/src/building.md: -------------------------------------------------------------------------------- 1 | # Building oranda 2 | 3 | In case you're interested in building oranda from scratch, be it for packaging for a software distribution or 4 | something different, here's some information on things to keep in mind! 5 | 6 | ## Basic setup 7 | 8 | To build oranda from source, you'll need the following: 9 | 10 | - Stable Rust toolchain 11 | - _Optionally_, if you want to pre-compile CSS, Node & npm 12 | - `cargo-dist`, if you want to opt into the same release flags we use in our CI 13 | 14 | oranda can be built from source by running `cargo build --release` (not including the CSS - read below for more). 15 | Since we use `cargo-dist` to build our binaries for the official distribution of oranda, you may want to 16 | run `cargo dist build` instead. This 17 | ensures that you generate the same builds that we do. 18 | 19 | ### The trouble with CSS 20 | 21 | oranda includes some CSS to style pages - we call this oranda-css. This CSS uses [TailwindCSS](https://tailwindcss.com), 22 | and therefore needs to be compiled before it can be included. To figure out _how_ to build and/or include this CSS, 23 | we use a `build.rs` file. This file sets configuration variables for one of these cases: 24 | 25 | > Added in version 0.4.0. 26 | 27 | - You have the environment variable `ORANDA_USE_TAILWIND_BINARY` set. This causes oranda to attempt to download a 28 | `tailwindcss` binary, and build using that. 29 | - If you run this in a development build, the CSS will get built at runtime. 30 | - If you run it in a release build, the CSS will be built as part of `build.rs` and embedded into the resulting 31 | binary. 32 | - If a file exists at `oranda-css/dist/oranda.css`, oranda will inline that file and use it as its _current_ version, 33 | meaning it'll insert it into sites unless the user overrides it with a different version. This means you can prebuild 34 | the CSS using npm, and then run `cargo build` to let it be picked up. 35 | - If neither of these conditions are true, a Cargo build will produce a binary that'll always fetch the CSS from our 36 | GitHub releases. Stricly seen, this is a **worse version of oranda** (because it has to do extra CSS requests), so 37 | we suggest not distributing a binary that was built this way. You can check if a binary was built this way by looking 38 | out for the following log 39 | line: `warning: This build of oranda will pull CSS directly from GitHub releases! This is probably not what you want.` 40 | 41 | > For `cargo install` users: Your `oranda` binary is of the third type in the list above. This is unfortunately a 42 | > shortcoming of Cargo's build pipeline, but if you're fine with using a slightly slower version of 43 | > oranda, `cargo install` 44 | > works fine. If you want a regular binary, check the [install page](./install.md)! 45 | 46 | If you're distributing binaries anywhere, you can either use the Node toolchain, or the Tailwind binary 47 | using `ORANDA_USE_TAILWIND_BINARY`, depending on which is easier/more conformant in your build environment. 48 | 49 | ```sh 50 | # either: 51 | ORANDA_USE_TAILWIND_BINARY=true cargo dist build 52 | 53 | # or: 54 | cd oranda-css 55 | npm run build 56 | cd .. 57 | cargo dist build 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /docs/src/cli.md: -------------------------------------------------------------------------------- 1 | # Command Line 2 | 3 | Oranda currently has four subcommands that work in similar, but nuanced ways. 4 | 5 | - [`build`](./cli/build.md) 6 | - [`serve`](./cli/serve.md) 7 | - [`dev`](./cli/dev.md) 8 | - [`generate`](./cli/generate.md) 9 | 10 | Oranda supports some common options on each command: 11 | 12 | - `--verbose`. This controls the verbosity level for logs. 13 | - `--output-format`. If you want JSON for processing it with a machine, this is where you'd toggle it. 14 | -------------------------------------------------------------------------------- /docs/src/cli/build.md: -------------------------------------------------------------------------------- 1 | # `oranda build` 2 | 3 | This command builds your oranda site. You can pass the `--json-only` flag in order for oranda to _only_ build an 4 | `artifacts.json` file that can be read by other tools (or websites) for integration purposes. You can also specify 5 | `--config-path` if your configuration file is not `./oranda.json`, but oranda will still look for an 6 | `oranda-workspace.json` in the current directory. 7 | -------------------------------------------------------------------------------- /docs/src/cli/dev.md: -------------------------------------------------------------------------------- 1 | # `oranda dev` 2 | 3 | This command basically combined `oranda build` and `oranda serve`, with the added benefit of watching for changes 4 | and recompiling automatically. When you launch, what happens is this: 5 | 6 | 1. Oranda builds your site (unless you told it not to) 7 | 2. Oranda launches a server similar to `oranda serve` 8 | 3. Oranda starts watching its relevant files for changes, and will rerun the build process when something changes 9 | 10 | Oranda's build can have a lot of side-effects (reading/writing files, but also talking to the GitHub API), and as 11 | such, we have to take care to only run the build process when _relevant_ files change. These files are: 12 | 13 | - Your project manifest files (`Cargo.toml`, `package.json`) 14 | - Your oranda configuration file 15 | - Any mdbook source files you may have 16 | - Your readme, and additional files specified in the configuration 17 | - Files immediately relevant to certain components oranda renders (funding, for example) 18 | - Any other paths you give it using `--include-paths` 19 | 20 | This command also supports several options: 21 | 22 | - `--port` to set a custom port for the file server 23 | - `--config-path` to specify a custom path for your oranda config (but oranda will still look for an `oranda-workspace.json`) in your current directory). 24 | - `--no-first-build` to skip the first step mentioned above where oranda builds your site before starting the watch process 25 | - `-i`, `--include-paths` to specify custom paths for oranda to watch 26 | -------------------------------------------------------------------------------- /docs/src/cli/generate.md: -------------------------------------------------------------------------------- 1 | # `oranda generate` 2 | 3 | > Added in version 0.4.0. 4 | 5 | This command generates files useful for working with oranda. Currently, it only supports one subcommand. 6 | 7 | ## `oranda generate ci` 8 | 9 | Generates a CI file that deploys your site. Supports the following options: 10 | 11 | - `-o, --output-path`: Specify a path for the file to be written to. Default: `.github/workflows/web.yml` 12 | - `-s, --site-path`: Specify a path to your oranda site, in case it's in a subdirectory to your repository 13 | - `--ci`: Specify which CI platform to use. Currently only accepts and defaults to `github`, which deploys to GitHub 14 | Pages using GitHub Actions. 15 | 16 | You can rerun this command to update the CI file based on what we currently recommend as the best workflow, but also 17 | to, for example, update the oranda version that the CI uses (which will always be the oranda version you run 18 | `generate` with). 19 | -------------------------------------------------------------------------------- /docs/src/cli/serve.md: -------------------------------------------------------------------------------- 1 | # `oranda serve` 2 | 3 | This command launches a small [`axum`][axum]-powered server that serves your generated oranda site. 4 | 5 | Importantly, this does **not** build your site for you. If it can't find a build in the `public/` directory, 6 | it will error and exit. You can set the port for the server to be launched using the `--port` option. 7 | 8 | [axum]: https://cra.tw/axum 9 | -------------------------------------------------------------------------------- /docs/src/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | 4 | **`oranda` is designed to work with no configuration**- for projects with a 5 | `package.json` or `Cargo.toml`, `oranda` will grab the project metadata it needs 6 | from your project manifest file. It can also infer a lot of the things it wants to 7 | render from your already existing environment. 8 | 9 | If you project has both a `Cargo.toml` and a `package.json` we recommend defining 10 | project metadata fields like `name` in your `oranda.json`. 11 | 12 | ## Manifest file: `oranda.json` 13 | 14 | If you'd like to customize your project you can do so in a `oranda.json` file. 15 | 16 | For example: 17 | 18 | ```json 19 | { 20 | "build": { 21 | "path_prefix": "oranda" 22 | }, 23 | "styles": { 24 | "theme": "axodark", 25 | "favicon": "https://www.axo.dev/favicon.ico" 26 | }, 27 | "marketing": { 28 | "social": { 29 | "image": "https://www.axo.dev/meta_small.jpeg", 30 | "image_alt": "axo", 31 | "twitter_account": "@axodotdev" 32 | }, 33 | "analytics": { 34 | "plausible": { 35 | "domain": "opensource.axo.dev" 36 | } 37 | } 38 | }, 39 | "components": { 40 | "changelog": true, 41 | "artifacts": { 42 | "package_managers": { 43 | "preferred": { 44 | "npm": "npm install @axodotdev/oranda --save-dev", 45 | "cargo": "cargo install oranda --locked --profile=dist" 46 | }, 47 | "additional": { 48 | "npx": "npx @axodotdev/oranda", 49 | "binstall": "cargo binstall oranda", 50 | "nix-env": "nix-env -i oranda", 51 | "nix flake": "nix profile install github:axodotdev/oranda" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | > **NOTE:** All paths in `oranda.json` are relative to the `oranda.json` file. 60 | 61 | **See the [configuration reference](./configuration/reference.md) for a detailed explanations of all options!** 62 | 63 | ## Workspace manifest file: `oranda-workspace.json` 64 | 65 | > Added in version 0.3.0. 66 | 67 | oranda supports building multiple sites at once (referred to as building in a "workspace"). To control this behavior, 68 | you can create a `oranda-workspace.json` file inside your workspace root. Running an oranda command will pick up this 69 | file, and build the workspace members accordingly. 70 | 71 | The workspace file supports all other oranda config keys, which will be passed down to each workspace members. 72 | 73 | [Read more about workspaces](configuration/workspaces.md) or [see the workspace reference](./configuration/reference.md#workspace) 74 | 75 | ## Configuration before 0.1.0 76 | 77 | Before version 0.1.0 (the last stable version was/is 0.0.3, the last prerelease was/is 0.1.0-prerelease7), the 78 | configuration format looked like this: 79 | 80 | ```json 81 | { 82 | "name": "oranda", 83 | "description": "generate static sites for your dev tools", 84 | "dist_dir": "oranda_out", 85 | "homepage": "https://oranda.axo.dev", 86 | "static_dir": "static", 87 | "no_header": false, 88 | "readme_path": "dev/README.md", 89 | "repository": "https://github.com/axodotdev/oranda", 90 | "additional_pages": { 91 | "Another page": "dev/additional.md" 92 | }, 93 | "favicon": "https://www.axo.dev/favicon.ico", 94 | "analytics": { 95 | "plausible": { 96 | "domain": "tools.axo.dev/oranda" 97 | } 98 | }, 99 | "social": { 100 | "image": "https://www.axo.dev/meta_small.jpeg", 101 | "image_alt": "axo", 102 | "twitter_account": "@axodotdev" 103 | }, 104 | "artifacts": { 105 | "cargo_dist": true 106 | }, 107 | "logo": "assets/oranda.png", 108 | "license": "MIT OR Apache-2.0", 109 | "mdbook": false, 110 | "path_prefix": "oranda", 111 | "styles": { 112 | "theme": "axo_dark" 113 | }, 114 | "funding": { 115 | "preferred_funding": "github" 116 | }, 117 | "changelog": true 118 | } 119 | ``` 120 | 121 | -------------------------------------------------------------------------------- /docs/src/configuration/additional-pages.md: -------------------------------------------------------------------------------- 1 | # Additional Pages 2 | 3 | If you have extra Markdown files you'd like to link directly as root pages on your generated website, you can 4 | use the `additional_pages` option to list them. 5 | 6 | The option's format is an object with the human-readable page name as keys, and the path to the file as values. Example: 7 | 8 | ```json 9 | { 10 | "build": { 11 | "additional_pages": { 12 | "Another page": "./AnotherFile.md" 13 | } 14 | } 15 | } 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/src/configuration/analytics.md: -------------------------------------------------------------------------------- 1 | # Analytics 2 | 3 | oranda supports automatically inserting the correct analytics snippet your provider into your generated pages. 4 | 5 | Right now we support the following analytics providers: 6 | 7 | - [Google Analytics](https://analytics.google.com/analytics/web/) 8 | - [Plausible](https://plausible.io/) 9 | - [Fathom](https://usefathom.com/) 10 | - [Unami](https://umami.is/) 11 | 12 | To add any of these, add the required configuration under the `analytics` key: 13 | 14 | ### Google Analytics 15 | 16 | ```json 17 | { 18 | "marketing": { 19 | "analytics": { 20 | "google_analytics": { 21 | "tracking_id": "String" 22 | } 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | ### Plausible 29 | 30 | ```json 31 | { 32 | "marketing": { 33 | "analytics": { 34 | "plausible": { 35 | "domain": "String", 36 | "script_url": "Optional string for self hosted" 37 | } 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | ### Fathom 44 | 45 | ```json 46 | { 47 | "marketing": { 48 | "analytics": { 49 | "fathom": { 50 | "site": "String" 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | ### Unami 58 | 59 | ```json 60 | { 61 | "marketing": { 62 | "analytics": { 63 | "unami": { 64 | "website": "String", 65 | "script_url": "String" 66 | } 67 | } 68 | } 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/src/configuration/artifacts.md: -------------------------------------------------------------------------------- 1 | # Artifacts & `cargo-dist` 2 | 3 | oranda has first-class support for handling releases, including ones generated by [`cargo-dist`]. 4 | It can even detect the user's platform and recommend the best installer/archive to use! 5 | 6 | Artifact settings are managed in the `artifacts` key in your oranda config. This is what an example config looks like: 7 | 8 | ```json 9 | { 10 | "components": { 11 | "artifacts": { 12 | "package_managers": { 13 | "preferred": { 14 | "npm": "npm install @axodotdev/oranda --save-dev", 15 | "crates.io": "cargo install oranda --locked --profile=dist", 16 | }, 17 | "additional": { 18 | "npx": "npx @axodotdev/oranda", 19 | "binstall": "cargo binstall oranda" 20 | } 21 | } 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | ## Enabling cargo-dist 28 | 29 | oranda will automatically attempt to find a `cargo-dist` config in your Cargo.toml. If you want to force disable this, 30 | set `components.artifacts.cargo_dist` to `false`. Once oranda determines that `cargo-dist` support should be enabled, 31 | the following will happen: 32 | 33 | - oranda will attempt to find GitHub releases generated by `cargo-dist` 34 | - A new "Install" page will be generated, containing all artifacts and installers for the latest version 35 | - A section to quickly install the latest release for the user's current platform will be added to the homepage 36 | 37 | ## Enabling arbitrary GitHub release support 38 | 39 | Even if you don't have `cargo-dist` set up, oranda can attempt to glean information about your supported targets and 40 | OSes from your GitHub release artifacts. It will attempt to do this if it can find any releases associated with your 41 | repository. It does this by trying to see if it can recognize any known target triples from your filenames, so for example, 42 | it will recognize `mytool-aarch64-apple-darwin.tar.gz`. If you would like to completely disable this, set 43 | `components.artifacts` to `false` (we may offer a more fine-grained setting for this in the future). 44 | 45 | ## Enabling matching a release to a specific package 46 | 47 | If you have multiple packages being produced by a workspace and need to match a release to a specific package, you can do 48 | so by setting `components.artifacts.match_package_names` to `true`. Upon enabling, while working to determine the latest 49 | release oranda will check if the release tag contains the name of the project being generated. If no match is found 50 | that particular release will be skipped. 51 | 52 | ## Adding package manager installation instructions 53 | 54 | You can add custom installation instructions for package managers or package manager-esque methods using the 55 | `components.artifacts.package_managers` key. These are broken up into two sections: 56 | 57 | - `components.artifacts.package_managers.preferred` - methods that you want to be recommended on the front page install 58 | widget 59 | - `components.artifacts.package_managers.additional` - methods that should only show up on the dedicated "install" page 60 | 61 | All package manager entries are currently treated as "cross-platform", meaning they'll show up in the install widget for 62 | any platform you support. We're aware of this limitation, and will likely expand support for this in the future. 63 | 64 | [`cargo-dist`]: https://opensource.axo.dev/cargo-dist/ 65 | -------------------------------------------------------------------------------- /docs/src/configuration/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelogs 2 | 3 | oranda can generate a separate changelog file from either a local `CHANGELOG.md` file in your repository, or from the body 4 | of GitHub releases. This setting is **enabled** by default, as long as you have a repository set for your project. To disable this 5 | feature, set it to false in the `oranda.json`: 6 | 7 | ```json 8 | { 9 | "components": { 10 | "changelog": false 11 | } 12 | } 13 | ``` 14 | 15 | By default, oranda will also generate a `changelog.rss` file which you can plug into RSS readers or other automation! 16 | 17 | ## Controlling where changelogs are read from 18 | 19 | By default, oranda will try to read changelog contents from a file called `CHANGELOG(.md)`. This file needs to be formatted 20 | in such a way that it can be parsed, meaning you'll have to specify consistent headers in your Markdown file, like this: 21 | 22 | ```markdown 23 | # Changelog 24 | 25 | ## 0.1.1 - 2023-04-05 26 | 27 | - Fixed things 28 | 29 | ## 0.1.0 - 2023-04-02 30 | 31 | ### New features 32 | 33 | - Fancy thingie 34 | - Other cool stuff 35 | 36 | ### Fixes 37 | 38 | - Beep booping is now consistent 39 | ``` 40 | 41 | If you would like oranda to use the bodies of GitHub releases that it finds instead, set the following option: 42 | 43 | ```json 44 | { 45 | "components": { 46 | "changelog": { 47 | "read_changelog_file": false 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | > Even if oranda reads from a local changelog file, it will still try to match those releases to GitHub releases. Make 54 | > sure that both version numbering schemes are the same between your local changelog and GitHub releases. 55 | 56 | For a complete reference of changelog configuration, consult the [reference](./reference.md#componentschangelog) 57 | 58 | ## For workspaces 59 | 60 | If you have a [workspace](./workspaces.md), but you would like to opt-out of changelogs for only some members, you'll need 61 | to add manual overrides in those `oranda.json` files right now. 62 | -------------------------------------------------------------------------------- /docs/src/configuration/funding.md: -------------------------------------------------------------------------------- 1 | # Funding page 2 | 3 | Oranda has the capability of reading information from your GitHub funding file, and 4 | automatically writing a page based on it. Unless you disable it by setting `components.funding` to `false` 5 | in the oranda config file, oranda will search your project for 6 | a `.github/FUNDING.yml` file, and generate a page based off of it. You can read 7 | more about the format of this file on [GitHub's docs][funding-docs]. 8 | 9 | Oranda will display your different sponsor/funding links next to each other, but 10 | if you have a "main" funding option, you can set the following configuration setting: 11 | 12 | ```json 13 | { 14 | "components": { 15 | "funding": { 16 | "preferred_funding": "github" 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | Make sure this key corresponds to one of the possible entries in the `FUNDING.yml` 23 | file. 24 | 25 | If you want to display additional information or context, oranda can also include 26 | the contents of a top-level `funding.md` Markdown file. Its contents will be translated 27 | into HTML and displayed on the Funding page as well. 28 | 29 | Both of the YAML and Markdown file paths can be customized as such: 30 | 31 | ```json 32 | { 33 | "components": { 34 | "funding": { 35 | "md_path": "myfunding.md", 36 | "yml_path": "misc/funding.yml" 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | > oranda's funding parsing and site generation are currently an experiment into how 43 | to better integrate common funding methods into your tools' websites. If you have 44 | any feedback on how we could do things better, let us know on 45 | [Discord][axodiscord] or [GitHub][newissue]! 46 | 47 | [funding-docs]: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository 48 | [axodiscord]: https://discord.com/invite/wVqCRGsb 49 | [newissue]: https://github.com/axodotdev/oranda/issues/new -------------------------------------------------------------------------------- /docs/src/configuration/mdbook.md: -------------------------------------------------------------------------------- 1 | # mdbook support 2 | 3 | oranda can generate [mdbooks][mdbook] for you. If you've already worked with mdbook, it's as simple as pointing oranda 4 | at your book directory using the `mdbook.path` option: 5 | 6 | ```json 7 | { 8 | "components": { 9 | "mdbook": { 10 | "path": "./docs" 11 | } 12 | } 13 | } 14 | ``` 15 | 16 | This will cause oranda to automatically recompile your book for you, which will be served at the `yoursite/book/` URL. 17 | `oranda dev` will also be watching this directory. 18 | 19 | ## mdbook quickstart 20 | 21 | If this is the first time you're working with mdbook, these are the minimal steps you'd need before editing the oranda config. 22 | After you've [installed the mdbook tool][mdbook-install], you can generate a new book scaffold: 23 | 24 | ```sh 25 | mdbook init docs # replace docs with your preferred directory 26 | ``` 27 | 28 | You can either use `oranda dev` or `mdbook serve docs/` to have a preview for your mdbook. 29 | 30 | [mdbook]: https://rust-lang.github.io/mdBook/ 31 | [mdbook-install]: https://rust-lang.github.io/mdBook/guide/installation.html 32 | -------------------------------------------------------------------------------- /docs/src/configuration/social.md: -------------------------------------------------------------------------------- 1 | # Social 2 | 3 | In order to help with SEO, there are a couple of options you can use. 4 | 5 | ```json 6 | { 7 | "marketing": { 8 | "social": { 9 | "image": "used as the share image for social media", 10 | "image_alt": "alt for said image", 11 | "twitter_account": "twitter account for the website" 12 | } 13 | } 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/src/configuration/theme.md: -------------------------------------------------------------------------------- 1 | # Theming 2 | 3 | ## Predefined themes 4 | 5 | Oranda comes with six default themes: 6 | 7 | - Light 8 | - Dark 9 | - Axo Light (`axo_light` or `axolight`) 10 | - Axo Dark (`axo_dark` or `axodark`) 11 | - Hacker 12 | - Cupcake 13 | 14 | You can change the theme by adding the `styles.theme` key to your `oranda.json`: 15 | 16 | ```json 17 | { 18 | "styles": { 19 | "theme": "hacker" 20 | } 21 | } 22 | ``` 23 | 24 | Dark is the default theme. 25 | 26 | ## Customizing Themes 27 | 28 | Themes can be further customized by adding extra CSS. 29 | 30 | Additional CSS can be added using the `styles.additional_css` key. 31 | 32 | ```json 33 | { 34 | "styles": { 35 | "additional_css": ["./local/file.css", "http://www.remote.dev/file.css"] 36 | } 37 | } 38 | ``` 39 | 40 | > Note: Remote files will be copied and the copy served locally, so once a link is updated, the site must be regenerated for changes to take effect. 41 | 42 | ### Adding CSS 43 | 44 | Oranda's CSS makes use of [cascade layers](https://css-tricks.com/css-cascade-layers/) to scope CSS and make it simpler to override styles. To override themed styles, say on a `

` element, place it inside a layer called `overrides`. 45 | 46 | ```css 47 | @layer overrides { 48 | p { 49 | color: aquamarine; 50 | } 51 | } 52 | ``` 53 | 54 | Alternately, CSS that is not defined within a layer has precedence over all layered CSS, so this will also work. 55 | 56 | ```css 57 | p { 58 | color: aquamarine; 59 | } 60 | ``` 61 | 62 | ### Dark vs. Light 63 | 64 | When the `dark` theme is selected, a `dark` class is added to the page, and styles to be applied in dark mode only can include this selector. For instance, 65 | 66 | ```css 67 | .dark p { 68 | color: aquamarine; 69 | } 70 | ``` 71 | 72 | Will create paragraphs colored aquamarine in dark mode only. 73 | 74 | ### Adding Classes 75 | 76 | When there are specific elements you would like to add to your pages, these can be added into Markdown files as raw HTML with class selectors that you can target with your CSS. 77 | 78 | ```html 79 | 80 | 81 | ## A Different Kind of Box 82 | 83 |

84 |

An outlined box

85 |
86 | ``` 87 | 88 | ```css 89 | .my-border-class { 90 | padding: 1rem; 91 | border: 6px dotted seagreen; 92 | } 93 | ``` 94 | 95 | ## Creating a New Theme 96 | 97 | Currently, to create a new theme, you need to follow the directions above in "Customizing Themes" and overwrite the given CSS. We recommend continuing the layer approach and placing overrides in the `overrides` layer and then adding a new named layer for your theme. 98 | 99 | The ability to add a different theme directly will be included in future releases. Following the layers approach will make it simpler to transition your theme. 100 | -------------------------------------------------------------------------------- /docs/src/configuration/theme/previews.md: -------------------------------------------------------------------------------- 1 | # Theme Previews 2 | 3 | Here you can see what themes look like without having to set up oranda yourself. 4 | 5 | These previews are generated by running oranda on itself. 6 | 7 | ## Dark (default) 8 | 9 | ![dark theme preview](../../images/themes/dark.png) 10 | 11 | ## Light 12 | 13 | ![light theme preview](../../images/themes/light.png) 14 | 15 | ## Axo Dark 16 | 17 | ![axo dark theme preview](../../images/themes/axodark.png) 18 | 19 | ## Axo Light 20 | 21 | 22 | ![axo light theme preview](../../images/themes/axolight.png) 23 | 24 | ## Hacker 25 | 26 | ![hacker theme preview](../../images/themes/hacker.png) 27 | 28 | ## Cupcake 29 | 30 | ![cupcake preview](../../images/themes/cupcake.png) 31 | -------------------------------------------------------------------------------- /docs/src/configuration/workspaces.md: -------------------------------------------------------------------------------- 1 | # Workspaces 2 | 3 | oranda supports building multiple sites at once (referred to as building in a "workspace"). To control this behavior, 4 | you can create a `oranda-workspace.json` file inside your workspace root. Running an oranda command will pick up this 5 | file, and build the workspace members accordingly. 6 | 7 | The reason why this is a separate file, and not part of the `oranda.json` file is to avoid confusing between _nonvirtual_ 8 | workspace root members (meaning if a workspace root also contains a site/package of some kind). By putting your workspace 9 | configuration in a separate file, you can still have an oranda site at the same directory level, without any problems. 10 | 11 | > **NOTE**: Workspace functionality will not be enabled if the `oranda-workspace.json` file doesn't exist! 12 | 13 | A workspace configuration file looks something like this: 14 | 15 | ```json 16 | { 17 | "workspace": { 18 | "name": "My Workspace", 19 | "members": [ 20 | { 21 | "slug": "projectone", 22 | "path": "./project-one" 23 | }, 24 | { 25 | "slug": "project_two", 26 | "path": "./project-two" 27 | } 28 | ] 29 | } 30 | } 31 | ``` 32 | 33 | When ran with `oranda build`, this will produce two oranda sites, one at `/projectone`, and one at `/project_two`. oranda 34 | will consider each separate project's `oranda.json` file (should it exist). 35 | 36 | You can additionally pass down keys you'd like to be set for each member project: 37 | 38 | ```json 39 | { 40 | "workspace": { 41 | "name": "My Workspace", 42 | "members": [ 43 | { 44 | "slug": "projectone", 45 | "path": "./project-one" 46 | }, 47 | { 48 | "slug": "project_two", 49 | "path": "./project-two" 50 | } 51 | ] 52 | }, 53 | "styles": { 54 | "theme": "hacker" 55 | } 56 | } 57 | ``` 58 | 59 | Individual workspace member configs will still override what's set here, though. Also, _every_ key will be passed down, 60 | including ones that don't make a lot of sense to be the same in multiple projects (for example [package manager](artifacts.md) 61 | configuration). 62 | 63 | Building a workspace will also generate a nice workspace index page that can be used to provide an overview over the 64 | workspace's members, as well as some quick info and metadata. 65 | -------------------------------------------------------------------------------- /docs/src/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Here's some helpful tips for contributing. 4 | 5 | ## Auto-recompiling based on source changes 6 | 7 | If you're working on oranda and you want to rebuild both oranda itself and your preview site when stuff changes, 8 | this is the command to keep in mind (assuming you have `cargo-watch` installed): 9 | 10 | ```shell 11 | cargo watch -x "run dev" 12 | ``` 13 | 14 | On some platforms, apparently images also get recompiled and picked up by cargo-watch: 15 | 16 | ```shell 17 | cargo watch -x "run dev" --ignore "*.png" --ignore "*.jpg" 18 | ``` 19 | 20 | ## ...plus working on the CSS 21 | 22 | We provide an environment variable (`ORANDA_USE_TAILWIND_BINARY`) that, when set, will cause oranda to 23 | use a prebuilt Tailwind binary to rebuild the CSS on the fly. You can use it like this: 24 | 25 | ```shell 26 | ORANDA_USE_TAILWIND_BINARY=true cargo run dev 27 | # for fish shell: 28 | env ORANDA_USE_TAILWIND_BINARY=true cargo run dev 29 | ``` 30 | 31 | (if you don't use this flag, oranda will most likely revert to fetching CSS from the current GitHub release. 32 | Read [this section](./building.md#the-trouble-with-css) in the build documentation for more in-depth info) 33 | 34 | `oranda dev` doesn't automatically reload on CSS changes (because it's meant to be used by users), 35 | but you can include the CSS directory manually like such: 36 | 37 | ```shell 38 | ORANDA_USE_TAILWIND_BINARY=true cargo run dev -i oranda-css/css/ 39 | ``` 40 | 41 | ## Updating syntax highlighting languages 42 | 43 | We use [syntect] to support syntax highlighting in Markdown code blocks. If you want to add support for a new language 44 | that's not included in syntect's default set of languages or the ones oranda provides, you'll need to extend the 45 | `oranda::site::markdown::syntax_highlight::dump_syntax_themes` function to load your new `.sublime-syntax` file from 46 | disk 47 | and to include it in our syntax set dump. This function, once adjusted, only needs to be ran once manually, by including 48 | it anywhere in the call path of the application (I recommend somewhere close to the top of the build CLI function). 49 | 50 | ### Converting from .tmLanguage 51 | 52 | `syntect` accepts `.sublime-syntax` files, but Sublime Text can also accept `.tmLanguage` (TextMate syntax bundles) 53 | files, 54 | so sometimes we need to convert from one to the other. Thankfully, the Sublime Text editor has a built-in feature for 55 | this. 56 | Here's what you need to do: 57 | 58 | 1. Download and install Sublime Text 59 | 2. In Sublime Text, from the menu, select Tools -> Developer -> New Syntax... 60 | 3. This puts you in your Packages/User folder. Paste your tmLanguage file contents and save as `.tmLanguage`. 61 | 4. Next, you should be able to run Tools -> Developer -> New Syntax from .tmLanguage... 62 | 5. This opens a new tab with the converted output. Save and copy it or paste it into a new file in oranda. Profit! 63 | 64 | [syntect]: https://crates.io/crates/syntect -------------------------------------------------------------------------------- /docs/src/hosting.md: -------------------------------------------------------------------------------- 1 | # Hosting 2 | 3 | ## On GitHub pages 4 | 5 | > Added in version 0.4.0. 6 | 7 | You can use `oranda generate ci --ci=github` to write a CI file for deploying your main branch to GitHub Pages. 8 | 9 | When hosting on GitHub Pages, depending on the name of your repository, your site may get served at different URLs: 10 | 11 | - If your repo name is `.github.io`, your site will be at that URL 12 | - If you repo name is anything else, it'll be at `.github.io/`. 13 | 14 | For the latter case, you'll need to configure oranda to write its links to your `oranda.json` (more on that on chapter 8) with `` as a prefix: 15 | 16 | ```json 17 | { 18 | "build": { 19 | "path_prefix": "reponame" 20 | } 21 | } 22 | ``` 23 | 24 | This will cause, for example, a link to `/changelog/` to be written as `/reponame/changelog/`. 25 | 26 | ## Elsewhere 27 | 28 | oranda is, effectively, a static site generator. It outputs HTML, CSS and JavaScript files. These can all be hosted on a 29 | looooot of different platforms, in fact, too many for us to enumerate here! You can use Vercel, Netlify, any GitHub pages 30 | competitor, or you can plop it on your own server that runs nginx, Apache httpd, Caddy, or anything else! 31 | 32 | You can, in fact, also use the CI generated by `oranda generate ci` linked above and modify it to deploy to different 33 | platforms. If you do, we'd love to hear about it! 34 | 35 | [web.yml]: https://github.com/axodotdev/oranda/blob/main/.github/workflows/web.yml 36 | -------------------------------------------------------------------------------- /docs/src/images/artifacts-pkgman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/artifacts-pkgman.png -------------------------------------------------------------------------------- /docs/src/images/quickstart-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/quickstart-1.png -------------------------------------------------------------------------------- /docs/src/images/quickstart-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/quickstart-2.png -------------------------------------------------------------------------------- /docs/src/images/themes/axodark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/themes/axodark.png -------------------------------------------------------------------------------- /docs/src/images/themes/axolight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/themes/axolight.png -------------------------------------------------------------------------------- /docs/src/images/themes/cupcake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/themes/cupcake.png -------------------------------------------------------------------------------- /docs/src/images/themes/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/themes/dark.png -------------------------------------------------------------------------------- /docs/src/images/themes/hacker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/themes/hacker.png -------------------------------------------------------------------------------- /docs/src/images/themes/light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/themes/light.jpg -------------------------------------------------------------------------------- /docs/src/images/themes/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/docs/src/images/themes/light.png -------------------------------------------------------------------------------- /docs/src/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | There's lots of ways to install oranda! 4 | 5 | ## The Quickest Way 6 | 7 | On the [oranda website][website], there's a one-liner command you can execute for your 8 | OS that'll download and install oranda for you, without any further hassle! 9 | 10 | ## Install Prebuilt Binaries With [cargo-binstall] 11 | 12 | ```sh 13 | cargo binstall oranda 14 | ``` 15 | 16 | ## Build From Source With Cargo 17 | 18 | ```sh 19 | cargo install oranda --locked --profile=dist 20 | ``` 21 | 22 | > `--profile=dist` is the profile we build our shippable binaries with, it's optional. 23 | > 24 | > `--locked` asks Cargo to respect the lockfile, improving build reproducibility at 25 | > the cost of not getting any bugfixes from newer releases of its dependencies. 26 | 27 | 28 | ## Download Prebuilt Binaries From GitHub Releases 29 | 30 | [See the latest release](https://github.com/axodotdev/oranda/releases/latest)! 31 | 32 | ## Install With NPM 33 | 34 | ```sh 35 | npm install @axodotdev/oranda 36 | # alternatively: 37 | npx @axodotdev/oranda build 38 | ``` 39 | 40 | ## Install With Nix 41 | oranda is available in [`nixpkgs`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/oranda/default.nix), and also as a nix flake. This installer is currently experimental, so we don't recommend you use it in production workflows. 42 | 43 | On a system with nix installed, you can run 44 | ```sh 45 | nix-env -i oranda 46 | ``` 47 | 48 | or to install from GitHub using the flake, 49 | ```sh 50 | nix profile install github:axodotdev/oranda 51 | ``` 52 | 53 | [cargo-binstall]:https://github.com/cargo-bins/cargo-binstall 54 | [website]: https://opensource.axo.dev/oranda 55 | -------------------------------------------------------------------------------- /docs/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Oranda is a tool for generating beautiful landing pages for your projects. 4 | 5 | It can: 6 | 7 | - Automagically generate a webpage based off your project readme file 8 | - Include arbitrary Markdown pages 9 | - Generate `mdbook` books for you 10 | - Show downloadable and installable artifacts, and ways to install them, by integrating with [cargo-dist] 11 | - Provide integration with several web analytics providers 12 | 13 | and more! 14 | 15 | This is the oranda documentation, where we explain how to use the tool in detail. Use the 16 | sidebar to the left to navigate between pages. 17 | 18 | > **Caveat emptor!** oranda is still _beta-quality software_! There may be breaking changes at times, especially 19 | in the configuration format (although we're working hard on stabilizing it) 20 | 21 | [cargo-dist]: https://github.com/axodotdev/cargo-dist 22 | -------------------------------------------------------------------------------- /docs/src/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | After you've [installed](./install.md) oranda, it's time to give it a spin. Make sure you can execute the 4 | `oranda` command, its output should look something like this: 5 | 6 | ``` 7 | $ oranda 8 | 🎁 generate beautiful landing pages for your projects 9 | 10 | Usage: oranda [OPTIONS] 11 | 12 | Commands: 13 | build Build an oranda site 14 | dev Start a local development server that recompiles your oranda site if a file changes 15 | serve Start a file server to access your oranda site in a browser 16 | generate Generate infrastructure files for oranda sites 17 | help Print this message or the help of the given subcommand(s) 18 | 19 | Options: 20 | -h, --help 21 | Print help (see a summary with '-h') 22 | 23 | -V, --version 24 | Print version 25 | 26 | GLOBAL OPTIONS: 27 | -v, --verbose 28 | Whether to output more detailed debug information 29 | 30 | --output-format 31 | The format of the output 32 | 33 | [default: human] 34 | 35 | Possible values: 36 | - human: Human-readable output 37 | - json: Machine-readable JSON output 38 | ``` 39 | 40 | Since `oranda` is designed to work without configuration, the quickest start is to just run `oranda dev` in an 41 | existing project! This will spawn a web server that serves your site, plus an extra process that watches for 42 | changes in files relevant to `oranda`'s build process. 43 | 44 | > __NOTE__: Prior to version 0.5.0, oranda expects there to be a README.md file in your root directory! 45 | 46 | ## In a Cargo project 47 | 48 | `oranda` integrates with Cargo projects seamlessly. `oranda build` will pick up relevant 49 | metadata from your `Cargo.toml` file automatically, including [`cargo-dist`] configuration, 50 | if you have that set up. 51 | 52 | ## In a Node project 53 | 54 | If you use Node.js, oranda can not only be installed via npm, but also supports reading metadata 55 | from your package manifest file. Additionally, npm scripts make it easy to integrate `oranda` into 56 | your workflows, for example like this: 57 | 58 | ```json 59 | { 60 | "scripts": { 61 | "build:site": "oranda build" 62 | }, 63 | "dependencies": { 64 | "@axodotdev/oranda": "~0.3.0" 65 | } 66 | } 67 | ``` 68 | 69 | ## Further Steps 70 | 71 | - Explore the [`oranda` configuration options](./configuration.md) 72 | - [Host your site](./hosting.md), on GitHub Pages or elsewhere 73 | - Read the [CLI docs](./cli.md) 74 | - Learn more about [hosting `oranda` sites](./hosting.md) 75 | 76 | [`cargo-dist`]: https://opensource.axo.dev/cargo-dist 77 | -------------------------------------------------------------------------------- /docs/src/tips.md: -------------------------------------------------------------------------------- 1 | # Tips and Tricks 2 | 3 | ## Hiding the Markdown title 4 | 5 | Oranda breaks out your project's title into its own header, which can be annoying if you've started your own 6 | README.md with something like this: 7 | 8 | ```markdown 9 | # myprojectname 10 | 11 | Blah blah blah etc 12 | ``` 13 | 14 | If you build your oranda site like this, the title will appear twice! oranda supports a special class called `oranda-hide` 15 | that you can wrap your title (or whatever you don't want to appear on the page) with, like this: 16 | 17 | ```markdown 18 |
19 | 20 | # myprojectname 21 | 22 |
23 | 24 | Blah blah blah etc 25 | ``` 26 | 27 | Keep in mind the line breaks before and after the HTML, otherwise the Markdown parser may not function correctly. 28 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "rust-analyzer-src": "rust-analyzer-src" 9 | }, 10 | "locked": { 11 | "lastModified": 1692889293, 12 | "narHash": "sha256-pkQGbmDcHD4To6lZGbPcUynTuceqmt0Le6uUfxijYs4=", 13 | "owner": "nix-community", 14 | "repo": "fenix", 15 | "rev": "d3f055733e5449a31c22d64951300072d84c5f81", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "nix-community", 20 | "repo": "fenix", 21 | "type": "github" 22 | } 23 | }, 24 | "flake-utils": { 25 | "inputs": { 26 | "systems": "systems" 27 | }, 28 | "locked": { 29 | "lastModified": 1692799911, 30 | "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", 31 | "owner": "numtide", 32 | "repo": "flake-utils", 33 | "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", 34 | "type": "github" 35 | }, 36 | "original": { 37 | "owner": "numtide", 38 | "repo": "flake-utils", 39 | "type": "github" 40 | } 41 | }, 42 | "nixpkgs": { 43 | "locked": { 44 | "lastModified": 1692734709, 45 | "narHash": "sha256-SCFnyHCyYjwEmgUsHDDuU0TsbVMKeU1vwkR+r7uS2Rg=", 46 | "owner": "NixOS", 47 | "repo": "nixpkgs", 48 | "rev": "b85ed9dcbf187b909ef7964774f8847d554fab3b", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "NixOS", 53 | "ref": "nixos-unstable", 54 | "repo": "nixpkgs", 55 | "type": "github" 56 | } 57 | }, 58 | "root": { 59 | "inputs": { 60 | "fenix": "fenix", 61 | "flake-utils": "flake-utils", 62 | "nixpkgs": "nixpkgs" 63 | } 64 | }, 65 | "rust-analyzer-src": { 66 | "flake": false, 67 | "locked": { 68 | "lastModified": 1692775770, 69 | "narHash": "sha256-LwoR5N1JHykSte2Ak+Pj/HjJ9fKy9zMJNEftfBJQkLs=", 70 | "owner": "rust-lang", 71 | "repo": "rust-analyzer", 72 | "rev": "f5b7c60ff7a79bfb3e10f3e98c81b7bb4cb53c68", 73 | "type": "github" 74 | }, 75 | "original": { 76 | "owner": "rust-lang", 77 | "ref": "nightly", 78 | "repo": "rust-analyzer", 79 | "type": "github" 80 | } 81 | }, 82 | "systems": { 83 | "locked": { 84 | "lastModified": 1681028828, 85 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 86 | "owner": "nix-systems", 87 | "repo": "default", 88 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 89 | "type": "github" 90 | }, 91 | "original": { 92 | "owner": "nix-systems", 93 | "repo": "default", 94 | "type": "github" 95 | } 96 | } 97 | }, 98 | "root": "root", 99 | "version": 7 100 | } 101 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "🎁 generate beautiful landing pages for your developer tools"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | fenix = { 8 | url = "github:nix-community/fenix"; 9 | inputs.nixpkgs.follows = "nixpkgs"; 10 | }; 11 | }; 12 | 13 | outputs = 14 | { self 15 | , nixpkgs 16 | , flake-utils 17 | , fenix 18 | , ... 19 | }: 20 | flake-utils.lib.eachDefaultSystem 21 | (system: 22 | let 23 | pkgs = import nixpkgs { 24 | inherit system; 25 | overlays = [ 26 | fenix.overlays.default 27 | ]; 28 | }; 29 | 30 | # Parse the local Cargo.toml so we track the usual rust workflow 31 | cargo_toml = builtins.fromTOML (builtins.readFile ./Cargo.toml); 32 | 33 | package = with pkgs; 34 | rustPlatform.buildRustPackage { 35 | pname = cargo_toml.package.name; 36 | version = cargo_toml.package.version; 37 | src = ./.; 38 | cargoLock = { 39 | lockFile = ./Cargo.lock; 40 | }; 41 | 42 | # Don't run checks 43 | doCheck = false; 44 | 45 | # Package metadata 46 | meta = with pkgs.lib; { 47 | description = cargo_toml.package.description; 48 | homepage = cargo_toml.package.repository; 49 | license = with licenses; [ asl20 mit ]; 50 | }; 51 | 52 | nativeBuildInputs = with pkgs; [ 53 | pkg-config 54 | tailwindcss 55 | ]; 56 | 57 | buildInputs = with pkgs; ([ 58 | bzip2 59 | oniguruma 60 | openssl 61 | xz 62 | zstd 63 | ] 64 | ++ darwinInputs); 65 | 66 | RUSTONIG_SYSTEM_LIBONIG = true; 67 | ZSTD_SYS_USE_PKG_CONFIG = true; 68 | NIX_LDFLAGS = nixLdFlags; 69 | ORANDA_USE_TAILWIND_BINARY = true; 70 | }; 71 | 72 | # Darwin-specific build requirements 73 | frameworks = pkgs.darwin.apple_sdk.frameworks; 74 | darwinInputs = with pkgs; (lib.optionals stdenv.isDarwin [ libiconv frameworks.Security ]); 75 | nixLdFlags = with pkgs; (lib.optionalString (stdenv.isDarwin) "-F${frameworks.CoreServices}/Library/Frameworks -framework CoreServices -L${libiconv}/lib"); 76 | in 77 | { 78 | packages = rec { 79 | oranda = package; 80 | default = oranda; 81 | }; 82 | 83 | devShells = with pkgs; { 84 | default = mkShell { 85 | nativeBuildInputs = package.nativeBuildInputs; 86 | 87 | buildInputs = 88 | [ 89 | (fenix.packages.${system}.complete.withComponents [ 90 | "cargo" 91 | "clippy" 92 | "rust-src" 93 | "rustc" 94 | "rustfmt" 95 | ]) 96 | ] 97 | ++ darwinInputs; 98 | 99 | # Allow rust-analyzer and other tools to see rust src 100 | RUST_SRC_PATH = "${rustPlatform.rustLibSrc}"; 101 | 102 | # Fix missing OpenSSL 103 | PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; 104 | 105 | shellHook = '' 106 | export NIX_LDFLAGS="${nixLdFlags}" 107 | ''; 108 | }; 109 | }; 110 | }); 111 | } 112 | -------------------------------------------------------------------------------- /generate-css/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oranda-generate-css" 3 | version = "0.6.5" 4 | description = "the part of oranda that generates CSS" 5 | repository = "https://github.com/axodotdev/oranda" 6 | homepage = "https://opensource.axo.dev/oranda" 7 | edition = "2021" 8 | authors = ["Axo Developer Co. "] 9 | license = "MIT OR Apache-2.0" 10 | 11 | [dependencies] 12 | directories = "5.0.1" 13 | camino = "1.1.4" 14 | axoasset = "0.4.0" 15 | tracing = "0.1" 16 | tokio = { version = "1.20.1", features = ["full"] } 17 | reqwest = { version = "0.11.13", default-features = false, features = ["json", "rustls-tls"] } 18 | miette = "5.7.0" 19 | thiserror = "1.0.37" 20 | -------------------------------------------------------------------------------- /generate-css/src/errors.rs: -------------------------------------------------------------------------------- 1 | use miette::Diagnostic; 2 | use thiserror::Error; 3 | 4 | pub type Result = std::result::Result; 5 | 6 | #[derive(Debug, Diagnostic, Error)] 7 | pub enum GenerateCssError { 8 | #[error(transparent)] 9 | Io(#[from] std::io::Error), 10 | 11 | #[error(transparent)] 12 | Reqwest(#[from] reqwest::Error), 13 | 14 | #[error(transparent)] 15 | AxoAsset(#[from] axoasset::AxoassetError), 16 | 17 | #[error(transparent)] 18 | NonUtf8Path(#[from] std::str::Utf8Error), 19 | } 20 | -------------------------------------------------------------------------------- /oranda-css/css/base.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | /* GLOBAL */ 4 | 5 | body, 6 | html { 7 | @apply scroll-smooth h-full; 8 | font-family: var(--font-face); 9 | } 10 | 11 | .container { 12 | @apply min-h-full flex flex-col; 13 | } 14 | 15 | .page-body { 16 | @apply grow; 17 | } 18 | 19 | *:focus { 20 | @apply outline outline-offset-4 outline-2; 21 | } 22 | 23 | body { 24 | background-color: var(--bg-color); 25 | color: var(--fg-color); 26 | } 27 | 28 | /* TEXT */ 29 | 30 | a { 31 | color: var(--link-color); 32 | @apply hover:underline hover:underline-offset-4; 33 | } 34 | 35 | .title { 36 | @apply text-center text-6xl sm:text-8xl pb-2; 37 | } 38 | 39 | h1 { 40 | @apply heading-1; 41 | } 42 | 43 | h2 { 44 | @apply heading-2; 45 | } 46 | 47 | h2, 48 | h3 { 49 | @apply mt-12 sm:mt-24; 50 | } 51 | 52 | h3 { 53 | @apply heading-3; 54 | } 55 | 56 | h4 { 57 | @apply heading-4; 58 | } 59 | 60 | h5 { 61 | @apply heading-5; 62 | } 63 | 64 | h6 { 65 | @apply heading-6; 66 | } 67 | 68 | p, 69 | table { 70 | @apply text-base sm:text-lg leading-relaxed mb-8; 71 | } 72 | 73 | b, 74 | li { 75 | @apply text-base sm:text-lg leading-relaxed; 76 | } 77 | 78 | /* TABLES */ 79 | 80 | table { 81 | @apply my-16; 82 | } 83 | 84 | table th { 85 | @apply text-left uppercase; 86 | padding: 1rem; 87 | } 88 | 89 | table td { 90 | @apply align-top p-4 text-sm font-mono; 91 | } 92 | 93 | table td > code { 94 | @apply text-sm; 95 | } 96 | 97 | table tbody tr { 98 | @apply border-t; 99 | border-color: var(--fg-color); 100 | } 101 | 102 | div.table { 103 | @apply grid grid-cols-4 w-full my-16; 104 | } 105 | 106 | div.table .th { 107 | @apply text-left uppercase font-bold border-t text-lg p-4; 108 | border-color: var(--fg-color); 109 | } 110 | 111 | div.table span:not(.th) { 112 | @apply text-sm font-mono border-t p-4; 113 | border-color: var(--fg-color); 114 | } 115 | 116 | /* LISTS */ 117 | 118 | ul, 119 | li { 120 | @apply list-none; 121 | } 122 | 123 | .rendered-markdown ul, 124 | .rendered-markdown li { 125 | @apply list-disc; 126 | } 127 | 128 | li { 129 | @apply sm:ml-8 ml-4 mb-4; 130 | } 131 | 132 | /* CODE */ 133 | code { 134 | @apply whitespace-pre-wrap text-base sm:text-lg leading-relaxed mb-4; 135 | color: var(--link-color); 136 | } 137 | 138 | div.table code { 139 | @apply text-sm; 140 | } 141 | 142 | h1 code, 143 | h2 code, 144 | h3 code, 145 | h4 code, 146 | h5 code, 147 | h6 code { 148 | font-size: inherit; 149 | line-height: inherit; 150 | } 151 | 152 | pre { 153 | @apply p-4 overflow-auto my-16; 154 | } 155 | 156 | pre > code { 157 | @apply text-xs sm:text-base; 158 | } 159 | 160 | hr { 161 | @apply text-center my-20 border border-dashed w-64 md:w-96 m-auto; 162 | } 163 | 164 | img { 165 | @apply inline; 166 | } 167 | 168 | /* 169 | In Markdown, you'll want standalone images to live in their own paragraph - which is why we can target only these 170 | standalone images and center them probably. 171 | */ 172 | p > img:only-child { 173 | @apply block m-auto; 174 | } 175 | 176 | blockquote { 177 | @apply border-l-2 text-2xl pl-6; 178 | border-color: var(--link-color); 179 | } 180 | 181 | main { 182 | @apply mx-auto my-24 lg:max-w-4xl max-w-[80%]; 183 | } 184 | 185 | .github-icon { 186 | @apply w-5 h-5 bg-github-logo; 187 | } 188 | 189 | .dark .github-icon { 190 | @apply bg-github-logo-dark; 191 | } 192 | 193 | .dark .artifacts, 194 | .light .artifacts { 195 | @apply p-8; 196 | } 197 | 198 | .logo { 199 | @apply m-auto block max-w-xs; 200 | } 201 | 202 | .inline-code { 203 | @apply text-center break-all; 204 | } 205 | -------------------------------------------------------------------------------- /oranda-css/css/buttons.css: -------------------------------------------------------------------------------- 1 | .button { 2 | @apply w-40 min-w-max p-3 rounded text-lg cursor-pointer transition disabled:opacity-60 disabled:cursor-default border-2; 3 | } 4 | 5 | /* A mono-color button */ 6 | .button.primary { 7 | @apply border-transparent; 8 | color: var(--fg-color); 9 | background-color: var(--bg-color); 10 | } 11 | 12 | /* A duo-color button that changes when you hover over it. */ 13 | .button.secondary { 14 | color: var(--fg-color); 15 | background-color: var(--bg-color); 16 | border-color: var(--fg-color); 17 | } 18 | 19 | .button.secondary:hover { 20 | color: var(--bg-color); 21 | background-color: var(--fg-color); 22 | border-color: var(--bg-color); 23 | } 24 | 25 | select { 26 | color: var(--fg-color); 27 | background-color: var(--bg-color); 28 | } -------------------------------------------------------------------------------- /oranda-css/css/components.css: -------------------------------------------------------------------------------- 1 | /* FOOTER */ 2 | 3 | footer { 4 | @apply flex w-full justify-between px-4 py-2 text-xs items-center shrink grow-0; 5 | background-color: var(--fg-color); 6 | color: var(--bg-color); 7 | } 8 | 9 | /* NAV */ 10 | 11 | .nav { 12 | @apply p-0 text-center mb-12; 13 | } 14 | 15 | .nav ul { 16 | @apply p-0 flex flex-wrap gap-6 items-center text-center list-none justify-center; 17 | } 18 | 19 | .nav ul li { 20 | @apply m-0 capitalize; 21 | } 22 | 23 | /* REPO BANNER */ 24 | 25 | .repo_banner { 26 | @apply py-1.5; 27 | color: var(--bg-color); 28 | background-color: var(--fg-color); 29 | } 30 | 31 | .repo_banner > a { 32 | @apply flex justify-center gap-2 items-start hover:text-slate-50 text-slate-50 dark:text-axo-black dark:hover:text-axo-black h-[20px] hover:underline hover:underline-offset-1 dark:hover:decoration-axo-black hover:decoration-slate-50; 33 | } 34 | 35 | /* FUNDING */ 36 | 37 | .funding-wrapper { 38 | @apply mt-8 flex flex-col items-center; 39 | } 40 | 41 | .funding-list { 42 | @apply my-12 w-full lg:grid grid-cols-2 gap-4; 43 | } 44 | 45 | .funding-list li { 46 | @apply m-0 mb-4; 47 | } 48 | 49 | .funding-list li a { 50 | @apply flex gap-2 items-center; 51 | } 52 | 53 | .funding-list li a:hover button { 54 | @apply text-slate-100 bg-axo-orange-dark border-axo-orange-dark; 55 | color: var(--bg-color); 56 | background-color: var(--fg-color); 57 | border-color: var(--bg-color); 58 | } 59 | 60 | .funding-list .button { 61 | @apply block w-auto mr-2; 62 | } 63 | 64 | .preferred-funding-list { 65 | @apply grid-cols-1; 66 | } 67 | 68 | .preferred-funding-list li a { 69 | @apply flex-col text-4xl font-bold; 70 | } 71 | 72 | .preferred-funding-list svg { 73 | @apply w-12 h-12; 74 | } 75 | 76 | .preferred-funding-list .button { 77 | @apply border-0; 78 | } -------------------------------------------------------------------------------- /oranda-css/css/main.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;700;900&display=swap"); 2 | @import "variables.css"; 3 | 4 | @import "base.css"; 5 | @import "utilities.css"; 6 | @import "buttons.css"; 7 | @import "components.css"; 8 | 9 | @import "pages/artifacts.css"; 10 | @import "pages/changelog.css"; 11 | @import "pages/workspace_index.css"; 12 | 13 | @import "themes/axo.css"; 14 | @import "themes/hacker.css"; 15 | @import "themes/cupcake.css"; 16 | -------------------------------------------------------------------------------- /oranda-css/css/pages/artifacts.css: -------------------------------------------------------------------------------- 1 | .package-managers-downloads ul { 2 | @apply my-16; 3 | } 4 | 5 | .package-managers-downloads ul li { 6 | @apply ml-0; 7 | } 8 | 9 | .package-managers-downloads pre { 10 | @apply my-0; 11 | } 12 | 13 | .artifacts { 14 | @apply sm:flex items-center flex-col mb-8 p-0 hidden; 15 | color: var(--highlight-fg-color); 16 | background-color: var(--highlight-bg-color); 17 | } 18 | 19 | .artifacts-table { 20 | @apply block max-w-full overflow-auto; 21 | } 22 | 23 | ul.tabs { 24 | @apply flex border-b-2; 25 | border-color: var(--highlight-fg-color); 26 | } 27 | 28 | ul.tabs li { 29 | @apply hover:cursor-pointer m-0 px-3 py-2 text-base; 30 | } 31 | 32 | ul.tabs li small { 33 | @apply text-xs text-gray-400 block; 34 | } 35 | 36 | ul.tabs li.selected, 37 | ul.tabs li.selected small { 38 | color: var(--highlight-bg-color); 39 | background-color: var(--highlight-fg-color); 40 | } 41 | 42 | .install-content { 43 | @apply max-w-full p-0 m-0; 44 | } 45 | 46 | .detect { 47 | @apply text-center pr-2 md:pr-0; 48 | } 49 | 50 | .detect + a { 51 | @apply block sm:inline my-2 sm:my-0; 52 | } 53 | 54 | .detect .detected-os { 55 | @apply capitalize; 56 | } 57 | 58 | .artifact-header pre { 59 | @apply my-0 mx-auto; 60 | } 61 | 62 | .artifact-header > h4 { 63 | @apply text-center -mb-2 font-bold; 64 | } 65 | 66 | .artifact-header { 67 | @apply max-w-full w-full; 68 | } 69 | 70 | .artifact-header > div:not(.install-code-wrapper) { 71 | @apply md:flex md:gap-4 justify-center text-center md:text-left items-center mt-4; 72 | } 73 | 74 | .backup-download { 75 | @apply hover:no-underline; 76 | } 77 | 78 | .bottom-options { 79 | @apply flex flex-row w-full justify-between items-center; 80 | } 81 | .bottom-options.one { 82 | @apply justify-center; 83 | } 84 | 85 | .install-code-wrapper { 86 | @apply flex items-stretch; 87 | } 88 | 89 | .install-code-wrapper > pre { 90 | @apply flex-grow flex-shrink; 91 | } 92 | 93 | .install-code-wrapper > .button { 94 | @apply w-auto rounded-l-none flex items-center hover:no-underline focus:outline-offset-[-2px]; 95 | } 96 | 97 | .install-code-wrapper > .button.copy-clipboard-button { 98 | @apply rounded-none; 99 | } 100 | 101 | .download-wrapper { 102 | @apply flex flex-row justify-center; 103 | } 104 | .button .button-subtitle { 105 | @apply text-xs block; 106 | } 107 | 108 | .published-date { 109 | @apply block mb-2; 110 | } 111 | 112 | .arch { 113 | @apply p-0 m-0 pt-4; 114 | } 115 | .arch .contents { 116 | @apply pt-4; 117 | min-height: 7rem; 118 | } 119 | 120 | .mobile-download { 121 | @apply block sm:hidden mx-auto mb-12; 122 | } 123 | -------------------------------------------------------------------------------- /oranda-css/css/pages/changelog.css: -------------------------------------------------------------------------------- 1 | .install-code-wrapper > .button svg { 2 | @apply w-6 h-6; 3 | } 4 | 5 | .release-body { 6 | margin-top: 2rem; 7 | word-break: break-word; 8 | } 9 | 10 | .release-body h1 { 11 | @apply heading-2 mt-12; 12 | } 13 | .release-body h2 { 14 | @apply heading-3 mt-12; 15 | } 16 | 17 | .release-body h3 { 18 | @apply heading-4; 19 | } 20 | .release-body h4 { 21 | @apply heading-5; 22 | } 23 | .release-body h5 { 24 | @apply heading-6; 25 | } 26 | 27 | .release-body ul, 28 | .release-body li { 29 | @apply list-disc; 30 | } 31 | 32 | .releases-nav { 33 | @apply top-12 sticky self-start w-max; 34 | } 35 | 36 | .release > h2 { 37 | @apply mt-0; 38 | } 39 | 40 | .release > h2 a { 41 | color: var(--fg-color); 42 | } 43 | 44 | .releases-list { 45 | @apply flex flex-col gap-32; 46 | } 47 | 48 | .releases-wrapper { 49 | @apply md:grid gap-12 relative mt-12; 50 | grid-template-columns: 160px minmax(0, 1fr); 51 | } 52 | 53 | .releases-nav ul { 54 | @apply hidden list-none m-0 md:flex flex-col gap-2 border-l-4 pl-4; 55 | border-color: var(--fg-color); 56 | } 57 | 58 | .releases-nav ul li { 59 | @apply m-0 relative ml-1 text-sm; 60 | } 61 | 62 | .releases-nav ul li:before { 63 | content: ""; 64 | @apply h-1 w-4 block absolute top-1/2 -left-5 -translate-y-1/2; 65 | background-color: var(--fg-color); 66 | } 67 | 68 | .releases-nav ul li a { 69 | @apply decoration-transparent underline-offset-2 hover:underline; 70 | color: var(--fg-color); 71 | } 72 | 73 | .release-info { 74 | @apply flex items-center gap-8 text-base; 75 | } 76 | 77 | .prereleases-toggle { 78 | @apply hidden relative md:flex items-center mb-6 w-max; 79 | } 80 | 81 | .prereleases-toggle input { 82 | @apply h-5 w-5 rounded; 83 | color: var(--fg-color); 84 | } 85 | 86 | .prereleases-toggle label { 87 | @apply font-medium ml-3; 88 | } 89 | 90 | .release-info svg { 91 | @apply w-6 h-6; 92 | } 93 | 94 | .release-info > span { 95 | @apply flex gap-2 items-center; 96 | } 97 | -------------------------------------------------------------------------------- /oranda-css/css/pages/workspace_index.css: -------------------------------------------------------------------------------- 1 | ul.index-grid { 2 | @apply grid lg:grid-cols-2 md:grid-cols-2 gap-8 mt-16 items-stretch; 3 | } 4 | 5 | .index-grid li { 6 | @apply ml-0 border rounded flex flex-col justify-between; 7 | border-color: var(--bg-color); 8 | box-shadow: 0px 0px 0px 8px rgba(0, 0, 0, 0.3); 9 | } 10 | 11 | .index-grid .content { 12 | @apply flex justify-between p-4; 13 | } 14 | 15 | .index-grid .links { 16 | @apply flex w-full border-t divide-x; 17 | } 18 | 19 | .index-grid .links a { 20 | @apply inline-flex items-center space-x-2 w-1/2 px-6 py-4; 21 | } 22 | 23 | .index-grid .content .index-logo { 24 | @apply relative flex-shrink-0 w-20 h-20; 25 | } 26 | 27 | .index-grid li.preferred { 28 | @apply col-span-2; 29 | } 30 | 31 | .index-about h2 { 32 | @apply mt-0; 33 | } -------------------------------------------------------------------------------- /oranda-css/css/themes/axo.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Comfortaa:wght@400;700&display=swap"); 2 | 3 | html.axo { 4 | /* Our theme's own colors */ 5 | --highlight-color: rgb(167 139 250); 6 | --axo-orange-color: #F57070; 7 | --axo-pink-color: #FF75C3; 8 | 9 | /* Base Oranda theme variables */ 10 | --light-fg-color: #141414; 11 | --light-link-color: var(--axo-pink-color); 12 | --dark-link-color: var(--axo-pink-color); 13 | --light-highlight-bg-color: var(--light-bg-color); 14 | --light-highlight-fg-color: var(--light-fg-color); 15 | --dark-highlight-bg-color: var(--light-fg-color); 16 | --dark-highlight-fg-color: var(--light-bg-color); 17 | --font-face: "Comfortaa", sans-serif; 18 | } 19 | 20 | h1, 21 | h2, 22 | h3, 23 | code { 24 | color: var(--highlight-color); 25 | } 26 | 27 | html.axo .button.primary { 28 | background-color: var(--link-color); 29 | } 30 | 31 | html.axo footer, 32 | html.axo .repo_banner { 33 | @apply axo-gradient; 34 | } 35 | 36 | html.axo h1.title { 37 | @apply axo-gradient-text; 38 | } 39 | 40 | .axo-gradient { 41 | background: -webkit-linear-gradient( 42 | left, 43 | var(--axo-orange-color), 44 | var(--axo-pink-color), 45 | var(--axo-orange-color) 46 | ); 47 | background-size: 1600px 200px; 48 | animation-duration: 3s; 49 | animation-name: animation-gradient-title; 50 | animation-iteration-count: infinite; 51 | animation-fill-mode: forwards; 52 | } 53 | 54 | .text-fill-transparent { 55 | -webkit-text-fill-color: transparent; 56 | } 57 | 58 | .axo-gradient-text { 59 | @apply text-fill-transparent axo-gradient; 60 | background-clip: text; 61 | } 62 | 63 | @media (prefers-reduced-motion) { 64 | .axo-gradient { 65 | animation-duration: 0s; 66 | } 67 | } 68 | 69 | @keyframes slide-in { 70 | 0% { 71 | top: -100vh; 72 | } 73 | 100% { 74 | top: 0; 75 | } 76 | } 77 | 78 | @keyframes animation-gradient-title { 79 | 0% { 80 | background-position: 0 1600px; 81 | } 82 | 100% { 83 | background-position: 1600px 0; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /oranda-css/css/themes/cupcake.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"); 2 | 3 | html.cupcake body { 4 | --b1: #faf7f5; 5 | --b2: #dfaff7; 6 | --text: rgba(41, 19, 52, 0.8); 7 | --links: #291334; 8 | --primary: #65c3c8; 9 | --secondary: #291334; 10 | --secondary-100: #210f2a; 11 | --code: #291334; 12 | 13 | font-family: "Inter", sans-serif; 14 | background-color: var(--b1); 15 | color: var(--text); 16 | } 17 | 18 | html.cupcake ::selection { 19 | background-color: var(--b2); 20 | color: var(--code); 21 | -webkit-text-fill-color: var(--code); 22 | } 23 | 24 | html.cupcake .button.primary { 25 | background: var(--secondary); 26 | color: var(--b2); 27 | @apply hover:bg-orange-600 border-transparent no-underline; 28 | } 29 | 30 | html.cupcake .button.primary:hover { 31 | background: var(--secondary-100); 32 | } 33 | 34 | html.cupcake .button.secondary { 35 | border: 1px solid var(--secondary); 36 | color: var(--secondary-100); 37 | } 38 | 39 | html.cupcake .button.secondary:hover { 40 | background: var(--secondary); 41 | color: var(--b2); 42 | } 43 | 44 | html.cupcake h1, 45 | html.cupcake h2, 46 | html.cupcake h3, 47 | html.cupcake h4, 48 | html.cupcake h5, 49 | html.cupcake h6, 50 | html.cupcake p, 51 | html.cupcake table { 52 | color: var(--text); 53 | } 54 | 55 | html.cupcake .title { 56 | color: var(--primary); 57 | } 58 | 59 | html.cupcake a { 60 | color: var(--links); 61 | @apply font-medium underline-offset-4 underline; 62 | } 63 | 64 | html.cupcake a:hover { 65 | color: var(--code); 66 | @apply underline-offset-2; 67 | } 68 | 69 | html.cupcake .axo-gradient { 70 | background: var(--secondary); 71 | } 72 | 73 | html.cupcake .github-icon { 74 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23dfaff7' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); 75 | } 76 | 77 | html.cupcake .repo_banner > a, 78 | html.cupcake footer { 79 | color: var(--b2); 80 | text-decoration: none; 81 | } 82 | 83 | html.cupcake code { 84 | color: var(--code); 85 | @apply font-mono font-medium; 86 | } 87 | 88 | html.cupcake .prereleases-toggle input:checked { 89 | background-color: var(--primary); 90 | } 91 | 92 | html.cupcake .artifacts { 93 | @apply p-8; 94 | } 95 | 96 | html.cupcake .releases-nav ul li a { 97 | color: var(--links); 98 | } 99 | 100 | html.cupcake .releases-nav ul li:before { 101 | @apply bg-gray-300; 102 | } 103 | 104 | html.cupcake .releases-nav ul { 105 | @apply border-l-gray-300; 106 | } 107 | 108 | html.cupcake div.table .th { 109 | color: var(--primary); 110 | } 111 | -------------------------------------------------------------------------------- /oranda-css/css/themes/hacker.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&display=swap"); 2 | 3 | html.hacker { 4 | --light-highlight-bg-color: var(--dark-highlight-bg-color); 5 | --light-highlight-fg-color: var(--dark-highlight-fg-color); 6 | --hacker-green: #20c20e; 7 | } 8 | 9 | html.hacker ::selection { 10 | @apply text-axo-black; 11 | background-color: #20c20e; 12 | } 13 | 14 | html.hacker body { 15 | @apply bg-axo-black text-slate-300; 16 | font-family: "IBM Plex Mono", monospace; 17 | } 18 | 19 | html.hacker .button.secondary { 20 | @apply text-slate-300 border-orange-500 hover:bg-orange-500 hover:text-axo-black; 21 | } 22 | 23 | html.hacker h2, 24 | html.hacker h3, 25 | html.hacker h4, 26 | html.hacker h5, 27 | html.hacker h6 { 28 | @apply text-violet-600; 29 | } 30 | 31 | html.hacker .repo_banner > a, 32 | html.hacker footer { 33 | color: var(--light-color); 34 | @apply py-2; 35 | } 36 | 37 | html.hacker p, 38 | html.hacker table { 39 | @apply text-slate-300; 40 | } 41 | 42 | html.hacker .title { 43 | @apply text-left relative inline-block ml-8; 44 | } 45 | 46 | @keyframes blink-animation { 47 | to { 48 | visibility: hidden; 49 | } 50 | } 51 | 52 | html.hacker .title:after { 53 | content: ""; 54 | height: 70px; 55 | background: var(--hacker-green); 56 | animation: blink-animation 1s steps(5, start) infinite; 57 | @apply block absolute left-full ml-3 w-4 top-3; 58 | } 59 | 60 | html.hacker .title::before { 61 | content: "> "; 62 | @apply block text-gray-800 text-5xl absolute top-1/2 -translate-y-1/2 -left-8 mt-2; 63 | } 64 | 65 | html.hacker .title, 66 | html.hacker div.table .th, 67 | html.hacker h1 { 68 | color: var(--hacker-green); 69 | } 70 | 71 | html.hacker a { 72 | @apply text-orange-500 hover:decoration-orange-500; 73 | } 74 | 75 | html.hacker .axo-gradient { 76 | background: -webkit-linear-gradient( 77 | left, 78 | var(--hacker-green), 79 | var(--color-green-600), 80 | var(--hacker-green) 81 | ); 82 | } 83 | 84 | html.hacker .nav ul { 85 | @apply justify-start; 86 | } 87 | 88 | html.hacker .button.primary { 89 | @apply text-axo-black bg-orange-500 hover:bg-orange-600 border-transparent; 90 | } 91 | 92 | html.hacker .artifact-header > h4 { 93 | @apply text-left; 94 | color: var(--light-color); 95 | } 96 | 97 | html.hacker .releases-nav ul li a { 98 | @apply text-slate-200; 99 | } 100 | 101 | html.hacker .releases-nav ul li:before { 102 | @apply bg-gray-600; 103 | } 104 | 105 | html.hacker .releases-nav ul { 106 | @apply border-l-gray-600; 107 | } 108 | 109 | html.hacker .prereleases-toggle input:checked { 110 | @apply bg-orange-500; 111 | } 112 | 113 | html.hacker .releases-nav ul li a { 114 | @apply hover:decoration-orange-500; 115 | } 116 | 117 | html.hacker .funding-wrapper { 118 | @apply items-start; 119 | } 120 | 121 | html.hacker .artifacts { 122 | @apply p-8; 123 | } 124 | 125 | html.hacker .published-date { 126 | @apply block w-full; 127 | } 128 | 129 | html.hacker .logo { 130 | @apply block m-0; 131 | } 132 | -------------------------------------------------------------------------------- /oranda-css/css/utilities.css: -------------------------------------------------------------------------------- 1 | @tailwind utilities; 2 | 3 | .oblique { 4 | font-style: oblique; 5 | } 6 | 7 | .well-color { 8 | @apply bg-gray-900; 9 | } 10 | 11 | .oranda-hide { 12 | @apply hidden; 13 | } 14 | 15 | @media (prefers-reduced-motion) { 16 | .axo-gradient { 17 | animation-duration: 0s; 18 | } 19 | } 20 | 21 | .heading-1 { 22 | @apply text-3xl sm:text-6xl leading-tight font-black mb-8; 23 | } 24 | 25 | .heading-2 { 26 | @apply text-2xl sm:text-5xl leading-tight font-bold mb-6; 27 | } 28 | 29 | .heading-3 { 30 | @apply text-2xl sm:text-4xl leading-tight font-bold mb-4; 31 | } 32 | 33 | .heading-4 { 34 | @apply text-2xl sm:text-3xl leading-tight mb-4; 35 | } 36 | .heading-5 { 37 | @apply text-xl sm:text-2xl leading-tight text-slate-700 dark:text-slate-200 font-bold mb-4; 38 | } 39 | 40 | .heading-6 { 41 | @apply text-xl sm:text-xl leading-tight text-slate-800 dark:text-slate-300 font-bold mb-4; 42 | } 43 | 44 | .hidden { 45 | display: none; 46 | } 47 | 48 | .inline-icon > svg { 49 | height: 25px; 50 | width: 25px; 51 | display: inline-block; 52 | } 53 | -------------------------------------------------------------------------------- /oranda-css/css/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Base colors for text and background */ 3 | --dark-fg-color: #ffffff; 4 | --light-fg-color: #141414; 5 | --light-bg-color: var(--dark-fg-color); 6 | --dark-bg-color: var(--light-fg-color); 7 | --fg-color: var(--light-fg-color); 8 | --bg-color: var(--light-bg-color); 9 | 10 | /* Link colors */ 11 | --light-link-color: #0284c7; 12 | --dark-link-color: #8BB9FE; 13 | --link-color: var(--light-link-color); 14 | 15 | /* Emphasis colors for text and background */ 16 | --light-highlight-bg-color: #ededed; 17 | --light-highlight-fg-color: #595959; 18 | --dark-highlight-bg-color: #27272a; 19 | --dark-highlight-fg-color: #ededed; 20 | --highlight-fg-color: var(--light-highlight-fg-color); 21 | --highlight-bg-color: var(--light-highlight-bg-color); 22 | --font-face: "Fira Sans", sans-serif; 23 | } 24 | 25 | :root.dark { 26 | --fg-color: var(--dark-fg-color); 27 | --bg-color: var(--dark-bg-color); 28 | --link-color: var(--dark-link-color); 29 | --highlight-fg-color: var(--dark-highlight-fg-color); 30 | --highlight-bg-color: var(--dark-highlight-bg-color); 31 | } -------------------------------------------------------------------------------- /oranda-css/mdbook-theme/fonts/fonts.css: -------------------------------------------------------------------------------- 1 | /* fonts used by axo themes, not sure if this is the best way to fetch them */ 2 | 3 | @import url("https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;700&display=swap"); 4 | @import url("https://fonts.googleapis.com/css2?family=Comfortaa:wght@400;700&display=swap"); 5 | @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&display=swap"); 6 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"); 7 | 8 | /* note that the standard mdbook themes lose their custom fonts by us overriding this */ 9 | -------------------------------------------------------------------------------- /oranda-css/mdbook-theme/oranda-themes/cupcake.css: -------------------------------------------------------------------------------- 1 | .oranda-light { 2 | /* 3 | This part is just defining constants which fringe/oranda-css should probably 4 | be injecting into this file. For now, they're hardcoded. 5 | */ 6 | --color-cupcake-white: rgb(250, 247, 245); 7 | --color-cupcake-black: rgb(41, 19, 52); 8 | --color-cupcake-faded-black: rgba(41, 19, 52, 0.8); 9 | --color-cupcake-blue: rgb(101, 195, 200); 10 | --color-cupcake-rouge: rgb(245, 112, 112); 11 | 12 | /* 13 | Here we select which colors/fonts to use for this specific theme. 14 | This first block calls a lot of the shots, most other definitions just 15 | defer to these values. 16 | */ 17 | --bg: var(--color-cupcake-white); 18 | --fg: var(--color-cupcake-faded-black); 19 | --well-bg: var(--bg); 20 | --well-bg-highlight: var(--color-cupcake-blue); 21 | --title-fg: var(--color-cupcake-blue); 22 | --subtitle-fg: var(--fg); 23 | --border-color: var(--color-cupcake-rouge); 24 | --main-font: Inter, sans-serif; 25 | --mono-font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace; 26 | 27 | --sidebar-bg: var(--bg); 28 | --sidebar-fg: var(--fg); 29 | --sidebar-non-existant: var(--bg); 30 | --sidebar-active: var(--color-cupcake-faded-black); 31 | --sidebar-spacer: var(--border-color); 32 | 33 | --scrollbar: default; 34 | 35 | --icons: var(--color-cupcake-black); 36 | --icons-hover: var(--color-cupcake-black); 37 | 38 | --links: var(--color-cupcake-black); 39 | --links-hover: var(--color-cupcake-black); 40 | 41 | --inline-code-color: var(--color-cupcake-black); 42 | 43 | --theme-popup-bg: var(--well-bg); 44 | --theme-popup-border: var(--border-color); 45 | --theme-hover: var(--well-bg-highlight); 46 | 47 | --quote-bg: var(--bg); 48 | --quote-border: var(--border-color); 49 | 50 | --table-border-color: var(--border-color); 51 | --table-header-bg: var(--well-bg-highlight); 52 | --table-alternate-bg: var(--well-bg); 53 | 54 | --searchbar-border-color: var(--border-color); 55 | --searchbar-bg: var(--well-bg); 56 | --searchbar-fg: var(--fg); 57 | --searchbar-shadow-color: var(--border-color); 58 | --searchresults-header-fg: var(--title-fg); 59 | --searchresults-border-color: var(--border-color); 60 | --searchresults-li-bg: var(--well-bg); 61 | --search-mark-bg: var(--well-bg-highlight); 62 | } 63 | -------------------------------------------------------------------------------- /oranda-css/mdbook-theme/oranda-themes/hacker.css: -------------------------------------------------------------------------------- 1 | .oranda-dark { 2 | /* 3 | This part is just defining constants which fringe/oranda-css should probably 4 | be injecting into this file. For now, they're hardcoded. 5 | */ 6 | --color-hacker-rouge: rgb(245, 112, 112); 7 | --color-hacker-orange: rgb(249, 115, 22); 8 | --color-hacker-green: rgb(32, 194, 14); 9 | --color-hacker-purple: rgb(124, 58, 237); 10 | --color-hacker-black: rgb(13, 13, 13); 11 | --color-hacker-white: rgb(203, 213, 225); 12 | --color-wellish: rgb(38, 50, 56); 13 | --color-wellish-light: rgb(73, 96, 108); 14 | 15 | /* 16 | Here we select which colors/fonts to use for this specific theme. 17 | This first block calls a lot of the shots, most other definitions just 18 | defer to these values. 19 | */ 20 | --bg: var(--color-hacker-black); 21 | --fg: var(--color-hacker-white); 22 | --well-bg: var(--color-wellish); 23 | --well-bg-highlight: var(--color-wellish-light); 24 | --title-fg: var(--color-hacker-green); 25 | --subtitle-fg: var(--color-hacker-purple); 26 | --border-color: var(--color-hacker-orange); 27 | --main-font: IBM Plex Mono, monospace; 28 | --mono-font: IBM Plex Mono, monospace; 29 | 30 | --sidebar-bg: var(--bg); 31 | --sidebar-fg: var(--color-hacker-orange); 32 | --sidebar-non-existant: var(--bg); 33 | --sidebar-active: var(--color-hacker-purple); 34 | --sidebar-spacer: var(--border-color); 35 | 36 | --scrollbar: default; 37 | 38 | --icons: var(--color-hacker-orange); 39 | --icons-hover: var(--color-hacker-orange); 40 | 41 | --links: var(--color-hacker-orange); 42 | --links-hover: var(--color-hacker-orange); 43 | 44 | --inline-code-color: var(--color-hacker-purple); 45 | 46 | --theme-popup-bg: var(--well-bg); 47 | --theme-popup-border: var(--border-color); 48 | --theme-hover: var(--well-bg-highlight); 49 | 50 | --quote-bg: var(--bg); 51 | --quote-border: var(--border-color); 52 | 53 | --table-border-color: var(--border-color); 54 | --table-header-bg: var(--well-bg-highlight); 55 | --table-alternate-bg: var(--well-bg); 56 | 57 | --searchbar-border-color: var(--border-color); 58 | --searchbar-bg: var(--well-bg); 59 | --searchbar-fg: var(--fg); 60 | --searchbar-shadow-color: var(--border-color); 61 | --searchresults-header-fg: var(--title-fg); 62 | --searchresults-border-color: var(--border-color); 63 | --searchresults-li-bg: var(--well-bg); 64 | --search-mark-bg: var(--well-bg-highlight); 65 | } 66 | -------------------------------------------------------------------------------- /oranda-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "tailwindcss -c tailwind.config.js -i ./css/main.css -o ./dist/oranda.css --minify" 5 | }, 6 | "dependencies": { 7 | "@tailwindcss/forms": "^0.5.5", 8 | "@tailwindcss/typography": "^0.5.9", 9 | "tailwindcss": "^3.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /oranda-css/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | const listStyleType = { 3 | circle: "circle", 4 | square: "square", 5 | "lower-roman": "lower-roman", 6 | "lower-alpha": "lower-alpha", 7 | }; 8 | 9 | const maxWidth = { 10 | "prose-lg": "80ch", 11 | }; 12 | 13 | const backgroundImage = { 14 | "github-logo": `url("data:image/svg+xml,%3Csvg role='img' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3EGitHub%3C/title%3E%3Cpath fill='#ffffff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")`, 15 | "github-logo-dark": `url("data:image/svg+xml,%3Csvg role='img' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3EGitHub%3C/title%3E%3Cpath fill='#141414' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")`, 16 | }; 17 | const extend = { 18 | listStyleType, 19 | maxWidth, 20 | backgroundImage, 21 | colors: { 22 | "axo-pink": "hsla(326, 100%, 73%, 1)", 23 | "axo-pink-dark": "hsla(326, 52%, 58%, 1)", 24 | "axo-orange": "hsla(0, 87%, 70%, 1)", 25 | "axo-orange-dark": "hsla(356, 75%, 64%, 1)", 26 | "axo-highlighter": "hsla(51, 100%, 50%, 1)", 27 | "axo-black": "hsla(0, 0%, 8%, 1)", 28 | "axo-light-gray": "hsla(0, 0%, 93%, 1)", 29 | "axo-dark-gray": "hsla(0, 0%, 35%, 1)" 30 | }, 31 | }; 32 | 33 | const extractColorVars = (themeColors, colorGroup = "") => 34 | Object.keys(themeColors).reduce((currentVars, colorKey) => { 35 | const value = themeColors[colorKey]; 36 | const newVars = 37 | typeof value === "string" 38 | ? { [`--color${colorGroup}-${colorKey}`]: value } 39 | : extractColorVars(value, `-${colorKey}`); 40 | 41 | return { ...currentVars, ...newVars }; 42 | }, {}); 43 | 44 | const tailwindColorsToCSSVariables = ({ addBase, theme }) => 45 | addBase({ 46 | ":root": extractColorVars(theme("colors")), 47 | }); 48 | 49 | module.exports = { 50 | darkMode: "class", 51 | theme: { extend }, 52 | plugins: [ 53 | require("@tailwindcss/typography"), 54 | require("@tailwindcss/forms"), 55 | tailwindColorsToCSSVariables, 56 | ], 57 | }; 58 | -------------------------------------------------------------------------------- /oranda.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "path_prefix": "oranda" 4 | }, 5 | "styles": { 6 | "theme": "axodark", 7 | "favicon": "https://www.axo.dev/favicon.ico" 8 | }, 9 | "marketing": { 10 | "social": { 11 | "image": "https://www.axo.dev/meta_small.jpeg", 12 | "image_alt": "axo", 13 | "twitter_account": "@axodotdev" 14 | }, 15 | "analytics": { 16 | "plausible": { 17 | "domain": "opensource.axo.dev" 18 | } 19 | } 20 | }, 21 | "components": { 22 | "changelog": true, 23 | "artifacts": { 24 | "package_managers": { 25 | "preferred": { 26 | "npm": "npm install @axodotdev/oranda --save-dev" 27 | }, 28 | "additional": { 29 | "cargo": "cargo install oranda --locked --profile=dist", 30 | "npx": "npx @axodotdev/oranda", 31 | "binstall": "cargo binstall oranda", 32 | "nix-env": "nix-env -i oranda", 33 | "nix flake": "nix profile install github:axodotdev/oranda" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.78" 3 | components = ["rustc", "cargo", "rust-std", "clippy", "rustfmt"] 4 | -------------------------------------------------------------------------------- /src/commands/build.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use clap::Parser; 3 | 4 | use oranda::config::Config; 5 | 6 | use oranda::errors::*; 7 | use oranda::site::Site; 8 | 9 | #[derive(Debug, Parser)] 10 | pub struct Build { 11 | /// DO NOT USE: Path to the root dir of the project 12 | /// 13 | /// This flag exists for internal testing. It is incorrectly implemented for actual 14 | /// end-users and will make you very confused and sad. 15 | #[clap(hide = true)] 16 | #[arg(long, default_value = "./")] 17 | project_root: Utf8PathBuf, 18 | /// DO NOT USE: Path to the oranda.json 19 | /// 20 | /// This flag exists for internal testing. It is incorrectly implemented for actual 21 | /// end-users and will make you very confused and sad. 22 | #[clap(hide = true)] 23 | #[arg(long, default_value = "./oranda.json")] 24 | config_path: Utf8PathBuf, 25 | /// Only build the artifacts JSON file (if applicable) and other files that may be used to 26 | /// support it, such as installer source files. 27 | #[arg(long)] 28 | json_only: bool, 29 | } 30 | 31 | impl Build { 32 | pub fn new(project_root: Option, config_path: Option) -> Self { 33 | Build { 34 | project_root: project_root.unwrap_or(Utf8PathBuf::from("./")), 35 | config_path: config_path.unwrap_or(Utf8PathBuf::from("./oranda.json")), 36 | json_only: false, 37 | } 38 | } 39 | 40 | pub fn run(&self) -> Result<()> { 41 | if let Some(config) = Site::get_workspace_config()? { 42 | let sites = Site::build_multi(&config, self.json_only)?; 43 | if config.workspace.generate_index && !self.json_only { 44 | tracing::info!("Building workspace index page..."); 45 | let mut member_data = Vec::new(); 46 | for site in &sites { 47 | // Unwrap here because `Site::build_multi` always sets `workspace_data = Some(_)`. 48 | // It's only set to `None` on a _single_ page build, which can't happen in this 49 | // code path. 50 | member_data.push(site.workspace_data.clone().unwrap()); 51 | } 52 | Site::build_and_write_workspace_index(&config, &member_data)?; 53 | } 54 | 55 | for site in sites { 56 | site.write(None)?; 57 | } 58 | let msg = format!( 59 | "Your site builds are located in `{}`.", 60 | config.build.dist_dir 61 | ); 62 | tracing::info!(success = true, "{}", &msg); 63 | } else { 64 | let config = Config::build(&self.config_path)?; 65 | if self.json_only { 66 | Site::build_single_json_only(&config, None)?; 67 | } else { 68 | Site::build_single(&config, None)?.write(Some(&config))?; 69 | } 70 | let msg = format!("Your site build is located in `{}`.", { 71 | config.build.dist_dir 72 | }); 73 | tracing::info!(success = true, "{}", &msg); 74 | } 75 | Ok(()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/commands/generate.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use clap::{Parser, Subcommand, ValueEnum}; 3 | use oranda::errors::Result; 4 | 5 | #[derive(Debug, Subcommand)] 6 | pub enum GenerateCommand { 7 | /// Generates a CI file that can be used to deploy your site. 8 | Ci(Ci), 9 | } 10 | 11 | #[derive(Debug, Parser)] 12 | pub struct Ci { 13 | /// What CI to generate a file for. 14 | #[arg(long, default_value_t = CiType::Github)] 15 | #[clap(value_enum)] 16 | ci: CiType, 17 | } 18 | 19 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, ValueEnum)] 20 | pub enum CiType { 21 | /// Deploy to GitHub Pages using GitHub Actions 22 | Github, 23 | } 24 | 25 | #[derive(Debug, Parser)] 26 | pub struct Generate { 27 | /// What type of thing to generate. 28 | #[command(subcommand)] 29 | kind: GenerateCommand, 30 | /// Path to the output file. 31 | #[arg(short, long)] 32 | #[clap(global = true)] 33 | output_path: Option, 34 | /// Path to your oranda site 35 | #[arg(short, long)] 36 | #[clap(global = true)] 37 | site_path: Option, 38 | } 39 | 40 | impl Generate { 41 | pub fn run(&self) -> Result<()> { 42 | let path = self 43 | .output_path 44 | .clone() 45 | .unwrap_or_else(|| self.default_path()); 46 | match self.kind { 47 | // TODO: Pass `CiType` in here when we add another one. 48 | GenerateCommand::Ci(_) => oranda::generate::generate_ci(path, &self.site_path)?, 49 | }; 50 | Ok(()) 51 | } 52 | 53 | fn default_path(&self) -> Utf8PathBuf { 54 | match self.kind { 55 | // TODO: Match on `CiType` when we add another one. 56 | GenerateCommand::Ci(_) => Utf8PathBuf::from(".github/workflows/web.yml"), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | mod build; 2 | mod dev; 3 | mod generate; 4 | mod print; 5 | mod serve; 6 | 7 | pub use build::Build; 8 | pub use dev::Dev; 9 | pub use generate::Generate; 10 | pub use print::ConfigSchema; 11 | pub use print::GenerateCss; 12 | pub use serve::Serve; 13 | -------------------------------------------------------------------------------- /src/commands/print.rs: -------------------------------------------------------------------------------- 1 | use axoasset::LocalAsset; 2 | use camino::Utf8PathBuf; 3 | use clap::Parser; 4 | use oranda::errors::*; 5 | use oranda_generate_css::default_css_output_dir; 6 | 7 | #[derive(Debug, Parser)] 8 | pub struct ConfigSchema { 9 | /// Write the config schema to the named file instead of stdout 10 | #[clap(long)] 11 | pub output: Option, 12 | } 13 | 14 | impl ConfigSchema { 15 | pub fn run(&self) -> Result<()> { 16 | let schema = schemars::schema_for!(oranda::config::OrandaLayer); 17 | let json_schema = 18 | serde_json::to_string_pretty(&schema).expect("failed to stringify schema!?"); 19 | 20 | if let Some(output) = &self.output { 21 | let contents = json_schema + "\n"; 22 | LocalAsset::write_new(&contents, output)?; 23 | } else { 24 | println!("{json_schema}"); 25 | } 26 | Ok(()) 27 | } 28 | } 29 | 30 | #[derive(Debug, Parser)] 31 | pub struct GenerateCss { 32 | #[clap(long)] 33 | out_dir: Option, 34 | } 35 | 36 | impl GenerateCss { 37 | pub fn run(&self) -> Result<()> { 38 | let out_dir = self.out_dir.clone().unwrap_or_else(default_css_output_dir); 39 | let out_file = out_dir.join("oranda.css"); 40 | oranda_generate_css::build_css(&out_dir)?; 41 | tracing::info!("CSS placed in {out_file}"); 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/serve.rs: -------------------------------------------------------------------------------- 1 | use camino::{Utf8Path, Utf8PathBuf}; 2 | use std::net::SocketAddr; 3 | use std::sync::mpsc::Receiver; 4 | use std::thread; 5 | 6 | use oranda::config::Config; 7 | use oranda::errors::*; 8 | 9 | use axum::{http::StatusCode, routing::get_service, Router}; 10 | 11 | use clap::Parser; 12 | use tower_http::services::ServeDir; 13 | use tower_livereload::LiveReloadLayer; 14 | 15 | #[derive(Debug, Default, Parser)] 16 | pub struct Serve { 17 | #[arg(long, default_value = "7979")] 18 | port: u16, 19 | } 20 | 21 | impl Serve { 22 | pub fn new(port: Option) -> Self { 23 | Serve { 24 | port: port.unwrap_or(7979), 25 | } 26 | } 27 | 28 | pub fn run(&self) -> Result<()> { 29 | let config = Self::build_config()?; 30 | if Utf8Path::new(&config.build.dist_dir).is_dir() { 31 | self.serve(&config.build.dist_dir, &config.build.path_prefix, None)?; 32 | Ok(()) 33 | } else { 34 | Err(OrandaError::BuildNotFound { 35 | dist_dir: config.build.dist_dir.to_string(), 36 | }) 37 | } 38 | } 39 | 40 | pub fn run_with_livereload(&self, rx: Receiver<()>) -> Result<()> { 41 | let config = Self::build_config()?; 42 | if Utf8Path::new(&config.build.dist_dir).is_dir() { 43 | let livereload = LiveReloadLayer::new(); 44 | self.serve( 45 | &config.build.dist_dir, 46 | &config.build.path_prefix, 47 | Some((livereload, rx)), 48 | )?; 49 | 50 | Ok(()) 51 | } else { 52 | Err(OrandaError::BuildNotFound { 53 | dist_dir: config.build.dist_dir.to_string(), 54 | }) 55 | } 56 | } 57 | 58 | #[tokio::main] 59 | async fn serve( 60 | &self, 61 | dist_dir: &str, 62 | path_prefix: &Option, 63 | livereload: Option<(LiveReloadLayer, Receiver<()>)>, 64 | ) -> Result<()> { 65 | let serve_dir = 66 | get_service(ServeDir::new(dist_dir)).handle_error(|error: std::io::Error| async move { 67 | ( 68 | StatusCode::INTERNAL_SERVER_ERROR, 69 | format!("Unhandled internal error: {}", error), 70 | ) 71 | }); 72 | 73 | let prefix_route = if let Some(prefix) = path_prefix { 74 | format!("/{}", prefix) 75 | } else { 76 | "/".to_string() 77 | }; 78 | let mut app = Router::new().nest_service(&prefix_route, serve_dir); 79 | if let Some(livereload) = livereload { 80 | let (livereload, rx) = livereload; 81 | let reloader = livereload.reloader(); 82 | app = app.layer(livereload); 83 | 84 | // Because the server will later block this thread, spawn another thread to handle 85 | // reload request messages. 86 | thread::spawn(move || loop { 87 | rx.recv().expect("broken pipe"); 88 | reloader.reload(); 89 | }); 90 | } 91 | 92 | let addr = SocketAddr::from(([127, 0, 0, 1], self.port)); 93 | let msg = format!( 94 | "Your project is available at: http://{}/{}", 95 | addr, 96 | path_prefix.as_ref().unwrap_or(&String::new()) 97 | ); 98 | tracing::info!(success = true, "{}", &msg); 99 | axum::Server::bind(&addr) 100 | .serve(app.into_make_service()) 101 | .await 102 | .expect("failed to start server"); 103 | Ok(()) 104 | } 105 | 106 | fn build_config() -> Result { 107 | let workspace_config_path = &Utf8PathBuf::from("./oranda-workspace.json"); 108 | if workspace_config_path.exists() { 109 | Config::build(workspace_config_path) 110 | } else { 111 | Config::build(&Utf8PathBuf::from("./oranda.json")) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/config/builds.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::{ApplyLayer, ApplyOptExt, ApplyValExt}; 6 | 7 | #[derive(Debug, Clone)] 8 | /// Information about how the pages should be built (complete version) 9 | pub struct BuildConfig { 10 | /// Relative path to the dir where build output should be placed 11 | pub dist_dir: String, 12 | /// Relative path to a dir full of extra static content 13 | pub static_dir: String, 14 | /// A path fragment to prepend before URLs 15 | /// 16 | /// This allows things like hosting a static site at `axodotdev.github.io/my_project/` 17 | pub path_prefix: Option, 18 | /// Additional pages that should be included in the top level nav. 19 | /// 20 | /// This is a map from page-label to relative paths to pages. 21 | /// 22 | /// We use IndexMap to respect the order the user provided. 23 | pub additional_pages: IndexMap, 24 | } 25 | #[derive(Debug, Serialize, Deserialize, JsonSchema)] 26 | #[serde(deny_unknown_fields)] 27 | /// Information about how the pages of your site should be built 28 | pub struct BuildLayer { 29 | /// Relative path to the dir where build output should be placed 30 | /// 31 | /// This is "./public/" by default 32 | pub dist_dir: Option, 33 | /// Relative path to a dir full of extra static content that should be included in your site 34 | /// 35 | /// (FIXME: explain what paths it ends up at) 36 | /// 37 | /// This is "./static/" by default 38 | pub static_dir: Option, 39 | /// A path fragment to prepend before URLs 40 | /// 41 | /// This allows things like hosting a static site at `axodotdev.github.io/my_project/` 42 | /// (you would set path_prefix = "my_project" for that). 43 | pub path_prefix: Option, 44 | /// Additional pages that should be included in the top level nav. 45 | /// 46 | /// This is a map from page-label to relative paths to (Github Flavored) Markdown files 47 | /// that should be rendered into pages. 48 | /// 49 | /// These pages will be listed in the given order after "home" and before 50 | /// other pages that oranda automatically adds like "install" and "funding". 51 | pub additional_pages: Option>, 52 | } 53 | 54 | impl Default for BuildConfig { 55 | fn default() -> Self { 56 | BuildConfig { 57 | dist_dir: "public".to_owned(), 58 | static_dir: "static".to_owned(), 59 | path_prefix: None, 60 | additional_pages: Default::default(), 61 | } 62 | } 63 | } 64 | impl ApplyLayer for BuildConfig { 65 | type Layer = BuildLayer; 66 | fn apply_layer(&mut self, layer: Self::Layer) { 67 | // This is intentionally written slightly cumbersome to make you update this 68 | let BuildLayer { 69 | dist_dir, 70 | static_dir, 71 | path_prefix, 72 | additional_pages, 73 | } = layer; 74 | self.dist_dir.apply_val(dist_dir); 75 | self.static_dir.apply_val(static_dir); 76 | self.path_prefix.apply_opt(path_prefix); 77 | // In the future this might want to be `extend` 78 | self.additional_pages.apply_val(additional_pages); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/config/components/artifacts/package_managers.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::config::{ApplyLayer, ApplyValExt}; 6 | 7 | /// Package managers to display (complete version) 8 | #[derive(Debug, Clone)] 9 | pub struct PackageManagersConfig { 10 | pub preferred: IndexMap, 11 | pub additional: IndexMap, 12 | } 13 | /// Package managers to display 14 | #[derive(Debug, Serialize, Deserialize, JsonSchema)] 15 | #[serde(deny_unknown_fields)] 16 | pub struct PackageManagersLayer { 17 | /// Packages to display in both the install widget and install page 18 | /// 19 | /// See docs for the parent "package_managers" field for details 20 | pub preferred: Option>, 21 | /// Packages to display in just the install page 22 | /// 23 | /// See docs for the parent "package_managers" field for details 24 | pub additional: Option>, 25 | } 26 | 27 | impl Default for PackageManagersConfig { 28 | fn default() -> Self { 29 | PackageManagersConfig { 30 | preferred: IndexMap::default(), 31 | additional: IndexMap::default(), 32 | } 33 | } 34 | } 35 | impl ApplyLayer for PackageManagersConfig { 36 | type Layer = PackageManagersLayer; 37 | fn apply_layer(&mut self, layer: Self::Layer) { 38 | // This is intentionally written slightly cumbersome to make you update this 39 | let PackageManagersLayer { 40 | preferred, 41 | additional, 42 | } = layer; 43 | // In the future these might want to be `extend` 44 | self.preferred.apply_val(preferred); 45 | self.additional.apply_val(additional); 46 | } 47 | } 48 | 49 | impl PackageManagersConfig { 50 | pub fn has(&self, key: &str) -> bool { 51 | self.preferred.contains_key(key) || self.additional.contains_key(key) 52 | } 53 | pub fn has_npm(&self) -> bool { 54 | self.has("npm") || self.has("npx") 55 | } 56 | pub fn is_empty(&self) -> bool { 57 | self.preferred.is_empty() && self.additional.is_empty() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/config/components/changelog.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ApplyLayer, ApplyValExt}; 2 | use schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Config for tweaking the changelog page generation 6 | #[derive(Debug, Clone)] 7 | pub struct ChangelogConfig { 8 | /// Whether to attempt to read from the local changelog file 9 | pub read_changelog_file: bool, 10 | /// Whether to generate a RSS file 11 | pub rss_feed: bool, 12 | } 13 | 14 | /// The config for generating a separate changelog page 15 | #[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] 16 | pub struct ChangelogLayer { 17 | /// Whether we factor in the local `CHANGELOG.md` file, attempt to parse 18 | /// it, and try and match version headings to release versions that we 19 | /// detect. 20 | pub read_changelog_file: Option, 21 | /// Whether to generate a RSS file under `changelog.rss`. 22 | pub rss_feed: Option, 23 | } 24 | 25 | impl Default for ChangelogConfig { 26 | fn default() -> Self { 27 | ChangelogConfig { 28 | read_changelog_file: true, 29 | rss_feed: true, 30 | } 31 | } 32 | } 33 | 34 | impl ApplyLayer for ChangelogConfig { 35 | type Layer = ChangelogLayer; 36 | fn apply_layer(&mut self, layer: Self::Layer) { 37 | // This is intentionally written slightly cumbersome to make you update this 38 | let ChangelogLayer { 39 | read_changelog_file, 40 | rss_feed, 41 | } = layer; 42 | self.read_changelog_file.apply_val(read_changelog_file); 43 | self.rss_feed.apply_val(rss_feed); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/config/components/funding.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8PathBuf; 2 | use schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | use std::path::Path; 5 | 6 | use crate::config::{ApplyLayer, ApplyOptExt}; 7 | use crate::data::funding::FundingType; 8 | use crate::errors::*; 9 | 10 | /// Config for displaying funding information on your page (complete version) 11 | #[derive(Debug, Clone)] 12 | pub struct FundingConfig { 13 | pub preferred_funding: Option, 14 | pub yml_path: Option, 15 | pub md_path: Option, 16 | } 17 | /// Settings for displaying funding information on your page 18 | #[derive(Debug, Serialize, Deserialize, JsonSchema)] 19 | #[serde(deny_unknown_fields)] 20 | pub struct FundingLayer { 21 | /// A funding method to make larger/focused to encourage over all others 22 | pub preferred_funding: Option, 23 | /// A path to a github-format FUNDING.yml file 24 | /// 25 | /// We parse this out to get a list of funding sources. 26 | /// 27 | /// By default we try to find this at "./.github/FUNDING.yml" 28 | pub yml_path: Option, 29 | /// A relative path to a freeform github-flavor markdown file 30 | /// whose contents will be included on your funding page. 31 | /// 32 | /// By default we try to find this at "./funding.md" 33 | pub md_path: Option, 34 | } 35 | 36 | impl Default for FundingConfig { 37 | fn default() -> Self { 38 | FundingConfig { 39 | preferred_funding: None, 40 | yml_path: None, 41 | md_path: None, 42 | } 43 | } 44 | } 45 | impl ApplyLayer for FundingConfig { 46 | type Layer = FundingLayer; 47 | fn apply_layer(&mut self, layer: Self::Layer) { 48 | // This is intentionally written slightly cumbersome to make you update this 49 | let FundingLayer { 50 | preferred_funding, 51 | yml_path, 52 | md_path, 53 | } = layer; 54 | self.preferred_funding.apply_opt(preferred_funding); 55 | self.yml_path.apply_opt(yml_path); 56 | self.md_path.apply_opt(md_path); 57 | } 58 | } 59 | 60 | impl FundingConfig { 61 | /// If we have a FUNDING.yml file, try to find it. If we fail, we disable funding support. 62 | pub fn find_paths(config: &mut Option, start_dir: &Path) -> Result<()> { 63 | // If this is None, we were force-disabled and shouldn't auto-detect 64 | let Some(this) = config else { return Ok(()) }; 65 | 66 | // Try to auto-detect the FUNDING.yml if not specified 67 | if this.yml_path.is_none() { 68 | let default_yml_path = 69 | Utf8PathBuf::from(format!("{}/.github/FUNDING.yml", start_dir.display())); 70 | if default_yml_path.exists() { 71 | this.yml_path = Some(default_yml_path.to_string()); 72 | } 73 | } 74 | // Try to auto-detect funding.md if not specified 75 | if this.md_path.is_none() { 76 | let default_md_path = Utf8PathBuf::from(format!("{}/funding.md", start_dir.display())); 77 | if default_md_path.exists() { 78 | this.md_path = Some(default_md_path.to_string()); 79 | } 80 | } 81 | 82 | // This is intentionally written slightly cumbersome to make you update this 83 | let FundingConfig { 84 | preferred_funding, 85 | yml_path, 86 | md_path, 87 | } = this; 88 | let cant_find_files = yml_path.is_none() && md_path.is_none(); 89 | let has_user_config = preferred_funding.is_some(); 90 | if cant_find_files { 91 | // The config is unusable. 92 | // 93 | // * If the user customized stuff, error out because they clearly wanted this to work 94 | // * Otherwise, just disable the feature 95 | if has_user_config { 96 | return Err(OrandaError::FundingConfigInvalid); 97 | } else { 98 | *config = None; 99 | } 100 | } 101 | Ok(()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/config/components/mdbooks.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | use std::path::Path; 4 | 5 | use crate::config::{ApplyLayer, ApplyOptExt, ApplyValExt}; 6 | use crate::errors::*; 7 | 8 | /// Config for us building and integrating your mdbook (complete version) 9 | #[derive(Debug, Clone)] 10 | pub struct MdBookConfig { 11 | /// Path to the mdbook 12 | /// 13 | /// If not set we will attempt to auto-detect 14 | pub path: Option, 15 | /// Whether to enable the custom oranda/axo theme 16 | pub theme: bool, 17 | } 18 | 19 | /// The config for building and embedding an mdbook on your site 20 | #[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] 21 | #[serde(deny_unknown_fields)] 22 | pub struct MdBookLayer { 23 | /// Path to the mdbook (the directory containing book.toml) 24 | /// 25 | /// If not set we will attempt to auto-detect this by trying 26 | /// "./", "./book/", and "./docs/". 27 | pub path: Option, 28 | /// Whether to enable oranda's customized mdbook theme that unifies 29 | /// with your oranda theme. 30 | /// 31 | /// If enabled we will use mdbook's custom themeing system to overwrite 32 | /// most of the hbs/css/js file mdbook defines to add hooks for our 33 | /// custom themes to be enabled and defaulted on. The existing mdbook 34 | /// themes will still be available and should work normally. 35 | /// 36 | /// Unfortunately this means that `mdbook build` won't produce the same 37 | /// results as `oranda build`. In the future we may introduce a way 38 | /// to "vendor" the changes oranda makes so that `mdbook build` behaves 39 | /// the same. This should be possible because we mostly use officially 40 | /// supported mdbook settings when changing the theme (the only exception 41 | /// being we add an extra css file for our theme's syntax highlighter). 42 | /// 43 | /// Any other mdbook settings should ideally be preserved/respected. 44 | /// 45 | /// If the theme has a paired dark/light variant, that variant will 46 | /// also be made available, although we won't respect mdbook's builtin 47 | /// preferred dark-mode setting, to ensure the rest of your oranda site 48 | /// always looks the same (this may be improved when the rest of oranda 49 | /// gets richer support for light/dark-mode). 50 | /// 51 | /// defaults to true 52 | pub theme: Option, 53 | } 54 | 55 | impl Default for MdBookConfig { 56 | fn default() -> Self { 57 | MdBookConfig { 58 | path: None, 59 | theme: true, 60 | } 61 | } 62 | } 63 | impl ApplyLayer for MdBookConfig { 64 | type Layer = MdBookLayer; 65 | fn apply_layer(&mut self, layer: Self::Layer) { 66 | // This is intentionally written slightly cumbersome to make you update this 67 | let MdBookLayer { path, theme } = layer; 68 | self.path.apply_opt(path); 69 | self.theme.apply_val(theme); 70 | } 71 | } 72 | 73 | impl MdBookConfig { 74 | /// If mdbook is enabled but the path isn't set, we try to find it 75 | /// 76 | /// If we fail, we set mdbook to None to disable it. 77 | pub fn find_paths(config: &mut Option, start_dir: &Path) -> Result<()> { 78 | // If this is None, we were force-disabled and shouldn't auto-detect 79 | let Some(this) = config else { 80 | return Ok(()); 81 | }; 82 | 83 | if this.path.is_none() { 84 | // Ok time to auto-detect, try these dirs for a book.toml 85 | let possible_paths = vec!["./", "./book/", "./docs/"]; 86 | for book_dir in possible_paths { 87 | let book_path = start_dir.join(book_dir).join("book.toml"); 88 | if book_path.exists() { 89 | // nice, use it 90 | this.path = Some(book_dir.to_owned()); 91 | return Ok(()); 92 | } 93 | } 94 | } 95 | 96 | // This is intentionally written slightly cumbersome to make you update this 97 | let MdBookConfig { path, theme } = this; 98 | let cant_find_files = path.is_none(); 99 | let has_user_config = *theme != MdBookConfig::default().theme; 100 | if cant_find_files { 101 | // The config is unusable. 102 | // 103 | // * If the user customized stuff, error out because they clearly wanted this to work 104 | // * Otherwise, just disable the feature 105 | if has_user_config { 106 | return Err(OrandaError::MdBookConfigInvalid); 107 | } else { 108 | *config = None; 109 | } 110 | } 111 | Ok(()) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/config/marketing/analytics.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::site::layout::javascript::analytics::{Fathom, Google, Plausible, Umami}; 5 | 6 | /// Settings for Analytics 7 | /// 8 | /// Analytics providers are currently mututally exclusive -- you can pick at most one. 9 | #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] 10 | #[serde(rename_all = "lowercase")] 11 | pub enum AnalyticsConfig { 12 | /// Use Google Analytics 13 | Google(Google), 14 | /// Use Plausible Analytics 15 | Plausible(Plausible), 16 | /// Use Fathom Analytics 17 | Fathom(Fathom), 18 | /// Use Umami Analytics 19 | Umami(Umami), 20 | } 21 | -------------------------------------------------------------------------------- /src/config/marketing/mod.rs: -------------------------------------------------------------------------------- 1 | pub use analytics::AnalyticsConfig; 2 | use schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | pub use social::{SocialConfig, SocialLayer}; 5 | 6 | use super::ApplyLayer; 7 | 8 | mod analytics; 9 | mod social; 10 | 11 | /// Marketing config (complete version) 12 | #[derive(Debug, Clone)] 13 | pub struct MarketingConfig { 14 | /// Analytics 15 | pub analytics: Option, 16 | /// Social media 17 | pub social: SocialConfig, 18 | } 19 | /// Settings for marketing/social/analytics 20 | #[derive(Debug, Serialize, Deserialize, JsonSchema)] 21 | #[serde(deny_unknown_fields)] 22 | pub struct MarketingLayer { 23 | /// Settings for analytics 24 | /// 25 | /// Analytics providers are currently mututally exclusive -- you can pick at most one. 26 | pub analytics: Option, 27 | /// Settings for social media integrations 28 | pub social: Option, 29 | } 30 | 31 | impl Default for MarketingConfig { 32 | fn default() -> Self { 33 | MarketingConfig { 34 | analytics: None, 35 | social: SocialConfig::default(), 36 | } 37 | } 38 | } 39 | impl ApplyLayer for MarketingConfig { 40 | type Layer = MarketingLayer; 41 | fn apply_layer(&mut self, layer: Self::Layer) { 42 | // This is intentionally written slightly cumbersome to make you update this 43 | let MarketingLayer { analytics, social } = layer; 44 | 45 | // FIXME: this is kinda goofy but there's not an obvious thing to do 46 | // if we need to change the enum variant and we care about preserving things. 47 | // So we just clobber the old value no matter what 48 | if let Some(analytics) = analytics { 49 | self.analytics = Some(analytics); 50 | } 51 | self.social.apply_val_layer(social); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/config/marketing/social.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::config::{ApplyLayer, ApplyOptExt}; 5 | 6 | // Social media config (complete version) 7 | #[derive(Debug, Serialize, Clone)] 8 | pub struct SocialConfig { 9 | pub image: Option, 10 | pub image_alt: Option, 11 | pub twitter_account: Option, 12 | } 13 | // Settings for social media integrations 14 | #[derive(Debug, Serialize, Deserialize, JsonSchema)] 15 | #[serde(deny_unknown_fields)] 16 | pub struct SocialLayer { 17 | /// Image to show in link previews 18 | /// 19 | /// FIXME: what format? 20 | pub image: Option, 21 | /// Alt image to show in link previews 22 | /// 23 | /// FIXME: explain the distinction with "image" 24 | /// 25 | /// FIXME: what format? 26 | pub image_alt: Option, 27 | /// Twitter account to show in link previews 28 | /// 29 | /// Example: "@axodotdev" 30 | pub twitter_account: Option, 31 | } 32 | 33 | impl Default for SocialConfig { 34 | fn default() -> Self { 35 | SocialConfig { 36 | image: None, 37 | image_alt: None, 38 | twitter_account: None, 39 | } 40 | } 41 | } 42 | impl ApplyLayer for SocialConfig { 43 | type Layer = SocialLayer; 44 | fn apply_layer(&mut self, layer: Self::Layer) { 45 | // This is intentionally written slightly cumbersome to make you update this 46 | let SocialLayer { 47 | image, 48 | image_alt, 49 | twitter_account, 50 | } = layer; 51 | self.image.apply_opt(image); 52 | self.image_alt.apply_opt(image_alt); 53 | self.twitter_account.apply_opt(twitter_account); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/config/oranda_config.rs: -------------------------------------------------------------------------------- 1 | use axoasset::SourceFile; 2 | use camino::Utf8PathBuf; 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::errors::*; 7 | 8 | use super::{BuildLayer, ComponentLayer, MarketingLayer, ProjectLayer, StyleLayer, WorkspaceLayer}; 9 | 10 | /// Configuration for `oranda` (typically stored in oranda.json) 11 | #[derive(Debug, Serialize, Deserialize, JsonSchema)] 12 | #[serde(deny_unknown_fields)] 13 | pub struct OrandaLayer { 14 | /// Info about the project/application you're making a site for 15 | /// 16 | /// All of these values should automatically be sourced from your Cargo.toml or package.json 17 | /// whenever possible. You should only need to set these if you want to override the value. 18 | pub project: Option, 19 | /// Settings for the build/output of the site 20 | pub build: Option, 21 | /// Settings for social/marketing/analytics 22 | pub marketing: Option, 23 | /// Settings for themes/styles of the site 24 | pub styles: Option, 25 | /// Additional optional components 26 | pub components: Option, 27 | /// Workspace configuration 28 | pub workspace: Option, 29 | /// Field that text-editors can use to fetch the schema for this struct 30 | /// 31 | /// We never use this, but we don't want to error out if its set. 32 | #[serde(rename = "$schema")] 33 | pub _schema: Option, 34 | } 35 | 36 | impl OrandaLayer { 37 | pub fn load(config_path: &Utf8PathBuf) -> Result> { 38 | let config_result = SourceFile::load_local(config_path.as_path()); 39 | 40 | match config_result { 41 | Ok(config) => { 42 | let data: OrandaLayer = config.deserialize_json()?; 43 | Ok(Some(data)) 44 | } 45 | Err(_) => { 46 | tracing::debug!("No config found, using default values"); 47 | Ok(None) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/config/style.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::config::{ApplyLayer, ApplyOptExt}; 5 | use crate::site::{markdown::SyntaxTheme, oranda_theme::OrandaTheme}; 6 | 7 | use super::ApplyValExt; 8 | 9 | pub const ORANDA_CSS_TAG: &str = "v0.6.1"; 10 | 11 | /// Config related to styling your page (complete version) 12 | #[derive(Debug, Clone)] 13 | pub struct StyleConfig { 14 | pub theme: OrandaTheme, 15 | pub syntax_theme: SyntaxTheme, 16 | pub additional_css: Vec, 17 | pub oranda_css_version: String, 18 | pub logo: Option, 19 | pub favicon: Option, 20 | } 21 | /// Settings for styling your page 22 | #[derive(Debug, Serialize, Deserialize, JsonSchema)] 23 | #[serde(deny_unknown_fields)] 24 | pub struct StyleLayer { 25 | /// The builtin oranda theme to use for all your pages 26 | /// 27 | /// If using oranda's mdbook integration this will also restyle your mdbook 28 | /// (assuming we made an equivalent mdbook theme). 29 | /// 30 | /// Default is "dark" 31 | pub theme: Option, 32 | /// The builtin syntax highlighting theme to use for all your pages 33 | /// 34 | /// WARNING: this feature is currently non-functional, only the default works! 35 | /// 36 | /// Default is "MaterialTheme" 37 | syntax_theme: Option, 38 | /// A list of relative paths to extra css files to include in all your pages 39 | pub additional_css: Option>, 40 | /// A way to force oranda to use a different archived version of its builtin css 41 | /// 42 | /// The value is the git-tag of the release on the oranda repo to fetch oranda.css from. 43 | /// 44 | /// Example: "css-v0.0.7" 45 | pub oranda_css_version: Option, 46 | /// A relative path or URL to an image to use as the logo of your project 47 | pub logo: Option, 48 | /// A relative path or URL to an image to use as the favicon of your site 49 | pub favicon: Option, 50 | } 51 | 52 | impl Default for StyleConfig { 53 | fn default() -> Self { 54 | StyleConfig { 55 | theme: OrandaTheme::Dark, 56 | syntax_theme: SyntaxTheme::MaterialTheme, 57 | additional_css: vec![], 58 | oranda_css_version: ORANDA_CSS_TAG.to_owned(), 59 | logo: None, 60 | favicon: None, 61 | } 62 | } 63 | } 64 | impl ApplyLayer for StyleConfig { 65 | type Layer = StyleLayer; 66 | fn apply_layer(&mut self, layer: Self::Layer) { 67 | // This is intentionally written slightly cumbersome to make you update this 68 | let StyleLayer { 69 | theme, 70 | syntax_theme, 71 | additional_css, 72 | oranda_css_version, 73 | logo, 74 | favicon, 75 | } = layer; 76 | 77 | self.theme.apply_val(theme); 78 | self.syntax_theme.apply_val(syntax_theme); 79 | self.oranda_css_version.apply_val(oranda_css_version); 80 | // In the future this might want to be `extend` 81 | self.additional_css.apply_val(additional_css); 82 | self.logo.apply_opt(logo); 83 | self.favicon.apply_opt(favicon); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/config/workspace.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ApplyLayer, ApplyOptExt, ApplyValExt}; 2 | use schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | use std::path::PathBuf; 5 | 6 | #[derive(Debug, Serialize, Deserialize, JsonSchema)] 7 | #[serde(deny_unknown_fields)] 8 | /// Configuration regarding multi-project "workspaces". 9 | pub struct WorkspaceLayer { 10 | /// The top-level name to be used in the index page 11 | pub name: Option, 12 | /// Whether to generate an index page linking workspace members together 13 | pub generate_index: Option, 14 | /// A list of workspace members 15 | pub members: Option>, 16 | /// A list of members given priority in display 17 | pub preferred_members: Option>, 18 | /// Whether to enable workspace autodetection 19 | pub auto: Option, 20 | /// The path to additional documentation to render 21 | pub docs_path: Option, 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Hash, PartialEq, Eq)] 25 | #[serde(deny_unknown_fields)] 26 | pub struct WorkspaceMember { 27 | /// Slug for the generated URLs and directories 28 | pub slug: String, 29 | /// Path to the workspace member directory 30 | pub path: PathBuf, 31 | } 32 | 33 | #[derive(Debug, Serialize, Clone)] 34 | pub struct WorkspaceConfig { 35 | pub name: Option, 36 | pub generate_index: bool, 37 | pub members: Vec, 38 | pub preferred_members: Vec, 39 | pub auto: bool, 40 | pub docs_path: Option, 41 | } 42 | 43 | impl Default for WorkspaceConfig { 44 | fn default() -> Self { 45 | Self { 46 | name: Some("My Oranda Config".to_string()), 47 | generate_index: true, 48 | members: Vec::new(), 49 | preferred_members: Vec::new(), 50 | auto: false, 51 | docs_path: None, 52 | } 53 | } 54 | } 55 | 56 | impl ApplyLayer for WorkspaceConfig { 57 | type Layer = WorkspaceLayer; 58 | fn apply_layer(&mut self, layer: Self::Layer) { 59 | let WorkspaceLayer { 60 | name, 61 | members, 62 | preferred_members, 63 | generate_index, 64 | auto, 65 | docs_path, 66 | } = layer; 67 | self.name.apply_opt(name); 68 | self.generate_index.apply_val(generate_index); 69 | self.members.apply_val(members); 70 | self.preferred_members.apply_val(preferred_members); 71 | self.auto.apply_val(auto); 72 | self.docs_path = docs_path 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/data/axodotdev/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::*; 2 | 3 | use axoproject::GithubRepo; 4 | use gazenot::{Gazenot, PublicRelease, ReleaseAsset}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use super::artifacts::{File, ReleaseArtifacts}; 8 | 9 | #[derive(Clone, Debug, Serialize, Deserialize)] 10 | pub struct AxoResponse { 11 | pub success: bool, 12 | pub result: Vec, 13 | } 14 | 15 | #[derive(Clone, Debug, Serialize, Deserialize)] 16 | pub struct AxoRelease { 17 | pub tag_name: String, 18 | pub name: String, 19 | pub body: String, 20 | pub version: String, 21 | pub prerelease: bool, 22 | pub created_at: String, 23 | pub assets: Vec, 24 | } 25 | 26 | #[derive(Clone, Debug, Serialize, Deserialize)] 27 | pub struct AxoReleaseAsset { 28 | pub browser_download_url: String, 29 | pub name: String, 30 | pub uploaded_at: String, 31 | } 32 | 33 | impl AxoRelease { 34 | pub async fn fetch_all(package_name: &str, repo: &GithubRepo) -> Result> { 35 | let abyss = Gazenot::new_unauthed("github".to_string(), repo.owner.clone())?; 36 | let list = abyss 37 | .list_releases_many(vec![package_name.to_string()]) 38 | .await?; 39 | let list = list 40 | .into_iter() 41 | .find(|r| r.package_name == package_name) 42 | .ok_or(OrandaError::AxoReleasesFetchError)?; 43 | 44 | Ok(list.releases.into_iter().map(|r| r.into()).collect()) 45 | } 46 | 47 | pub fn has_dist_manifest(&self) -> bool { 48 | self.assets.iter().any(|a| a.name == "dist-manifest.json") 49 | } 50 | 51 | pub fn asset_url<'a>(&'a self, asset_name: &'a str) -> Option<&'a str> { 52 | for asset in &self.assets { 53 | if asset.name == asset_name { 54 | return Some(&asset.browser_download_url); 55 | } 56 | } 57 | None 58 | } 59 | 60 | pub fn repo_has_releases(name: &str, repo: &GithubRepo) -> Result { 61 | if let Ok(releases) = 62 | tokio::runtime::Handle::current().block_on(AxoRelease::fetch_all(name, repo)) 63 | { 64 | if releases.is_empty() { 65 | Ok(false) 66 | } else { 67 | Ok(true) 68 | } 69 | } else { 70 | let warning = OrandaError::ReleasesCheckFailed { 71 | repo: repo.to_string(), 72 | }; 73 | eprintln!("{:?}", miette::Report::new(warning)); 74 | Ok(false) 75 | } 76 | } 77 | } 78 | 79 | impl ReleaseArtifacts { 80 | pub fn add_axodotdev(&mut self, release: &AxoRelease) { 81 | for asset in &release.assets { 82 | let file = File { 83 | name: asset.name.clone(), 84 | download_url: asset.browser_download_url.clone(), 85 | view_path: None, 86 | checksum_file: None, 87 | infer: true, 88 | }; 89 | self.add_file(file); 90 | } 91 | } 92 | } 93 | 94 | impl From for AxoReleaseAsset { 95 | fn from(value: ReleaseAsset) -> Self { 96 | let ReleaseAsset { 97 | browser_download_url, 98 | name, 99 | uploaded_at, 100 | } = value; 101 | 102 | Self { 103 | browser_download_url, 104 | name, 105 | uploaded_at, 106 | } 107 | } 108 | } 109 | impl From for AxoRelease { 110 | fn from(value: PublicRelease) -> Self { 111 | let PublicRelease { 112 | tag_name, 113 | version, 114 | name, 115 | body, 116 | prerelease, 117 | created_at, 118 | assets, 119 | } = value; 120 | 121 | let assets: Vec = assets.into_iter().map(|a| a.into()).collect(); 122 | 123 | Self { 124 | tag_name, 125 | version, 126 | name, 127 | body, 128 | prerelease, 129 | created_at, 130 | assets, 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/data/workspaces.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::errors::{OrandaError, Result}; 3 | use camino::Utf8PathBuf; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct WorkspaceData { 7 | pub root_path: Utf8PathBuf, 8 | pub slug: String, 9 | pub path: Utf8PathBuf, 10 | pub config: Config, 11 | } 12 | 13 | pub fn from_config( 14 | workspace_config: &Config, 15 | root_path: &Utf8PathBuf, 16 | workspace_config_path: &Utf8PathBuf, 17 | ) -> Result> { 18 | let mut vec = Vec::new(); 19 | for member in workspace_config.workspace.members.clone() { 20 | if !member.path.exists() { 21 | return Err(OrandaError::FileNotFound { 22 | filedesc: "workspace member".to_string(), 23 | path: member.path.display().to_string(), 24 | }); 25 | } 26 | 27 | // FIXME: I expect this to break at some point, because making paths absolute is an absolute 28 | // hellhole, and should not be taken for granted. 29 | let path = Utf8PathBuf::from(member.path.display().to_string()).canonicalize_utf8()?; 30 | let mut config_path = path.clone(); 31 | config_path.push("oranda.json"); 32 | let mut config = Config::build_workspace_member( 33 | &config_path, 34 | workspace_config_path, 35 | &path, 36 | &member, 37 | Some(member.slug.clone()), 38 | )?; 39 | 40 | // Set the correct path prefix. This should be: 41 | // - If no root path prefix: `slug` 42 | // - If root path prefix: `path_prefix/slug` 43 | config.build.path_prefix = 44 | if let Some(path_prefix) = workspace_config.build.path_prefix.as_ref() { 45 | // FIXME: Doesn't account for trailing slashes right now 46 | Some(format!("{}/{}", path_prefix, &member.slug)) 47 | } else { 48 | Some(member.slug.to_string()) 49 | }; 50 | 51 | // Set the correct dist_dir. This should be `cwd_from_root/workspace_dist_dir/slug` 52 | config.build.dist_dir = format!( 53 | "{}/{}/{}", 54 | root_path, workspace_config.build.dist_dir, &member.slug 55 | ); 56 | 57 | vec.push(WorkspaceData { 58 | root_path: root_path.clone(), 59 | slug: member.slug.clone(), 60 | path, 61 | config, 62 | }); 63 | } 64 | 65 | Ok(vec) 66 | } 67 | -------------------------------------------------------------------------------- /src/generate.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Result; 2 | use axoasset::LocalAsset; 3 | use camino::Utf8PathBuf; 4 | use inquire::ui::{Color, RenderConfig, Styled}; 5 | use inquire::Confirm; 6 | use minijinja::{context, Environment}; 7 | 8 | const CI_TEMPLATE: &str = include_str!("../templates/generate/web.yml.j2"); 9 | 10 | pub fn generate_ci(path: Utf8PathBuf, site_dir: &Option) -> Result<()> { 11 | tracing::info!("Generating a CI deploy workflow for your site..."); 12 | 13 | let mut env = Environment::new(); 14 | // Modify the syntax so that it doesn't clash with GitHub Actions' syntax. 15 | env.set_syntax(minijinja::Syntax { 16 | block_start: "{{%".into(), 17 | block_end: "%}}".into(), 18 | variable_start: "{{{".into(), 19 | variable_end: "}}}".into(), 20 | comment_start: "{{#".into(), 21 | comment_end: "#}}".into(), 22 | })?; 23 | env.add_template_owned("web.yml", CI_TEMPLATE)?; 24 | let prompt_prefix = Styled::new("? >o_o<").with_fg(Color::DarkGreen); 25 | let render_config = RenderConfig::default().with_prompt_prefix(prompt_prefix); 26 | 27 | // Does the file already exist? If so, prompt the user to overwrite. 28 | let existing_file = if path.exists() { 29 | let confirm_prompt = Confirm::new(&format!( 30 | "There's already a file at {:?}! Do you want to override it?", 31 | path 32 | )) 33 | .with_default(false) 34 | .with_render_config(render_config) 35 | .prompt(); 36 | 37 | if let Ok(false) = confirm_prompt { 38 | tracing::info!("Ok, exiting..."); 39 | return Ok(()); 40 | } 41 | let file = LocalAsset::load_string(&path)?; 42 | Some(file) 43 | } else { 44 | None 45 | }; 46 | 47 | // Ask whether we want to include a link checker 48 | let check_links_prompt = 49 | Confirm::new("Do you want to include a tool for checking generated hyperlinks?") 50 | .with_default(true) 51 | .with_help_message( 52 | "Read more about the link checker tool: https://github.com/untitaker/hyperlink", 53 | ) 54 | .with_render_config(render_config) 55 | .prompt() 56 | .expect("Error while prompting!"); 57 | 58 | let use_latest_oranda_prompt = Confirm::new("Do you want to always use the latest version of oranda?") 59 | .with_default(false) 60 | .with_help_message("Using the latest version means you don't have to rerun this command when oranda updates, but it may also break your site if you use a lot of configuration!") 61 | .with_render_config(render_config) 62 | .prompt() 63 | .expect("Error while prompting!"); 64 | 65 | let context = context!(check_links => check_links_prompt, use_latest_oranda => use_latest_oranda_prompt, site_dir => site_dir); 66 | let template = env.get_template("web.yml")?; 67 | let rendered = template.render(context)?; 68 | if existing_file.is_some_and(|file| file == rendered) { 69 | tracing::warn!("File exists and is identical, aborting..."); 70 | return Ok(()); 71 | } 72 | 73 | let append_gitignore = Confirm::new("Would you like to append the default destination directory (`public`) to your .gitignore file?") 74 | .with_default(false) 75 | .with_render_config(render_config) 76 | .prompt() 77 | .expect("Error while prompting!"); 78 | if append_gitignore { 79 | append_dist_dir_to_gitignore()?; 80 | } 81 | 82 | LocalAsset::write_new_all(&rendered, &path)?; 83 | tracing::info!(success = true, "Wrote CI file to {:?}", path); 84 | 85 | Ok(()) 86 | } 87 | 88 | fn append_dist_dir_to_gitignore() -> Result<()> { 89 | let path = Utf8PathBuf::from(".gitignore"); 90 | let mut contents = if path.exists() { 91 | LocalAsset::load_string(&path)? 92 | } else { 93 | String::new() 94 | }; 95 | contents.push_str("\n# Generated by `oranda generate ci`\npublic/"); 96 | LocalAsset::write_new(&contents, &path)?; 97 | 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::uninlined_format_args)] 2 | #![allow(clippy::result_large_err)] 3 | 4 | pub mod config; 5 | pub mod data; 6 | pub mod errors; 7 | pub mod formatter; 8 | pub mod generate; 9 | pub mod paths; 10 | pub mod site; 11 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::uninlined_format_args)] 2 | 3 | use clap::{Parser, Subcommand}; 4 | use miette::Report; 5 | use tracing::subscriber::set_default; 6 | use tracing::Level; 7 | use tracing_subscriber::layer::SubscriberExt; 8 | 9 | mod commands; 10 | use commands::{Build, ConfigSchema, Dev, GenerateCss, Serve}; 11 | 12 | pub mod formatter; 13 | use crate::commands::Generate; 14 | use formatter::OutputFormat; 15 | 16 | #[derive(Parser, Debug)] 17 | #[command(author, version, about, long_about = None)] 18 | struct Cli { 19 | #[clap(subcommand)] 20 | command: Command, 21 | 22 | /// Whether to output more detailed debug information 23 | #[clap(short, long)] 24 | #[clap(help_heading = "GLOBAL OPTIONS", global = true)] 25 | pub verbose: bool, 26 | 27 | /// The format of the output 28 | #[clap(long, value_enum)] 29 | #[clap(default_value_t = OutputFormat::Human)] 30 | #[clap(help_heading = "GLOBAL OPTIONS", global = true)] 31 | pub output_format: OutputFormat, 32 | } 33 | 34 | #[derive(Subcommand, Debug)] 35 | enum Command { 36 | /// Build an oranda site. 37 | Build(Build), 38 | /// Start a local development server that recompiles your oranda site if a file changes. 39 | Dev(Dev), 40 | /// Start a file server to access your oranda site in a browser. 41 | Serve(Serve), 42 | /// Generate infrastructure files for oranda sites. 43 | Generate(Generate), 44 | #[clap(hide = true)] 45 | ConfigSchema(ConfigSchema), 46 | #[clap(hide = true)] 47 | GenerateCss(GenerateCss), 48 | } 49 | 50 | fn main() { 51 | let cli = Cli::parse(); 52 | 53 | axocli::CliAppBuilder::new("oranda") 54 | .json_errors(cli.output_format == OutputFormat::Json) 55 | .start(cli, run); 56 | } 57 | 58 | fn run(cli: &axocli::CliApp) -> Result<(), Report> { 59 | let runtime = tokio::runtime::Builder::new_multi_thread() 60 | .worker_threads(1) 61 | .max_blocking_threads(128) 62 | .enable_all() 63 | .build() 64 | .expect("Initializing tokio runtime failed"); 65 | let _guard = runtime.enter(); 66 | let log_level = if cli.config.verbose { 67 | Level::DEBUG 68 | } else { 69 | Level::INFO 70 | }; 71 | let sub_filter = tracing_subscriber::filter::Targets::new().with_target("oranda", log_level); 72 | let sub = tracing_subscriber::registry() 73 | .with(formatter::CaptureFieldsLayer) 74 | .with(tracing_subscriber::fmt::layer().event_format(formatter::OrandaFormatter)) 75 | .with(sub_filter); 76 | let _sub_guard = set_default(sub); 77 | 78 | match &cli.config.command { 79 | Command::Build(cmd) => cmd.run()?, 80 | Command::Dev(cmd) => cmd.clone().run()?, 81 | Command::Serve(cmd) => cmd.run()?, 82 | Command::ConfigSchema(cmd) => cmd.run()?, 83 | Command::GenerateCss(cmd) => cmd.run()?, 84 | Command::Generate(cmd) => cmd.run()?, 85 | }; 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /src/paths.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::errors::OrandaError; 3 | use camino::{Utf8Path, Utf8PathBuf}; 4 | 5 | /// Creates a workspace-safe relative path. Takes the following arguments: 6 | /// - The root path of the workspace (or single project) 7 | /// - An optional workspace member path 8 | /// - The path itself, usually extracted from the configuration 9 | /// Member path and the path itself can be relative or absolute . 10 | /// The function will attempt to lazily build the smallest possible absolute and canonicalized path, 11 | /// before diffing it with the root path to create a path that's always relative to the workspace root. 12 | /// 13 | /// Some example scenarios: 14 | /// 1. root path = "/my/directory", member path = None, path = "myfile.md" 15 | /// Output = "myfile.md" 16 | /// 2. root path = "/my/directory", member path = "member", path = "myfile.md" 17 | /// Output = "member/myfile.md" 18 | /// 3. root path= "/my/directory", member path = "/my/directory/member", path = "../root.md" 19 | /// Output = "root.md" 20 | pub fn determine_path( 21 | root_path: impl AsRef, 22 | member_path: &Option>, 23 | path: impl AsRef, 24 | ) -> errors::Result> { 25 | let root_path = root_path.as_ref(); 26 | let member_path = member_path.as_ref().map(|p| p.as_ref()); 27 | let path = path.as_ref(); 28 | if path.is_absolute() { 29 | // If absolute, return the path 30 | return Ok(Some(path.to_owned())); 31 | } 32 | 33 | // If the member path exists and is absolute, construct `member_path/path`. 34 | // If the member path exists and isn't absolute, construct `root_path/member_path/path`. 35 | // If the member path doesn't exist, construct `root_path/path`. 36 | let path_plus_member = if let Some(member_path) = member_path { 37 | if member_path.is_absolute() { 38 | let mut owned = Utf8PathBuf::new(); 39 | owned.push(member_path); 40 | owned.push(path); 41 | owned.canonicalize_utf8() 42 | } else { 43 | let mut owned = Utf8PathBuf::new(); 44 | owned.push(root_path); 45 | owned.push(member_path); 46 | owned.push(path); 47 | owned.canonicalize_utf8() 48 | } 49 | } else { 50 | let mut owned = Utf8PathBuf::new(); 51 | owned.push(root_path); 52 | owned.push(path); 53 | owned.canonicalize_utf8() 54 | }; 55 | 56 | match path_plus_member { 57 | Ok(path) => { 58 | // Create a relative path from difference between root and created path. 59 | Ok(Some(pathdiff::diff_utf8_paths(&path, root_path).ok_or( 60 | OrandaError::PathdiffError { 61 | root_path: root_path.to_string(), 62 | path: path.to_string(), 63 | }, 64 | )?)) 65 | } 66 | Err(_) => { 67 | // The path doesn't exist, return None 68 | Ok(None) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/site/layout/header.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::errors::*; 3 | use crate::site::link; 4 | 5 | use axoasset::{Asset, LocalAsset}; 6 | 7 | const DEFAULT_FAVICON: &[u8] = include_bytes!("../../../assets/favicon.ico"); 8 | 9 | /// Copies the favicon into the dist dir 10 | pub fn place_default_favicon(config: &Config) -> Result<()> { 11 | let asset = LocalAsset::new("favicon.ico", DEFAULT_FAVICON.to_vec())?; 12 | asset.write(&config.build.dist_dir)?; 13 | Ok(()) 14 | } 15 | 16 | /// Fetches the logo and adds it to the dist_dir, then returns the path to link it with 17 | pub fn get_logo(logo: &str, config: &Config) -> Result { 18 | let fetched_logo = fetch_logo(&config.build.path_prefix, &config.build.dist_dir, logo); 19 | 20 | tokio::runtime::Handle::current().block_on(fetched_logo) 21 | } 22 | 23 | /// Inner impl of [`get_logo`][] 24 | async fn fetch_logo( 25 | path_prefix: &Option, 26 | dist_dir: &str, 27 | origin_path: &str, 28 | ) -> Result { 29 | let copy_result = Asset::copy(origin_path, dist_dir).await?; 30 | 31 | let path_as_string = copy_result.strip_prefix(dist_dir)?.to_string_lossy(); 32 | let src = link::generate_relative(path_prefix, &path_as_string); 33 | 34 | Ok(src) 35 | } 36 | -------------------------------------------------------------------------------- /src/site/layout/javascript/analytics.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::config::AnalyticsConfig; 5 | 6 | #[derive(Serialize, Debug, Default)] 7 | pub struct Analytics { 8 | pub snippet: Option, 9 | pub google_script: Option, 10 | } 11 | 12 | impl Analytics { 13 | pub fn new(config: &Option) -> Self { 14 | if let Some(analytics) = config { 15 | match analytics { 16 | AnalyticsConfig::Google(provider) => { 17 | let google_script = Some(provider.get_script()); 18 | Self { 19 | snippet: Some(provider.snippet()), 20 | google_script, 21 | } 22 | } 23 | AnalyticsConfig::Plausible(provider) => Self::build(provider), 24 | AnalyticsConfig::Fathom(provider) => Self::build(provider), 25 | AnalyticsConfig::Umami(provider) => Self::build(provider), 26 | } 27 | } else { 28 | Self { 29 | snippet: None, 30 | google_script: None, 31 | } 32 | } 33 | } 34 | 35 | fn build(provider: &T) -> Self { 36 | Self { 37 | snippet: Some(provider.snippet()), 38 | google_script: None, 39 | } 40 | } 41 | } 42 | 43 | #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] 44 | pub struct Google { 45 | pub tracking_id: String, 46 | } 47 | 48 | #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] 49 | pub struct Fathom { 50 | pub site: String, 51 | } 52 | 53 | #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] 54 | pub struct Plausible { 55 | pub domain: String, 56 | pub script_url: Option, 57 | } 58 | 59 | #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] 60 | pub struct Umami { 61 | pub website: String, 62 | pub script_url: String, 63 | } 64 | 65 | const GOOGLE_SCRIPT_URL: &str = "https://www.googletagmanager.com/gtag/js"; 66 | const PLAUSIBLE_SCRIPT_URL: &str = "https://plausible.io/js/script.js"; 67 | const FATHOM_SCRIPT_URL: &str = "https://cdn.usefathom.com/script.js"; 68 | 69 | impl Google { 70 | pub fn get_script(&self) -> String { 71 | format!("window.dataLayer = window.dataLayer || []; function gtag(){{dataLayer.push(arguments);}} gtag('js', new Date());gtag('config', '{}');", self.tracking_id) 72 | } 73 | } 74 | 75 | trait Snippet { 76 | fn snippet(&self) -> String; 77 | } 78 | 79 | impl Snippet for Google { 80 | fn snippet(&self) -> String { 81 | let script_url = format!("{}?id={}", GOOGLE_SCRIPT_URL, self.tracking_id); 82 | format!(r#""#) 83 | } 84 | } 85 | 86 | impl Snippet for Fathom { 87 | fn snippet(&self) -> String { 88 | format!( 89 | r#""#, 90 | self.site 91 | ) 92 | } 93 | } 94 | 95 | impl Snippet for Umami { 96 | fn snippet(&self) -> String { 97 | format!( 98 | r#""#, 99 | self.script_url, self.website 100 | ) 101 | } 102 | } 103 | 104 | impl Snippet for Plausible { 105 | fn snippet(&self) -> String { 106 | let url = PLAUSIBLE_SCRIPT_URL.to_string(); 107 | let script_url = self.script_url.as_ref().unwrap_or(&url); 108 | format!( 109 | r#""#, 110 | self.domain 111 | ) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/site/layout/javascript/mod.rs: -------------------------------------------------------------------------------- 1 | use axoasset::LocalAsset; 2 | use camino::Utf8Path; 3 | 4 | use crate::errors::*; 5 | use crate::site::link; 6 | 7 | pub mod analytics; 8 | 9 | const ARTIFACTS_SCRIPT_SOURCE: &str = include_str!("./artifacts.js"); 10 | 11 | pub fn build_os_script_path(path_prefix: &Option) -> String { 12 | link::generate_relative(path_prefix, "artifacts.js") 13 | } 14 | 15 | pub fn write_os_script(dist_dir: &Utf8Path) -> Result<()> { 16 | LocalAsset::write_new(ARTIFACTS_SCRIPT_SOURCE, dist_dir.join("artifacts.js"))?; 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /src/site/link.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use camino::Utf8PathBuf; 3 | 4 | pub fn generate_relative(path_prefix: &Option, file_name: &str) -> String { 5 | // NOTE: intentionally no leading `/` here because it makes camino add a phantom `/` or `\` 6 | // to the front of the path when getting its components (because it's trying to tell us 7 | // the path is absolute, and we already know that). 8 | let path = if let Some(prefix) = &path_prefix { 9 | format!("{}/{}", prefix, file_name) 10 | } else { 11 | file_name.to_owned() 12 | }; 13 | 14 | sanitize_path(&path, file_name) 15 | } 16 | 17 | /// Generates an absolute path to the end-user hosted version of a file. Returns an option, in case 18 | /// the `url` configuration option wasn't set. 19 | pub fn generate_absolute(config: &Config, file_name: &str) -> Option { 20 | let url = "http://127.0.0.1:7979"; // FIXME: wip 21 | let url = url.trim_end_matches('/'); 22 | let path = if let Some(prefix) = &config.build.path_prefix { 23 | format!("{}/{}", prefix, file_name) 24 | } else { 25 | file_name.to_owned() 26 | }; 27 | 28 | let sanitized_path = sanitize_path(&path, file_name); 29 | Some(format!("{}{}", url, sanitized_path)) 30 | } 31 | 32 | fn sanitize_path(path: &str, file_name: &str) -> String { 33 | // Break the url up into its segments, and precent-encode each part, 34 | // prepending a `/` before each part to make the resulting URL absolute 35 | let path = Utf8PathBuf::from(path); 36 | let mut output = String::new(); 37 | for part in path.components() { 38 | output.push('/'); 39 | output.extend(url::form_urlencoded::byte_serialize( 40 | part.as_str().as_bytes(), 41 | )); 42 | } 43 | 44 | // re-add a trailing slash if original input had it 45 | if file_name.ends_with('/') { 46 | output.push('/'); 47 | } 48 | 49 | output 50 | } 51 | -------------------------------------------------------------------------------- /src/site/markdown/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | mod syntax_highlight; 4 | pub use syntax_highlight::syntax_themes::SyntaxTheme; 5 | pub use syntax_highlight::{dump_syntax_themes, syntax_highlight}; 6 | 7 | use crate::errors::*; 8 | 9 | use ammonia::Builder; 10 | use comrak::adapters::SyntaxHighlighterAdapter; 11 | use comrak::{self, ComrakOptions, ComrakPlugins}; 12 | 13 | pub struct Adapters<'a> { 14 | syntax_theme: &'a SyntaxTheme, 15 | } 16 | impl SyntaxHighlighterAdapter for Adapters<'_> { 17 | fn highlight(&self, lang: Option<&str>, code: &str) -> String { 18 | let highlighted_code = syntax_highlight(lang, code, self.syntax_theme); 19 | 20 | // requires a string to be returned 21 | match highlighted_code { 22 | Ok(code) => code, 23 | Err(_) => String::new(), 24 | } 25 | } 26 | 27 | fn build_pre_tag(&self, _attributes: &HashMap) -> String { 28 | String::new() 29 | } 30 | 31 | fn build_code_tag(&self, _attributes: &HashMap) -> String { 32 | String::new() 33 | } 34 | } 35 | 36 | fn initialize_comrak_options() -> ComrakOptions { 37 | let mut options = ComrakOptions::default(); 38 | 39 | options.extension.strikethrough = true; 40 | options.extension.table = true; 41 | options.extension.autolink = true; 42 | options.extension.tasklist = true; 43 | options.extension.footnotes = true; 44 | options.extension.description_lists = true; 45 | options.render.unsafe_ = true; 46 | 47 | options 48 | } 49 | 50 | pub fn to_html(markdown: &str, syntax_theme: &SyntaxTheme) -> Result { 51 | let options = initialize_comrak_options(); 52 | 53 | let mut plugins = ComrakPlugins::default(); 54 | let adapter = Adapters { syntax_theme }; 55 | plugins.render.codefence_syntax_highlighter = Some(&adapter); 56 | 57 | let unsafe_html = comrak::markdown_to_html_with_plugins(markdown, &options, &plugins); 58 | let safe_html = Builder::new() 59 | .add_generic_attributes(&["style", "class", "id"]) 60 | .clean(&unsafe_html) 61 | .to_string(); 62 | Ok(safe_html) 63 | } 64 | -------------------------------------------------------------------------------- /src/site/markdown/syntax_highlight/syntax_themes.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive( 5 | Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, 6 | )] 7 | pub enum SyntaxTheme { 8 | AgilaClassicOceanicNext, 9 | AgilaCobalt, 10 | AgilaLightSolarized, 11 | AgilaMonokaiExtended, 12 | AgilaNeonMonocyanide, 13 | AgilaOceanicNext, 14 | AgilaOriginOceanicNext, 15 | Base16EightiesDark, 16 | Base16MochaDark, 17 | Base16OceanDark, 18 | Base16OceanLight, 19 | Darkmatter, 20 | Dracula, 21 | GitHubLight, 22 | MaterialTheme, 23 | MaterialThemeDarker, 24 | MaterialThemeLighter, 25 | MaterialThemePalenight, 26 | NightOwl, 27 | OneDark, 28 | } 29 | 30 | impl SyntaxTheme { 31 | pub fn as_str(&self) -> String { 32 | format!("{:?}", &self) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/site/markdown/syntax_highlight/syntax_themes.themedump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axodotdev/oranda/53bdbda1835307ffe11348d4eeb517ccf8a0d2ff/src/site/markdown/syntax_highlight/syntax_themes.themedump -------------------------------------------------------------------------------- /src/site/oranda_theme.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Themes for oranda's output 5 | #[derive( 6 | Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, 7 | )] 8 | #[serde(rename_all = "lowercase")] 9 | pub enum OrandaTheme { 10 | Light, 11 | Dark, 12 | #[serde(alias = "axo_light")] 13 | AxoLight, 14 | #[serde(alias = "axo_dark")] 15 | AxoDark, 16 | Hacker, 17 | Cupcake, 18 | } 19 | 20 | impl Default for OrandaTheme { 21 | fn default() -> Self { 22 | Self::Dark 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/site/page/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use crate::config::Config; 4 | use crate::errors::*; 5 | use crate::site::markdown::{self, SyntaxTheme}; 6 | 7 | use crate::paths::determine_path; 8 | use crate::site::templates::Templates; 9 | use axoasset::SourceFile; 10 | use camino::Utf8PathBuf; 11 | use minijinja::context; 12 | use minijinja::value::Value; 13 | use serde::Serialize; 14 | 15 | pub mod source; 16 | 17 | #[derive(Debug)] 18 | pub struct Page { 19 | pub contents: String, 20 | pub filename: String, 21 | } 22 | 23 | impl Page { 24 | /// Creates a new page by rendering a template, using the provided template name and template context, 25 | /// and using the filename parameter as the output file name. 26 | pub fn new_from_template( 27 | filename: &str, 28 | templates: &Templates, 29 | template_name: &str, 30 | context: &T, 31 | ) -> Result { 32 | let contents = 33 | templates.render_to_string(template_name, Value::from_serializable(context))?; 34 | Ok(Self { 35 | contents, 36 | filename: filename.to_string(), 37 | }) 38 | } 39 | 40 | /// Creates a new page by rendering a Markdown file into the "markdown page" template. Automatically 41 | /// determines the output path based on the path to the input Markdown file, diffing it with the 42 | /// basepath of the project. 43 | pub fn new_from_markdown( 44 | path: &str, 45 | templates: &Templates, 46 | config: &Config, 47 | fail_fast: bool, 48 | ) -> Result { 49 | let body = Self::load_and_render_contents(path, &config.styles.syntax_theme)?; 50 | let contents = if let Some(body) = body { 51 | templates.render_to_string("markdown_page.html", context!(body))? 52 | } else { 53 | if fail_fast { 54 | return Err(OrandaError::PathDoesNotExist { 55 | path: path.to_string(), 56 | }); 57 | } 58 | templates.render_to_string("markdown_page.html", context!())? 59 | }; 60 | // Try diffing with the execution directory in case the user has provided an absolute-ish 61 | // path, in order to obtain the relative-to-dir path segment 62 | let relpath = if let Some(path) = pathdiff::diff_paths(path, std::env::current_dir()?) { 63 | path 64 | } else { 65 | path.into() 66 | }; 67 | Ok(Self { 68 | contents, 69 | filename: relpath.display().to_string(), 70 | }) 71 | } 72 | 73 | /// Combines both above functions by rendering a Markdown file into an arbitrary template. The markdown 74 | /// content itself will be available under the "markdown_content" key in the template itself. 75 | /// 76 | /// If the provided path doesn't exist, it _will_ keep rendering the other content, and output 77 | /// a warning to the console. 78 | pub fn new_from_both( 79 | path: &str, 80 | filename: &str, 81 | templates: &Templates, 82 | template_name: &str, 83 | context: T, 84 | config: &Config, 85 | ) -> Result { 86 | let body = Self::load_and_render_contents(path, &config.styles.syntax_theme)?; 87 | if body.is_none() { 88 | tracing::warn!("{} could not be found on disk!", path); 89 | } 90 | let template = templates.get(template_name)?; 91 | let context = 92 | context!(layout => templates.layout, page => context, markdown_content => body); 93 | let contents = template.render(context)?; 94 | Ok(Self { 95 | contents, 96 | filename: filename.to_string(), 97 | }) 98 | } 99 | 100 | fn load_and_render_contents( 101 | source: &str, 102 | syntax_theme: &SyntaxTheme, 103 | ) -> Result> { 104 | let src_path = Utf8PathBuf::from_path_buf(std::env::current_dir()?) 105 | .expect("Current directory is not UTF-8"); 106 | let path = determine_path(src_path, &None::, source)?; 107 | if let Some(path) = path { 108 | let source = SourceFile::load_local(path)?; 109 | let contents = source.contents(); 110 | Ok(Some(markdown::to_html(contents, syntax_theme)?)) 111 | } else { 112 | Ok(None) 113 | } 114 | } 115 | 116 | pub fn filename(source: &str) -> String { 117 | let file_stem = Path::new(source).file_stem().expect("source file exists"); 118 | format!("{}.html", file_stem.to_string_lossy()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/site/page/source.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{OrandaError, Result}; 2 | use camino::Utf8PathBuf; 3 | use std::path::Path; 4 | 5 | pub fn is_markdown(file: &str) -> bool { 6 | let file_path = Path::new(&file); 7 | match file_path.extension() { 8 | None => false, 9 | Some(ext) => ext.to_string_lossy().to_lowercase() == "md", 10 | } 11 | } 12 | 13 | pub fn get_filename_with_dir(file: &str) -> Result> { 14 | // Try diffing with the execution directory in case the user has provided an absolute-ish 15 | // path, in order to obtain the relative-to-dir path segment 16 | let cur_dir = Utf8PathBuf::from_path_buf(std::env::current_dir()?) 17 | .map_err(|_| OrandaError::Other("Unable to read your current working directory.".into()))?; 18 | let path = if let Some(path) = pathdiff::diff_utf8_paths(file, cur_dir) { 19 | path 20 | } else { 21 | Utf8PathBuf::from(file) 22 | }; 23 | 24 | Ok(Some(path.with_extension(""))) 25 | } 26 | -------------------------------------------------------------------------------- /src/site/rss.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::errors::Result; 3 | use crate::site::changelog::ChangelogContext; 4 | use crate::site::link::generate_absolute; 5 | use rss::extension::atom; 6 | use rss::{CategoryBuilder, Channel, ChannelBuilder, GuidBuilder, Item, ItemBuilder}; 7 | 8 | pub fn generate_rss_feed(context: &ChangelogContext, config: &Config) -> Result { 9 | let category = CategoryBuilder::default() 10 | .name(format!("{} Changelog", &config.project.name)) 11 | .domain(config.project.repository.clone()) 12 | .build(); 13 | 14 | let mut items: Vec = Vec::new(); 15 | for release in &context.releases { 16 | let link = 17 | generate_absolute(config, &format!("changelog/{}", release.version_tag)).unwrap(); 18 | let guid = GuidBuilder::default().permalink(true).value(&link).build(); 19 | let item = ItemBuilder::default() 20 | .title(release.name.clone().unwrap_or(release.version_tag.clone())) 21 | .content(Some(release.body.clone())) 22 | .categories(vec![category.clone()]) 23 | .link(link) 24 | .guid(guid) 25 | .build(); 26 | items.push(item); 27 | } 28 | 29 | let self_link = atom::Link { 30 | rel: "self".to_string(), 31 | href: generate_absolute(config, "changelog.rss").unwrap(), 32 | ..Default::default() 33 | }; 34 | let atom_link = atom::AtomExtensionBuilder::default() 35 | .links(vec![self_link]) 36 | .build(); 37 | let channel = ChannelBuilder::default() 38 | .title(format!("{} Changelog", &config.project.name)) 39 | .description(format!( 40 | "Changelog information for {}", 41 | &config.project.name 42 | )) 43 | .categories(vec![category]) 44 | .items(items) 45 | .link(generate_absolute(config, "changelog").unwrap()) 46 | .atom_ext(atom_link) 47 | .build(); 48 | Ok(channel) 49 | } 50 | -------------------------------------------------------------------------------- /src/site/workspace_index.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::data::workspaces::WorkspaceData; 3 | use crate::errors::{OrandaError, Result}; 4 | use crate::paths::determine_path; 5 | use crate::site::markdown::to_html; 6 | use axoasset::LocalAsset; 7 | use camino::Utf8PathBuf; 8 | use serde::Serialize; 9 | 10 | #[derive(Serialize, Debug)] 11 | pub struct WorkspaceIndexContext { 12 | pub members: Vec, 13 | pub docs_content: Option, 14 | pub preferred_members: Vec, 15 | } 16 | 17 | #[derive(Serialize, Debug)] 18 | pub struct WorkspaceIndexMember { 19 | pub name: String, 20 | pub slug: String, 21 | pub description: Option, 22 | pub repository: Option, 23 | pub logo: Option, 24 | } 25 | 26 | impl WorkspaceIndexContext { 27 | pub fn new(members: &Vec, workspace_config: &Config) -> Result { 28 | let mut index_members = Vec::new(); 29 | let mut index_preferred_members = Vec::new(); 30 | 31 | for member in members { 32 | let logo = if let Some(logo) = &member.config.styles.logo { 33 | Some(Self::find_logo_path(logo, member, workspace_config)?) 34 | } else { 35 | None 36 | }; 37 | let context = WorkspaceIndexMember { 38 | name: member.config.project.name.clone(), 39 | slug: member.slug.clone(), 40 | description: member.config.project.description.clone(), 41 | repository: member.config.project.repository.clone(), 42 | logo, 43 | }; 44 | if workspace_config 45 | .workspace 46 | .preferred_members 47 | .contains(&context.slug) 48 | { 49 | index_preferred_members.push(context); 50 | } else { 51 | index_members.push(context); 52 | } 53 | } 54 | 55 | let mut workspace = Self { 56 | docs_content: None, 57 | members: index_members, 58 | preferred_members: index_preferred_members, 59 | }; 60 | 61 | if let Some(docs_path) = &workspace_config.workspace.docs_path { 62 | let res = LocalAsset::load_string(docs_path)?; 63 | let html = to_html(&res, &workspace_config.styles.syntax_theme)?; 64 | workspace.docs_content = Some(html); 65 | } 66 | 67 | Ok(workspace) 68 | } 69 | 70 | fn find_logo_path( 71 | logo_url: &String, 72 | member: &WorkspaceData, 73 | workspace_config: &Config, 74 | ) -> Result { 75 | let root_path = Utf8PathBuf::from_path_buf(std::env::current_dir()?).unwrap_or_default(); 76 | if logo_url.starts_with("http") { 77 | // Lifted from axoasset. Expose it there? 78 | let mut filename = url::Url::parse(logo_url)? 79 | .path() 80 | .to_string() 81 | .replace('/', "_"); 82 | filename.remove(0); 83 | let mut path = Utf8PathBuf::from( 84 | workspace_config 85 | .build 86 | .path_prefix 87 | .clone() 88 | .unwrap_or_default(), 89 | ); 90 | path.push(&member.slug); 91 | path.push(filename); 92 | Ok(path) 93 | } else if let Some(path) = determine_path(root_path, &Some(&member.slug), logo_url)? { 94 | Ok(path) 95 | } else { 96 | Err(OrandaError::PathDoesNotExist { 97 | path: logo_url.clone(), 98 | }) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /templates/generate/web.yml.j2: -------------------------------------------------------------------------------- 1 | # Workflow to build your docs with oranda (and mdbook) 2 | # and deploy them to Github Pages 3 | name: Web 4 | 5 | # We're going to push to the gh-pages branch, so we need that permission 6 | permissions: 7 | contents: write 8 | 9 | # What situations do we want to build docs in? 10 | # All of these work independently and can be removed / commented out 11 | # if you don't want oranda/mdbook running in that situation 12 | on: 13 | # Check that a PR didn't break docs! 14 | # 15 | # Note that the "Deploy to Github Pages" step won't run in this mode, 16 | # so this won't have any side-effects. But it will tell you if a PR 17 | # completely broke oranda/mdbook. Sadly we don't provide previews (yet)! 18 | pull_request: 19 | 20 | # Whenever something gets pushed to main, update the docs! 21 | # This is great for getting docs changes live without cutting a full release. 22 | # 23 | # Note that if you're using cargo-dist, this will "race" the Release workflow 24 | # that actually builds the Github Release that oranda tries to read (and 25 | # this will almost certainly complete first). As a result you will publish 26 | # docs for the latest commit but the oranda landing page won't know about 27 | # the latest release. The workflow_run trigger below will properly wait for 28 | # cargo-dist, and so this half-published state will only last for ~10 minutes. 29 | # 30 | # If you only want docs to update with releases, disable this, or change it to 31 | # a "release" branch. You can, of course, also manually trigger a workflow run 32 | # when you want the docs to update. 33 | push: 34 | branches: 35 | - main 36 | 37 | # Whenever a workflow called "Release" completes, update the docs! 38 | # 39 | # If you're using cargo-dist, this is recommended, as it will ensure that 40 | # oranda always sees the latest release right when it's available. Note 41 | # however that Github's UI is wonky when you use workflow_run, and won't 42 | # show this workflow as part of any commit. You have to go to the "actions" 43 | # tab for your repo to see this one running (the gh-pages deploy will also 44 | # only show up there). 45 | workflow_run: 46 | workflows: [ "Release" ] 47 | types: 48 | - completed 49 | 50 | # Alright, let's do it! 51 | jobs: 52 | web: 53 | name: Build and deploy site and docs 54 | runs-on: ubuntu-latest 55 | steps: 56 | # Setup 57 | - uses: actions/checkout@v3 58 | with: 59 | fetch-depth: 0 60 | - uses: dtolnay/rust-toolchain@stable 61 | - uses: swatinem/rust-cache@v2 62 | 63 | # If you use any mdbook plugins, here's the place to install them! 64 | 65 | # Install and run oranda (and mdbook)! 66 | # 67 | # This will write all output to ./public/ (including copying mdbook's output to there). 68 | - name: Install and run oranda 69 | run: | 70 | {{%- if use_latest_oranda %}} 71 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/latest/download/oranda-installer.sh | sh 72 | {{%- else %}} 73 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/download/v0.6.1/oranda-installer.sh | sh 74 | {{%- endif %}} 75 | {{%- if site_dir %}} 76 | cd {{{ site_dir }}} 77 | {{%- endif %}} 78 | oranda build 79 | {{% if check_links %}} 80 | - name: Prepare HTML for link checking 81 | # untitaker/hyperlink supports no site prefixes, move entire site into 82 | # a subfolder 83 | run: mkdir /tmp/public/ && cp -R public /tmp/public/oranda 84 | 85 | - name: Check HTML for broken internal links 86 | uses: untitaker/hyperlink@0.1.29 87 | with: 88 | args: /tmp/public/ 89 | {{% endif %}} 90 | # Deploy to our gh-pages branch (creating it if it doesn't exist). 91 | # The "public" dir that oranda made above will become the root dir 92 | # of this branch. 93 | # 94 | # Note that once the gh-pages branch exists, you must 95 | # go into repo's settings > pages and set "deploy from branch: gh-pages". 96 | # The other defaults work fine. 97 | - name: Deploy to Github Pages 98 | uses: JamesIves/github-pages-deploy-action@v4.4.1 99 | # ONLY if we're on main (so no PRs or feature branches allowed!) 100 | if: ${{ github.ref == 'refs/heads/main' }} 101 | with: 102 | branch: gh-pages 103 | # Gotta tell the action where to find oranda's output 104 | folder: public 105 | token: ${{ secrets.GITHUB_TOKEN }} 106 | single-commit: true 107 | -------------------------------------------------------------------------------- /templates/site/artifacts.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |
4 |
5 | {% for installer in page.release.artifacts.installers | sort(attribute="label") %} 6 | {% if installer.display != "Hidden" and installer.method.type == "Run" %} 7 | {% set release = page.release %} 8 |
9 |

{{ installer.label }}

10 | {% include "includes/installer_run.html" %} 11 |
12 | {% endif %} 13 | {% endfor %} 14 |
15 |
16 |

Downloads

17 | 18 | 19 | 20 | 21 | 22 | {% if page.has_checksum_files %} 23 | 24 | {% endif %} 25 | 26 | {% for f in page.downloadable_files %} 27 | {% set file = f[1] %} 28 | {% set platforms = f[2] %} 29 | 30 | 31 | 38 | {% if page.has_checksum_files %} 39 | {% if file.checksum_file %} 40 | {% set checksum = page.release.artifacts.files[file.checksum_file] %} 41 | 42 | {% endif %} 43 | {% endif %} 44 | 45 | {% endfor %} 46 | 47 |
FilePlatformChecksum
{{ file.name }} 32 | {% for platform in platforms %} 33 | {% if platform %} 34 | {{ platform }}{% if not loop.first %}, {% endif %} 35 | {% endif %} 36 | {% endfor %} 37 | checksum
48 |
49 |
50 | {% endblock %} 51 | 52 | {% block os_script %} 53 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /templates/site/changelog_index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |
4 |

5 | Releases 6 | {% if page.has_rss_feed %} 7 | {% include "icons/rss.html" %} 8 | {% endif %} 9 |

10 |
11 | 34 | 35 |
36 | {% for release in page.releases %} 37 | {% set is_page = false %} 38 | {% include "includes/changelog_release.html" %} 39 | {% endfor %} 40 |
41 |
42 |
43 | {% endblock %} 44 | 45 | {% block os_script %} 46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /templates/site/changelog_single.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |
4 |

{% if page.release.name %}{{ page.release.name }}{% else %}{{ page.release.version_tag }}{% endif %}

5 |
6 | {% set is_page = true %} 7 | {% set release = page.release %} 8 | {% include "includes/changelog_release.html" %} 9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/site/funding.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |
4 |

Help fund this project!

5 | 23 | {% if page.docs_content %} 24 | {{ page.docs_content }} 25 | {% endif %} 26 | 42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/site/icons/copy.html.j2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/site/icons/date.html.j2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/site/icons/github.html.j2: -------------------------------------------------------------------------------- 1 | GitHub -------------------------------------------------------------------------------- /templates/site/icons/kofi.html.j2: -------------------------------------------------------------------------------- 1 | Ko-fi -------------------------------------------------------------------------------- /templates/site/icons/liberapay.html.j2: -------------------------------------------------------------------------------- 1 | Liberapay -------------------------------------------------------------------------------- /templates/site/icons/opencollective.html.j2: -------------------------------------------------------------------------------- 1 | Open Collective -------------------------------------------------------------------------------- /templates/site/icons/patreon.html.j2: -------------------------------------------------------------------------------- 1 | Patreon -------------------------------------------------------------------------------- /templates/site/icons/rss.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /templates/site/icons/tag.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /templates/site/icons/tidelift.html.j2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/site/icons/web.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /templates/site/includes/changelog_release.html.j2: -------------------------------------------------------------------------------- 1 |
2 | {% if not is_page %}

3 | 4 | {% if release.name %} 5 | {{ release.name }} 6 | {% else %} 7 | {{ release.version_tag }} 8 | {% endif %} 9 | 10 |

{% endif %} 11 |
12 | 13 | {% include "icons/tag.html" %} 14 | {{ release.version_tag }} 15 | 16 | 17 | {% if release.formatted_date %} 18 | {% include "icons/date.html" %} 19 | {{ release.formatted_date }} 20 | {% endif %} 21 | 22 |
23 |
24 | {{ release.body }} 25 |
26 |
27 | -------------------------------------------------------------------------------- /templates/site/includes/install_widget.html.j2: -------------------------------------------------------------------------------- 1 | {% set artifacts = page.artifacts %} 2 | {# Do we have only one platform? If so, simplify a bunch of stuff #} 3 | {% set simple_platforms = artifacts.platforms_with_downloads|length == 1 %} 4 | 5 |
6 |
7 |

Install {{ artifacts.tag }}

8 | {% if artifacts.formatted_date %} 9 |
Published on {{ artifacts.formatted_date }}
10 | {% endif %} 11 | 12 |
    13 | {% for platform in artifacts.platforms_with_downloads %} 14 |
  • 15 | {% if platform.installers | length > 1 %} 16 |
      17 | {% for i in platform.installers %} 18 | {% set installer = artifacts.release.artifacts.installers[i] %} 19 | {# Select the first tab #} 20 |
    • 21 | {{ installer.label }} 22 | 23 | {% if installer.app_name %} 24 | ({{ installer.app_name }}) 25 | {% endif %} 26 |
    • 27 | {% endfor %} 28 |
    29 | {% endif %} 30 | 31 |
      32 | {% for i in platform.installers %} 33 | {% set installer = artifacts.release.artifacts.installers[i] %} 34 |
    • 35 | {% if installer.method.type == "Run" %} 36 | {% set release = artifacts.release %} 37 | {% include "includes/installer_run.html" %} 38 | {% endif %} 39 | 40 | {% if installer.method.type == "Download" %} 41 | {% set file = artifacts.release.artifacts.files[installer.method.file] %} 42 | 50 | {% endif %} 51 |
    • 52 | {% endfor %} 53 |
    54 |
  • 55 | {% endfor %} 56 |
57 |
58 | 59 | {% if not simple_platforms %} 60 | 63 | 66 | {% endif %} 67 | 68 | 69 | {# Get the target from the first platform #} 70 | {% set first_target = artifacts.platforms_with_downloads | first | attr("target") | first %} 71 |
72 | View all installation options 73 | {% if simple_platforms %} 74 | {% if first_target and first_target != "all" %} 75 |
Platform: {{ artifacts.platforms_with_downloads | first | attr("display_name") }}
76 | {% endif %} 77 | {% else %} 78 | 86 | {% endif %} 87 |
88 |
89 | 90 | View all installation options 91 | -------------------------------------------------------------------------------- /templates/site/includes/installer_run.html.j2: -------------------------------------------------------------------------------- 1 |
2 | {{ installer.method.run_hint | syntax_highlight("sh", "") }} 3 | 6 | {# Grab the installer source link, if we can find it #} 7 | {% if installer.method.file %} 8 | {% set file = release.artifacts.files[installer.method.file] %} 9 | {% if file.view_path %} 10 | {% set url = file.view_path | generate_link(layout.path_prefix) %} 11 | {% else %} 12 | {% set url = file.download_url %} 13 | {% endif %} 14 | Source 15 | {% endif %} 16 |
17 | -------------------------------------------------------------------------------- /templates/site/includes/nav.html.j2: -------------------------------------------------------------------------------- 1 | {% if layout.has_nav %} 2 | 29 | {% endif %} 30 | -------------------------------------------------------------------------------- /templates/site/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 | {% if page.artifacts and page.artifacts.downloadable_files | length != 0 %} 4 | {% include "includes/install_widget.html" %} 5 | {% endif %} 6 | {% if markdown_content %} 7 | {{ markdown_content }} 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block os_script %} 12 | {% if page.artifacts %} 13 | 14 | {% endif %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/site/layout.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{{ layout.project_name }}{% endblock %} 5 | {% if layout.homepage %} 6 | 7 | {% endif %} 8 | {% if layout.favicon_url %} 9 | 10 | {% endif %} 11 | 12 | 13 | {% if layout.description %} 14 | 15 | 16 | {% endif %} 17 | 18 | 19 | {% if layout.social.image %} 20 | 21 | 22 | {% endif %} 23 | {% if layout.social.image_alt %} 24 | 25 | {% endif %} 26 | {% if layout.social.twitter_account %} 27 | 28 | 29 | {% endif %} 30 | 31 | 32 | {% if layout.has_additional_css %} 33 | 34 | {% endif %} 35 | {% block head %}{% endblock %} 36 | 37 | 38 |
39 |
40 | {% if layout.repository %} 41 | 47 | {% endif %} 48 | 49 |
50 |
51 | {% if layout.logo %} 52 | 53 | {% endif %} 54 |

{{ layout.project_name }}

55 | {% include "includes/nav.html" %} 56 |
57 | 58 | {% block content %}{% endblock %} 59 |
60 |
61 | 62 |
63 | {% if layout.repository %} 64 | 65 | {% endif %} 66 | 67 | {{ layout.project_name }}{% if layout.license %}, {{ layout.license }}{% endif %} 68 | 69 |
70 |
71 | 72 | {% if layout.analytics.snippet %} 73 | {{ layout.analytics.snippet }} 74 | {% endif %} 75 | {% if layout.analytics.google_script %} 76 | {{ layout.analytics.google_script }} 77 | {% endif %} 78 | 79 | {% block os_script %}{% endblock %} 80 | 81 | 82 | -------------------------------------------------------------------------------- /templates/site/markdown_page.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 | {% if page.body %} 4 | {{ page.body }} 5 | {% endif %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /templates/site/workspace_index/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "workspace_index/layout.html" %} 2 | 3 | {% block content %} 4 |
    5 | {% for preferred in page.preferred_members %} 6 |
  • 7 |
    8 |
    9 |

    {{ preferred.name }}

    10 | {% if preferred.description %} 11 |
    {{ preferred.description }}
    12 | {% endif %} 13 |
    14 |
    15 | 21 |
  • 22 | {% endfor %} 23 |
24 | 25 | {% if page.docs_content %} 26 | {{ page.docs_content|safe }} 27 | {% endif %} 28 | 29 |
    30 | {% for member in page.members %} 31 |
  • 32 |
    33 |
    34 |

    {{ member.name }}

    35 | {% if member.description %} 36 |
    {{ member.description }}
    37 | {% endif %} 38 |
    39 | {% if member.logo %} 40 | 41 | {% endif %} 42 |
    43 | 49 |
  • 50 | {% endfor %} 51 |
52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /templates/site/workspace_index/layout.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ layout.project_name }} 5 | 6 | 7 | 8 | {% if layout.has_additional_css %} 9 | 10 | {% endif %} 11 | {% block head %}{% endblock %} 12 | 13 | 14 |
15 |
16 |
17 |
18 |

{{ layout.project_name }}

19 |
20 | {% block content %}{% endblock %} 21 |
22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/autodetect/fixtures/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod project_config; 2 | -------------------------------------------------------------------------------- /tests/autodetect/fixtures/project_config.rs: -------------------------------------------------------------------------------- 1 | pub fn cargo_toml() -> &'static str { 2 | r#" 3 | [package] 4 | "name" = "axo" 5 | "version" = "0.0.0" 6 | "description" = "blublublub" 7 | "respository" = "https://github.com/axodotdev/not-a-real-project" 8 | "# 9 | } 10 | 11 | pub fn workspace_member_toml() -> &'static str { 12 | r#" 13 | [package] 14 | "name" = "axo2" 15 | "version" = "0.0.0" 16 | "description" = "aaaaahhhhh" 17 | "respository" = "https://github.com/axodotdev/not-a-real-project-too" 18 | "# 19 | } 20 | 21 | pub fn main_rs() -> &'static str { 22 | r#" 23 | fn main() { 24 | println!("hello world!); 25 | } 26 | "# 27 | } 28 | 29 | pub fn workspace_toml() -> &'static str { 30 | r#" 31 | [workspace] 32 | members = ["axo", "axo2"] 33 | "# 34 | } 35 | 36 | pub fn package_json() -> &'static str { 37 | r#" 38 | { 39 | "name": "axo", 40 | "version": "0.1.0", 41 | "description": ">o_o<", 42 | "bin": { 43 | "axo": "src/main.js" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/axodotdev/not-a-real-project" 48 | } 49 | } 50 | "# 51 | } 52 | -------------------------------------------------------------------------------- /tests/integration/fixtures/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod oranda_config; 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/oranda_config.rs: -------------------------------------------------------------------------------- 1 | use assert_fs::prelude::*; 2 | use assert_fs::TempDir; 3 | use camino::Utf8PathBuf; 4 | 5 | use oranda::config::Config; 6 | 7 | pub fn from_json(json: serde_json::Value, dir: &mut TempDir) -> Config { 8 | let c = dir.child("oranda.json"); 9 | c.write_str(&json.to_string()) 10 | .expect("Unable to write oranda.json"); 11 | let mut config = Config::build(&Utf8PathBuf::from_path_buf(c.path().to_path_buf()).unwrap()) 12 | .expect("Unable to generate config"); 13 | config.build.dist_dir = dir.path().display().to_string(); 14 | // Override repository, except if it's non-standard 15 | if config 16 | .project 17 | .repository 18 | .as_ref() 19 | .is_some_and(|repo| repo == "https://github.com/axodotdev/oranda") 20 | { 21 | config.project.repository = Some("https://github.com/oranda-gallery/oranda".to_string()); 22 | } 23 | config 24 | } 25 | -------------------------------------------------------------------------------- /tests/integration_gallery/errors.rs: -------------------------------------------------------------------------------- 1 | pub type Result = std::result::Result; 2 | -------------------------------------------------------------------------------- /tests/integration_gallery/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | mod command; 4 | mod errors; 5 | mod oranda_impl; 6 | mod repo; 7 | 8 | pub use errors::*; 9 | pub use oranda_impl::*; 10 | use std::collections::BTreeSet; 11 | 12 | use self::command::CommandInfo; 13 | 14 | /// Taken from cargo-insta to avoid copy-paste errors 15 | /// 16 | /// Gets the ~name of the function running this macro 17 | #[macro_export] 18 | macro_rules! _function_name { 19 | () => {{ 20 | fn f() {} 21 | fn type_name_of_val(_: T) -> &'static str { 22 | std::any::type_name::() 23 | } 24 | let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or(""); 25 | while let Some(rest) = name.strip_suffix("::{{closure}}") { 26 | name = rest; 27 | } 28 | name.split("::").last().unwrap_or(name) 29 | }}; 30 | } 31 | 32 | #[test] 33 | fn gal_axolotlsay() -> Result<()> { 34 | let test_name = _function_name!(); 35 | AXOLOTLSAY.run_test(|ctx| { 36 | let res = ctx.oranda_build(test_name)?; 37 | res.check_all()?; 38 | Ok(()) 39 | }) 40 | } 41 | 42 | #[test] 43 | fn gal_akaikatana() -> Result<()> { 44 | let test_name = _function_name!(); 45 | AKAIKATANA_REPACK.run_test(|ctx| { 46 | let res = ctx.oranda_build(test_name)?; 47 | res.check_all()?; 48 | Ok(()) 49 | }) 50 | } 51 | 52 | #[test] 53 | fn gal_oranda() -> Result<()> { 54 | let test_name = _function_name!(); 55 | ORANDA.run_test(|ctx| { 56 | let res = ctx.oranda_build(test_name)?; 57 | res.check_all()?; 58 | Ok(()) 59 | }) 60 | } 61 | 62 | #[test] 63 | fn gal_oranda_empty() -> Result<()> { 64 | let test_name = _function_name!(); 65 | EMPTY_TEST.run_test(|ctx| { 66 | let res = ctx.oranda_build(test_name)?; 67 | res.check_all()?; 68 | Ok(()) 69 | }) 70 | } 71 | 72 | #[test] 73 | fn gal_oranda_inference() -> Result<()> { 74 | let test_name = _function_name!(); 75 | INFERENCE_TEST.run_test(|ctx| { 76 | let res = ctx.oranda_build(test_name)?; 77 | res.check_all()?; 78 | Ok(()) 79 | }) 80 | } 81 | 82 | #[test] 83 | fn gal_workspace() -> Result<()> { 84 | let test_name = _function_name!(); 85 | let mut num_iters = 0; 86 | let mut should_sleep = true; 87 | 88 | loop { 89 | // Bail out and sleep for a while if not all the other tests are written 90 | AXOLOTLSAY.run_test(|ctx| { 91 | num_iters += 1; 92 | // Go to the root 93 | CommandInfo::set_working_dir(ctx.tools.temp_root()); 94 | 95 | // Load the oranda-workspace.json and check if all tests are done 96 | let json = ctx.tools.load_oranda_workspace_json()?; 97 | let members = json.workspace.as_ref().unwrap().members.as_ref().unwrap(); 98 | let members_set = members 99 | .iter() 100 | .map(|m| m.slug.clone()) 101 | .collect::>(); 102 | let required_set = vec![ 103 | "gal_axolotlsay".to_owned(), 104 | "gal_akaikatana".to_owned(), 105 | "gal_oranda".to_owned(), 106 | "gal_oranda_inference".to_owned(), 107 | "gal_oranda_empty".to_owned(), 108 | ] 109 | .into_iter() 110 | .collect::>(); 111 | 112 | if !required_set.is_subset(&members_set) { 113 | // Sleep 114 | return Ok(()); 115 | } 116 | should_sleep = false; 117 | 118 | let _res = ctx.oranda_build(test_name)?; 119 | // Currently not snapshotting because enormous, but maybe do index.html..? 120 | Ok(()) 121 | })?; 122 | 123 | if should_sleep { 124 | if num_iters < 30 { 125 | std::thread::sleep(std::time::Duration::from_secs(1)); 126 | } else { 127 | panic!("gal_workspace timed out waiting for other tests"); 128 | } 129 | } else { 130 | return Ok(()); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod utils; 2 | 3 | mod autodetect; 4 | mod integration; 5 | mod integration_gallery; 6 | -------------------------------------------------------------------------------- /tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod snapshots; 2 | pub mod tokio_utils; 3 | -------------------------------------------------------------------------------- /tests/utils/snapshots.rs: -------------------------------------------------------------------------------- 1 | use camino::Utf8Path; 2 | 3 | /// root dir of oranda so we can set the tests/snapshots/ dir reliably 4 | const ROOT_DIR: &str = env!("CARGO_MANIFEST_DIR"); 5 | 6 | pub fn snapshot_settings() -> insta::Settings { 7 | let mut settings = insta::Settings::clone_current(); 8 | let snapshot_dir = Utf8Path::new(ROOT_DIR).join("tests").join("snapshots"); 9 | settings.set_snapshot_path(snapshot_dir); 10 | settings.set_prepend_module_to_snapshot(false); 11 | settings 12 | } 13 | -------------------------------------------------------------------------------- /tests/utils/tokio_utils.rs: -------------------------------------------------------------------------------- 1 | lazy_static::lazy_static! { 2 | pub static ref TEST_RUNTIME: tokio::runtime::Runtime = { 3 | tokio::runtime::Builder::new_multi_thread() 4 | .worker_threads(1) 5 | .max_blocking_threads(128) 6 | .enable_all() 7 | .build() 8 | .expect("Initializing tokio runtime failed") 9 | }; 10 | } 11 | --------------------------------------------------------------------------------