diff --git a/website/.envrc b/website/.envrc new file mode 100644 index 0000000..4cc700c --- /dev/null +++ b/website/.envrc @@ -0,0 +1,2 @@ +# use_nix +use_flake diff --git a/website/.gitattributes b/website/.gitattributes new file mode 100644 index 0000000..0cb064a --- /dev/null +++ b/website/.gitattributes @@ -0,0 +1 @@ +*.zig text=auto eol=lf diff --git a/website/.github/workflows/pr.yml b/website/.github/workflows/pr.yml new file mode 100644 index 0000000..7d6289d --- /dev/null +++ b/website/.github/workflows/pr.yml @@ -0,0 +1,38 @@ +name: Render PR Preview + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Setup Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: master + - name: Render website + run: | + zig build + + - name: Deploy + uses: easingthemes/ssh-deploy@main + with: + SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }} + ARGS: "-vzrli" + SOURCE: "zig-out/" + REMOTE_HOST: ${{ secrets.DEPLOY_HOST }} + REMOTE_USER: ${{ secrets.DEPLOY_USER }} + REMOTE_PORT: ${{ secrets.DEPLOY_PORT }} + TARGET: "./staging/pulls/${{ github.event.number }}" + + - uses: mshick/add-pr-comment@v1 + with: + message: | + Heya! + You can check out a preview of your PR at [staging.microzig.tech/pulls/${{ github.event.number }}](https://staging.microzig.tech/pulls/${{ github.event.number }}/)! + repo-token: ${{ secrets.GITHUB_TOKEN }} + repo-token-user-login: 'github-actions[bot]' # The user.login for temporary GitHub tokens + allow-repeats: false # This is the default diff --git a/website/.github/workflows/website.yml b/website/.github/workflows/website.yml new file mode 100644 index 0000000..daa3250 --- /dev/null +++ b/website/.github/workflows/website.yml @@ -0,0 +1,36 @@ +name: Render Website + +on: + push: + branches: + - 'main' + - 'master' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + + - name: Setup Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: master + + - name: Render website + run: | + zig build + + - name: Deploy + uses: easingthemes/ssh-deploy@main + with: + SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }} + ARGS: "-vzrli" + SOURCE: "zig-out/" + REMOTE_HOST: ${{ secrets.DEPLOY_HOST }} + REMOTE_USER: ${{ secrets.DEPLOY_USER }} + REMOTE_PORT: ${{ secrets.DEPLOY_PORT }} + TARGET: "./live" diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000..06606ca --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,4 @@ +zig-cache/ +render/ +zig-out/ +.direnv/ diff --git a/website/.gitmodules b/website/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/website/LICENCE b/website/LICENCE new file mode 100644 index 0000000..16b66a9 --- /dev/null +++ b/website/LICENCE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Felix "xq" Queißner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..3649136 --- /dev/null +++ b/website/README.md @@ -0,0 +1,34 @@ +# Zig Embedded Group - Website and Articles + +This project both contains the contents and the generation of the ZEG website. + +## Folder Structure + +``` +. +├── build.zig +├── deps Contains submodule dependencies +│ └── … +├── LICENCE +├── README.md +├── render Not included in the repo, will contain the rendered website +│ └── … +├── src Source of the website generator and other tools +│ └── main.zig +└── website Contains the raw input data for the website + ├── articles Contains all articles in the format `YYYY-MM-dd - ${TITLE}.md` + │ └── … + ├── img Contains the images used on the website. + │ └── … + ├── index.md Index page of the website + └── tutorials Contains the raw tutorial files + └── … +``` + +## Markdown + +The website uses basic markdown that allows GFM style tables and also supports *some* placeholders: + +- `` will insert a table of contents if alone in a single line. The ToC will be rendered in the same depth as the next heading, so everything higher in the hierarchy will be ignored. +- `` renders a list of all available articles +- `` renders a list of the 10 latest articles diff --git a/website/WIP.md b/website/WIP.md new file mode 100644 index 0000000..1f26e46 --- /dev/null +++ b/website/WIP.md @@ -0,0 +1,42 @@ +# Planned/TODO articles + +## Work in progress + +These have either incomplete content on the website or a branch where they are being written. + +- Tutorials + - 01-embedded-basics + - 02-embedded-programming +- Articles + - NONE + +## TODO + +Things that should be written eventually. + +- Tutorials + - Getting started with: + - Arduino/AVR + - LPC1768 + - NRF52 + - Hardware: nRF52840 Dongle + - https://www.nordicsemi.com/Software-and-tools/Development-Kits/nRF52840-Dongle/GetStarted + - https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Connect-for-desktop + - https://infocenter.nordicsemi.com/index.jsp?topic=%2Fug_nc_programmer%2FUG%2Fnrf_connect_programmer%2Fncp_programming_dongle.html + - Raspberry Pi Pico + - STM32 + - What device to chose? + - Introduction to HAL 9001 +- Articles + - Creating your own JTAG debugger + - Black Magic Probe + - https://paramaggarwal.medium.com/converting-an-stm32f103-board-to-a-black-magic-probe-c013cf2cc38c + - zCOM, a network stack for embedded devices + +## Ideas + +Ideas for things to write that would be great to do eventually or projects to write up about. + +- Tutorials +- Articles + - Make your own keyboard with zig (and replace qmk) diff --git a/website/build.zig b/website/build.zig new file mode 100644 index 0000000..f1d8096 --- /dev/null +++ b/website/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); +const zine = @import("zine"); + +pub fn build(b: *std.Build) !void { + // zine.scriptyReferenceDocs(b, "content/documentation/scripty/index.md"); + try zine.addWebsite(b, .{ + .layouts_dir_path = "layouts", + .content_dir_path = "content", + .static_dir_path = "static", + .site = .{ + .base_url = "https://microzig.tech", + .title = "Zig Embedded Group", + }, + }); +} diff --git a/website/build.zig.zon b/website/build.zig.zon new file mode 100644 index 0000000..4ec7868 --- /dev/null +++ b/website/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = "microzig.tech", + .version = "0.2.0", + .paths = .{"."}, + .dependencies = .{ + .zine = .{ + .url = "https://github.com/kristoff-it/zine/archive/03f80646b83cadb2e693ff5d97445d3e16c8e222.tar.gz", + .hash = "1220e3e4938edf652776349c45b3bb58774d540a050034488ff8dab7dbe410cc2977", + }, + }, +} diff --git a/website/content/articles/.keep b/website/content/articles/.keep new file mode 100644 index 0000000..e69de29 diff --git a/website/content/getting-started.md b/website/content/getting-started.md new file mode 100644 index 0000000..d9edbd1 --- /dev/null +++ b/website/content/getting-started.md @@ -0,0 +1,11 @@ +--- +{ + "title": "Home", + "date": "2020-07-06T00:00:00", + "author": "Felix Queißner", + "draft": false, + "layout": "getting-started.html", + "tags": [] +} +--- +Dummy, full text is implemented in the HTML file for now. diff --git a/website/content/index.md b/website/content/index.md new file mode 100644 index 0000000..af1d6e7 --- /dev/null +++ b/website/content/index.md @@ -0,0 +1,11 @@ +--- +{ + "title": "Home", + "date": "2020-07-06T00:00:00", + "author": "Felix Queißner", + "draft": false, + "layout": "index.html", + "tags": [] +} +--- +Dummy, full text is implemented in the HTML file for now. diff --git a/website/content/tutorials/01-embedded-basics.md b/website/content/tutorials/01-embedded-basics.md new file mode 100644 index 0000000..e1a246a --- /dev/null +++ b/website/content/tutorials/01-embedded-basics.md @@ -0,0 +1,46 @@ +--- +{ + "title": "Embedded Basics", + "date": "2020-07-06T00:00:00", + "author": "Felix Queißner", + "draft": false, + "layout": "tutorial.html", + "tags": [] +} +--- +# Embedded Basics + +In this tutorial, you'll learn the absolute basics of the embedded world. If +you have already experience with embedded systems and/or electronics, this +chapter probably doesn't provide anything new to you. + +## Prerequisites + +None! This is your entry point into the embedded world! + +## Contents + + + +## What are embedded systems? + +Wikipedia does a good job defining embedded systems with this opener: + +> An embedded system is a computer system—a combination of a computer processor, computer memory, and input/output peripheral devices—that has a dedicated function within a larger mechanical or electrical system. + +So at the end of the day, if you are adding any sort of computation to some object who's main purpose is not being a computer, it's an embedded system. + +Some examples of Embedded systems: + +- cars +- industrial control systems +- mars rovers +- digital thermometer + +### Real time + +An important characteristic that's often required for an embedded system is "real time". +This is simply the ability for the system to respond to an input within a hard deadline, Eg. automatic breaks for a car. +A general operating system like Linux is not suitable for these applications because it uses time sharing when scheduling tasks/programs, and unreliably responds to important signals. + +## MORE COMING SOON \ No newline at end of file diff --git a/website/content/tutorials/02-embedded-programming.md b/website/content/tutorials/02-embedded-programming.md new file mode 100644 index 0000000..86e04e1 --- /dev/null +++ b/website/content/tutorials/02-embedded-programming.md @@ -0,0 +1,151 @@ +--- +{ + "title": "Embedded Basics", + "date": "2020-07-06T00:00:00", + "author": "Felix Queißner", + "draft": false, + "layout": "tutorial.html", + "tags": [] +} +--- +# Embedded Programming + +In this tutorial, you'll learn the ways of the embedded programmer and how to master your MCU. + +## Prerequisites + +- [Embedded Basics](01-embedded-basics.htm) + +## Contents + + + +## Differences to desktop programming + +The embedded world is quite different compared to the convenient environment of desktop computers. You are not +protected by an operating system, you don't have convenient APIs for file access or even an allocator. You are as +close to the metal as it can be. + +Most of your programs don't even have a real entry point, as an embedded system is usually "started" by triggering +the *RESET* interrupt (which is some kind of callback). Desktop programs also have a protected memory area, where the +address `0x00…00` is usually invalid and cannot be accessed. On embedded systems though, this address is either where +some relevant data is or even more important, your entry point. You usually also have very little RAM available, sometimes +even less than 2048 byte. This means that thinking about memory usage is very important. + +## Inventory of an embedded programmer + +Every embedded programmer requires some materials to get their work done efficiently. + +First of all, the *SOC datasheet*. It contains all relevant information about the SOC/chip you are using, which functions each pin of the package has, where your RAM and flash is located in the memory map and so on. You *will* learn to navigate this document very quickly, as it's the main reference for everything you do. + +Second, you need the schematics of the device you want to program. You usually can obtain them from the manufacturer of your development board (assuming you are using one), by the vendor of the device you're hacking (if you are lucky enough) or by reverse engineering the device you have at hands (consider this the *hard mode* of embedded development). Reading a schematic is crucial to get your device do what you want, and you can learn a bit about this in the [Embedded Basics](01-embedded-basics.htm) tutorial. + +These two documents are the ones you *definitly* need and it's near-impossible to work without them. But usually you need more documents than this: +Datasheets for all the peripherial devices like displays, display controllers, motor controllers, expander chips and so on. Another document that helps a lot is the CPU datasheet for the core of your SOC. This document contains a precise description of the startup procedure of your system, what instructions are available, how the interrupts work in detail and similar topics. + +And last, but not least: You need a [text editor of your choice](https://en.wikipedia.org/wiki/List_of_text_editors), a toolchain which consists of a [compiler](https://ziglang.org/) and [binutils](https://www.gnu.org/software/binutils/), and a programmer/flashing tool for your SOC, so you can load your program. + +## The startup procedure + +So to get an embedded program up and running, we first need to check out the *memory map* in the datasheet. These usually look like this: + +![Memory Map of LPC1768](memory-map.png) + +Here you can see that the memory contains continuous flash memory (*On-chip [non-volatile memory](https://en.wikipedia.org/wiki/Non-volatile_memory)*), two sections of SRAM (*On-chip [SRAM](https://en.wikipedia.org/wiki/Static_random-access_memory)*), some *Boot ROM*, and peripherials. + +This memory map tells us how to design the [linker script](https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html#SEC6) and how to lay out our sections (`.text`, `.data`, …). As sections are quite complex topic for themselves, they [will be explained later](#text-data-and-other-curious-sections). For now, we only need to know that `.text` is all of our code (this is where our functions live), `.rodata` is pre-initialized immutable data, `.data` is the pre-initialized mutable data and `.bss` is zero-initialized mutable data. + +Note the difference between `.rodata` and `.data`? We can safely put `.rodata` into a flash section in memory, as we can access flash memory the same as we can access RAM. `.data` must be in RAM though, as we need to be able to change it. But there is one huge problem: +The RAM will contain garbage after startup. Our variables aren't initialized and thus also contain garbage. So the first thing to do in a microcontroller software is somehow restoring the data in RAM and initialize both known and zero-initialized data. + +To do this, we first have to tell the compiler to store our pre-initialized data somewhere in flash memory, so we can copy it into RAM at startup. This is done via the linker script: + +```ld +/* This section declares two memory regions: + * flash: 512k of non-writeable memory at position 0, + * ram: 32k of writeable memory at position 256M + */ +MEMORY +{ + flash (rx!w) : ORIGIN = 0x00000000, LENGTH = 512k + ram (rwx) : ORIGIN = 0x10000000, LENGTH = 32k +} + +/* This section declares rules where to put different + * symbols (functions and variables) in memory + */ +SECTIONS +{ + /* this is the output section ".text" which will be located in flash */ + .text : + { + /* include all things that are functions or have linksection(".text") */ + *(.text) + } >flash /* this means that this section is layed out in "flash" MEMORY */ + + /* assign the current location to a symbol called code_end */ + code_end = .; + + /* this is the output section ".data" which will be located in RAM. + * AT(X) means that this section is *loaded* at position X in memory, in this + * case: after our code in flash. + */ + .data : AT (code_end) + { + /* create a symbol called data_begin at the start of the data section */ + data_begin = .; + + /* include all things that are variables or have linksection(".data") */ + *(.data) + + /* same as above, but now at the end of the section */ + data_end = .; + } >ram /* this means that this section is layed out in "ram" MEMORY */ +} +``` + +This script includes all symbols from `.text` and `.data` in the final executable and assigns addresses from everything in `.text` to the flash memory and from everything in `.data` into the RAM. The section `.data` will have a different *load address* though: It is located directly behind `.text` and is not located at `0x10000000`. + +As most people have never heard of the *load address*, here's a short excourse: When linking a program, two things happen: + +1. Objects get assigned a address in memory (*link address*) +2. Objects get a position where they are loaded (*load address*) + +On a normal desktop program, these two are the same. In our embedded world, we need to "load" the RAM contents into flash though, as RAM cannot store the data. This means that when we link our program, the linker will treat everything in `.data` as it would be stored in RAM (and puts the symbol *address* there), but will actually store the data into the flash (and will store the bits there). + +With this linker script, we now know two things: Everything in `.data` must be at the address of `data_begin` (which is in RAM), but is still located at `code_end`. Thus, we have to copy the memory from flash to RAM: + +```zig +const std = @import("std"); + +// We can access symbols by declaring them as extern c_void +// and taking their address +extern var code_end: c_void; +extern var data_begin: c_void; +extern var data_end: c_void; + +extern fn _start() callconv(.Naked) noreturn { + // first, gather both source and destination addresses: + const src_ptr = @ptrCast([*]const u8, &code_end); + const dst_ptr = @ptrCast([*]u8, &data_begin); + + // then, compute the length of the .data section by + // just subtracting two pointers + const length = @ptrToInt(&data_end) - @ptrToInt(&data_start); + + // and finally, initialize .data: + std.mem.copy(u8, dst_ptr[0..length], src_ptr[0..length]); + + // call your program enty point here: + // … +} +``` + +There's two sections i left out: +`.rodata`, which is just made the same way as `.text` and will reside in flash and `.bss` which is similar to `.data`, but doesn't have initial content and can just be set to zero with `std.mem.set(u8, bss_ptr[0..bss_length], 0)`. + +As you might have noticed, we have a function called `_start`. This is our programs entry point and *must never* return, otherwise **bad things** will happen (and arbitrary code will be executed). Make sure to always include some endless loop that disables interrupt for safety here! + +But you might wonder: How is this entry point called? This is very SOC-dependent and is explained in the respective tutorials for each SOC. The same is true for setting up the [stack pointer](https://en.wikipedia.org/wiki/Call_stack) which is required for calling functions and storing temporary variables. + +## MORE COMING SOON diff --git a/website/content/tutorials/memory-map.png b/website/content/tutorials/memory-map.png new file mode 100644 index 0000000..a41ac40 Binary files /dev/null and b/website/content/tutorials/memory-map.png differ diff --git a/website/ember-icon.xcf b/website/ember-icon.xcf new file mode 100644 index 0000000..7b1db9f Binary files /dev/null and b/website/ember-icon.xcf differ diff --git a/website/flake.lock b/website/flake.lock new file mode 100644 index 0000000..276e5a4 --- /dev/null +++ b/website/flake.lock @@ -0,0 +1,146 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1708682153, + "narHash": "sha256-5sMDOig3rOe5/2yrhiVjQZnVranorjKHVkzQGmZNNLY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "33a498b7b1e3af01cb9f99bed64c96c1b4acaa70", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "release-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1702350026, + "narHash": "sha256-A+GNZFZdfl4JdDphYKBJ5Ef1HOiFsP18vQe9mqjmUis=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9463103069725474698139ab10f17a9d125da859", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "zig": "zig" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig": { + "inputs": { + "flake-compat": "flake-compat_2", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1708647717, + "narHash": "sha256-iA+MJG6isCog6KIq9uyyTmBMacJCwIuecBbkZol8XiE=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "433ed3117af772faad68a50dcf0199bf273f8721", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/website/flake.nix b/website/flake.nix new file mode 100644 index 0000000..c8c8b64 --- /dev/null +++ b/website/flake.nix @@ -0,0 +1,52 @@ +{ + description = "microzig website environment"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/release-23.11"; + flake-utils.url = "github:numtide/flake-utils"; + + # required for latest zig + zig.url = "github:mitchellh/zig-overlay"; + + # Used for shell.nix + flake-compat = { + url = github:edolstra/flake-compat; + flake = false; + }; + }; + + outputs = + { self + , nixpkgs + , flake-utils + , ... + } @ inputs: + let + overlays = [ + (final: prev: { zigpkgs = inputs.zig.packages.${prev.system}; }) + ]; + + # Our supported systems are the same supported systems as the Zig binaries + systems = builtins.attrNames inputs.zig.packages; + in + flake-utils.lib.eachSystem systems ( + system: + let + pkgs = import nixpkgs { inherit overlays system; }; + in + rec { + devShells.default = pkgs.mkShell { + nativeBuildInputs = [ pkgs.zigpkgs.master ]; + + buildInputs = [ pkgs.bashInteractive ]; + + shellHook = '' + export SHELL=${pkgs.bashInteractive}/bin/bash + ''; + }; + + # For compatibility with older versions of the `nix` binary + devShell = self.devShells.${system}.default; + } + ); +} diff --git a/website/layouts/getting-started.html b/website/layouts/getting-started.html new file mode 100644 index 0000000..646f607 --- /dev/null +++ b/website/layouts/getting-started.html @@ -0,0 +1,56 @@ + + + + +
+ + +
+ +
+

microzig examples

+ + +
+ +
+

generic zig learning material

+ + +
+ +
+

external tutorials

+ + +
+ +
+ +
+ vectorized Ember, the awesome zeg mascot! +
+
diff --git a/website/layouts/index.html b/website/layouts/index.html new file mode 100644 index 0000000..4210966 --- /dev/null +++ b/website/layouts/index.html @@ -0,0 +1,96 @@ + + + + +
+ + +
+
+

+ + goals

+ +
    +
  • provide documents on how to get started with embedded programming (for absolute newbies)
  • +
  • provide example snippets for common operations on certain architectures (LPC, STM32, AVR, ...)
  • +
  • provide example worked through embedded mini-projects
  • +
  • create register definition libraries
  • +
  • create a common interface/HAL over several architectures
  • +
  • create a performant common set of drivers for external platforms
  • +
+
+ +
+

+ + important links

+ + +
+ +
+

+ + community

+ + +
+ +
+

+ + projects

+ + +
+ +
+

+ + core members

+ + +
+
+ +
+ vectorized Ember, the awesome zeg mascot! +
+
diff --git a/website/layouts/templates/base.html b/website/layouts/templates/base.html new file mode 100644 index 0000000..1f4c22b --- /dev/null +++ b/website/layouts/templates/base.html @@ -0,0 +1,28 @@ + + + + + <super/> - Zig Embedded Group + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/website/layouts/tutorial.html b/website/layouts/tutorial.html new file mode 100644 index 0000000..e3d4dbc --- /dev/null +++ b/website/layouts/tutorial.html @@ -0,0 +1,17 @@ + + + + +
+ + + +
+ +
+ vectorized Ember, the awesome zeg mascot! +
+
diff --git a/website/legacy-ssg/main.zig b/website/legacy-ssg/main.zig new file mode 100644 index 0000000..5cb44b1 --- /dev/null +++ b/website/legacy-ssg/main.zig @@ -0,0 +1,684 @@ +const std = @import("std"); +const koino = @import("koino"); + +const markdown_options = koino.Options{ + .extensions = .{ + .table = true, + .autolink = true, + .strikethrough = true, + }, + .render = .{ + .header_anchors = true, + .anchor_icon = "§ ", + }, +}; + +/// verifies and parses a file name in the format +/// "YYYY-MM-DD - " [.*] ".md" +fn isValidArticleFileName(path: []const u8) ?Date { + if (path.len < 16) + return null; + if (!std.mem.endsWith(u8, path, ".md")) + return null; + if (path[4] != '-' or path[7] != '-' or !std.mem.eql(u8, path[10..13], " - ")) + return null; + return Date{ + .year = std.fmt.parseInt(u16, path[0..4], 10) catch return null, + .month = std.fmt.parseInt(u8, path[5..7], 10) catch return null, + .day = std.fmt.parseInt(u8, path[8..10], 10) catch return null, + }; +} + +pub fn main() anyerror!void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + + const allocator = gpa.allocator(); + + var website = Website{ + .allocator = allocator, + .arena = std.heap.ArenaAllocator.init(allocator), + .articles = std.ArrayList(Article).init(allocator), + .tutorials = std.ArrayList(Tutorial).init(allocator), + .images = std.ArrayList([]const u8).init(allocator), + }; + defer website.deinit(); + + // gather step + { + var root_dir = try std.fs.cwd().openDir("website", .{}); + defer root_dir.close(); + + // Tutorials are maintained manually right now + try website.addTutorial(Tutorial{ + .src_file = "website/tutorials/01-embedded-basics.md", + }); + try website.addTutorial(Tutorial{ + .src_file = "website/tutorials/02-embedded-programming.md", + }); + // try website.addTutorial(Tutorial{ + // .src_file = "website/tutorials/03-lpc1768.md", + // }); + // try website.addTutorial(Tutorial{ + // .src_file = "website/tutorials/03-nrf52.md", + // }); + // try website.addTutorial(Tutorial{ + // .src_file = "website/tutorials/03-avr.md", + // }); + // try website.addTutorial(Tutorial{ + // .src_file = "website/tutorials/03-pi-pico.md", + // }); + // try website.addTutorial(Tutorial{ + // .src_file = "website/tutorials/03-stm32.md", + // }); + // try website.addTutorial(Tutorial{ + // .src_file = "website/tutorials/04-chose-device.md", + // }); + // try website.addTutorial(Tutorial{ + // .src_file = "website/tutorials/05-hal.md", + // }); + + // img articles + { + var dir = try root_dir.openIterableDir("img", .{}); + defer dir.close(); + + var iter = dir.iterate(); + + while (try iter.next()) |entry| { + if (entry.kind != .File) { + std.log.err("Illegal folder in directory website/img: {s}", .{entry.name}); + continue; + } + + const path = try std.fs.path.join(website.arena.allocator(), &[_][]const u8{ + "website", + "img", + entry.name, + }); + + try website.addImage(path); + } + } + + // gather articles + { + var dir = try root_dir.openIterableDir("articles", .{}); + defer dir.close(); + + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind != .File) { + std.log.err("Illegal folder in directory website/articles: {s}", .{entry.name}); + continue; + } + + const date = isValidArticleFileName(entry.name) orelse { + if (!std.mem.eql(u8, entry.name, ".keep")) + std.log.err("Illegal file name in directory website/articles: {s}", .{entry.name}); + continue; + }; + + var article = Article{ + .title = "Not yet generated", + .src_file = undefined, + .date = date, + }; + + article.src_file = try std.fs.path.join(website.arena.allocator(), &[_][]const u8{ + "website", + "articles", + entry.name, + }); + + try website.addArticle(article); + } + } + } + + try website.prepareRendering(); + + // final rendering + { + var root_dir = try std.fs.cwd().makeOpenPath("render", .{}); + defer root_dir.close(); + + try std.fs.Dir.copyFile( + std.fs.cwd(), + "src/style.css", + root_dir, + "style.css", + .{}, + ); + + try std.fs.Dir.copyFile( + std.fs.cwd(), + "website/favicon.ico", + root_dir, + "favicon.ico", + .{}, + ); + + try website.renderHtmlFile("website/index.htm", root_dir, "index.htm"); + try website.renderHtmlFile("website/getting-started.htm", root_dir, "getting-started.htm"); + + try website.renderArticleIndex(root_dir, "articles.htm"); + + var art_dir = try root_dir.makeOpenPath("articles", .{}); + defer art_dir.close(); + + var tut_dir = try root_dir.makeOpenPath("tutorials", .{}); + defer tut_dir.close(); + + var img_dir = try root_dir.makeOpenPath("img", .{}); + defer img_dir.close(); + + try website.renderArticles(art_dir); + + try website.renderTutorials(tut_dir); + + try website.renderAtomFeed(root_dir, "feed.atom"); + + try website.renderImages(img_dir); + } +} + +const Date = struct { + const Self = @This(); + + day: u8, + month: u8, + year: u16, + + fn toInteger(self: Self) u32 { + return @as(u32, self.day) + 33 * @as(u32, self.month) + (33 * 13) * @as(u32, self.year); + } + + pub fn lessThan(lhs: Self, rhs: Self) bool { + return lhs.toInteger() < rhs.toInteger(); + } + + pub fn eql(a: Self, b: Self) bool { + return std.meta.eql(a, b); + } + + pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + _ = fmt; + _ = options; + try writer.print("{d:0>4}-{d:0>2}-{d:0>2}", .{ + self.year, self.month, self.day, + }); + } +}; + +const Article = struct { + date: Date, + src_file: []const u8, + title: []const u8 = "", +}; + +const Tutorial = struct { + src_file: []const u8, + title: []const u8 = "", +}; + +const Website = struct { + const Self = @This(); + + is_prepared: bool = false, + allocator: std.mem.Allocator, + arena: std.heap.ArenaAllocator, + articles: std.ArrayList(Article), + tutorials: std.ArrayList(Tutorial), + images: std.ArrayList([]const u8), + + fn deinit(self: *Self) void { + self.tutorials.deinit(); + self.articles.deinit(); + self.images.deinit(); + self.arena.deinit(); + self.* = undefined; + } + + fn addArticle(self: *Self, article: Article) !void { + self.is_prepared = false; + try self.articles.append(Article{ + .date = article.date, + .src_file = try self.arena.allocator().dupe(u8, article.src_file), + .title = try self.arena.allocator().dupe(u8, article.title), + }); + } + + fn addTutorial(self: *Self, tutorial: Tutorial) !void { + self.is_prepared = false; + try self.tutorials.append(Tutorial{ + .src_file = try self.arena.allocator().dupe(u8, tutorial.src_file), + .title = try self.arena.allocator().dupe(u8, tutorial.title), + }); + } + + fn addImage(self: *Self, path: []const u8) !void { + self.is_prepared = false; + try self.images.append(try self.arena.allocator().dupe(u8, path)); + } + + fn findTitle(self: *Self, file: []const u8) !?[]const u8 { + var doc = blk: { + var p = try koino.parser.Parser.init(self.allocator, markdown_options); + defer p.deinit(); + + const markdown = try std.fs.cwd().readFileAlloc(self.allocator, file, 10_000_000); + defer self.allocator.free(markdown); + + try p.feed(markdown); + + break :blk try p.finish(); + }; + defer doc.deinit(); + + std.debug.assert(doc.data.value == .Document); + + var iter = doc.first_child; + var heading_or_null: ?*koino.nodes.AstNode = while (iter) |item| : (iter = item.next) { + if (item.data.value == .Heading) { + if (item.data.value.Heading.level == 1) { + break item; + } + } + } else null; + + if (heading_or_null) |heading| { + var list = std.ArrayList(u8).init(self.arena.allocator()); + defer list.deinit(); + + var options = markdown_options; + options.render.header_anchors = false; + + try koino.html.print(list.writer(), self.arena.allocator(), options, heading); + + const string = list.toOwnedSlice(); + + std.debug.assert(std.mem.startsWith(u8, string, "

")); + std.debug.assert(std.mem.endsWith(u8, string, "

\n")); + + return string[4 .. string.len - 6]; + } else { + return null; + } + } + + fn prepareRendering(self: *Self) !void { + std.sort.sort(Article, self.articles.items, self.*, sortArticlesDesc); + + for (self.articles.items) |*article| { + if (try self.findTitle(article.src_file)) |title| { + article.title = title; + } + } + + for (self.tutorials.items) |*tutorial| { + std.debug.print("{s}\n", .{tutorial.src_file}); + if (try self.findTitle(tutorial.src_file)) |title| { + tutorial.title = title; + } + } + + self.is_prepared = true; + } + + fn sortArticlesDesc(self: Self, lhs: Article, rhs: Article) bool { + _ = self; + if (lhs.date.lessThan(rhs.date)) + return false; + if (rhs.date.lessThan(lhs.date)) + return true; + return (std.mem.order(u8, lhs.title, rhs.title) == .gt); + } + + fn removeExtension(src_name: []const u8) []const u8 { + const ext = std.fs.path.extension(src_name); + return src_name[0 .. src_name.len - ext.len]; + } + + fn changeExtension(self: *Self, src_name: []const u8, new_ext: []const u8) ![]const u8 { + return std.mem.join(self.arena.allocator(), "", &[_][]const u8{ + removeExtension(src_name), + new_ext, + }); + } + + fn urlEscape(self: *Self, text: []const u8) ![]u8 { + const legal_character = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + + var len: usize = 0; + for (text) |c| { + len += if (std.mem.indexOfScalar(u8, legal_character, c) == null) + @as(usize, 3) + else + @as(usize, 1); + } + + const buf = try self.arena.allocator().alloc(u8, len); + var offset: usize = 0; + for (text) |c| { + if (std.mem.indexOfScalar(u8, legal_character, c) == null) { + const hexdigits = "0123456789ABCDEF"; + buf[offset + 0] = '%'; + buf[offset + 1] = hexdigits[(c >> 4) & 0xF]; + buf[offset + 2] = hexdigits[(c >> 0) & 0xF]; + offset += 3; + } else { + buf[offset] = c; + offset += 1; + } + } + + return buf; + } + + fn renderArticles(self: *Self, dst_dir: std.fs.Dir) !void { + std.debug.assert(self.is_prepared); + for (self.articles.items) |art| { + try self.renderMarkdownFile( + art.src_file, + dst_dir, + try self.changeExtension(std.fs.path.basename(art.src_file), ".htm"), + ); + } + } + + fn renderTutorials(self: *Self, dst_dir: std.fs.Dir) !void { + std.debug.assert(self.is_prepared); + for (self.tutorials.items) |tut| { + try self.renderMarkdownFile( + tut.src_file, + dst_dir, + try self.changeExtension(std.fs.path.basename(tut.src_file), ".htm"), + ); + } + } + + /// Renders a list of all possible articles + fn renderArticleIndex(self: *Self, dst_dir: std.fs.Dir, file_name: []const u8) !void { + std.debug.assert(self.is_prepared); + + try self.renderMarkdown( + \\# Articles + \\ + \\ + , dst_dir, file_name); + } + + /// Render a given markdown file into `dst_path`. + fn renderMarkdownFile(self: *Self, src_path: []const u8, dst_dir: std.fs.Dir, dst_path: []const u8) !void { + std.debug.assert(self.is_prepared); + + var markdown_input = try std.fs.cwd().readFileAlloc(self.allocator, src_path, 10_000_000); + defer self.allocator.free(markdown_input); + + try self.renderMarkdown(markdown_input, dst_dir, dst_path); + } + + /// Render the given markdown source into `dst_path`. + /// supported features here are: + /// - `` (renders a table of contents with all items that come *after* said TOC + /// - `` Renders the 10 latest articles + /// - `` Renders all articles + fn renderMarkdown(self: *Self, source: []const u8, dst_dir: std.fs.Dir, dst_path: []const u8) !void { + std.debug.assert(self.is_prepared); + + var doc: *koino.nodes.AstNode = blk: { + var p = try koino.parser.Parser.init(self.arena.allocator(), markdown_options); + try p.feed(source); + defer p.deinit(); + break :blk try p.finish(); + }; + defer doc.deinit(); + + std.debug.assert(doc.data.value == .Document); + + var output_file = try dst_dir.createFile(dst_path, .{}); + defer output_file.close(); + + var writer = output_file.writer(); + + try self.renderHeader(writer); + { + var renderer = koino.html.makeHtmlFormatter(writer, self.arena.allocator(), markdown_options); + defer renderer.deinit(); + + var iter = doc.first_child; + while (iter) |item| : (iter = item.next) { + if (item.data.value == .HtmlBlock) { + const raw_string = item.data.value.HtmlBlock.literal.items; + + const string = std.mem.trim(u8, raw_string, " \t\r\n"); + + if (std.mem.eql(u8, string, "")) { + var min_heading_level: ?u8 = null; + var current_heading_level: u8 = undefined; + + var heading_options = markdown_options; + heading_options.render.header_anchors = false; + + try writer.writeAll("
    "); + + var it = item.next; + while (it) |child| : (it = child.next) { + if (child.data.value == .Heading) { + var heading = child.data.value.Heading; + + if (min_heading_level == null) { + min_heading_level = heading.level; + current_heading_level = heading.level; + } + + if (heading.level < min_heading_level.?) + continue; + + while (current_heading_level > heading.level) { + try writer.writeAll("
"); + current_heading_level -= 1; + } + while (current_heading_level < heading.level) { + try writer.writeAll(""); + current_heading_level -= 1; + } + + try writer.writeAll(""); + } + } + + if (min_heading_level) |mhl| { + while (current_heading_level > mhl) { + try writer.writeAll(""); + current_heading_level -= 1; + } + } + + try writer.writeAll(""); + } else if (std.mem.eql(u8, string, "")) { + for (self.articles.items[0..std.math.min(self.articles.items.len, 10)]) |art| { + try writer.print( + \\
  • {} - {s}
  • + \\ + , .{ + try self.urlEscape(removeExtension(std.fs.path.basename(art.src_file))), + art.date, + art.title, + }); + } + } else if (std.mem.eql(u8, string, "")) { + try writer.writeAll("
      \n"); + for (self.articles.items[0..std.math.min(self.articles.items.len, 10)]) |art| { + try writer.print( + \\
    • {} - {s}
    • + \\ + , .{ + try self.urlEscape(removeExtension(std.fs.path.basename(art.src_file))), + art.date, + art.title, + }); + } + try writer.writeAll("
    \n"); + } else { + std.log.err("Unhandled HTML inline: {s}", .{string}); + } + } else { + try renderer.format(item, false); + } + } + } + try self.renderFooter(writer); + } + + /// Render a given markdown file into `dst_path`. + fn renderHtmlFile(self: *Self, src_path: []const u8, dst_dir: std.fs.Dir, dst_path: []const u8) !void { + std.debug.assert(self.is_prepared); + + var html_input = try std.fs.cwd().readFileAlloc(self.allocator, src_path, 10_000_000); + defer self.allocator.free(html_input); + + try self.renderHtml(html_input, dst_dir, dst_path); + } + + /// Render the html body into `dst_path`. + fn renderHtml(self: Self, source: []const u8, dst_dir: std.fs.Dir, dst_path: []const u8) !void { + std.debug.assert(self.is_prepared); + + var output_file = try dst_dir.createFile(dst_path, .{}); + defer output_file.close(); + + var writer = output_file.writer(); + + try self.renderHeader(writer); + try writer.writeAll(source); + try self.renderFooter(writer); + } + + fn renderHeader(self: Self, writer: anytype) !void { + std.debug.assert(self.is_prepared); + try writer.writeAll( + \\ + \\ + \\ + \\ + \\ + \\ + \\ ZEG + \\ + \\ + \\ + ); + } + + fn renderFooter(self: Self, writer: anytype) !void { + std.debug.assert(self.is_prepared); + try writer.writeAll( + \\ + \\ + \\ + ); + } + + fn renderAtomFeed(self: *Self, dir: std.fs.Dir, file_name: []const u8) !void { + var feed_file = try dir.createFile(file_name, .{}); + defer feed_file.close(); + + var feed_writer = feed_file.writer(); + + try feed_writer.writeAll( + \\ + \\ + \\ + \\ Zig Embedded Group + \\ + \\ Zig Embedded Group + \\ https://zeg.random-projects.net/ + \\ + ); + + var last_update = Date{ .year = 0, .month = 0, .day = 0 }; + var article_count: usize = 0; + for (self.articles.items) |article| { + if (last_update.lessThan(article.date)) { + last_update = article.date; + article_count = 0; + } else { + article_count += 1; + } + } + + try feed_writer.print(" {d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:00:00Z\n", .{ + last_update.year, + last_update.month, + last_update.day, + article_count, // this is fake, but is just here for creating a incremental version for multiple articles a day + }); + + for (self.articles.items) |article| { + const uri_name = try self.urlEscape(removeExtension(article.src_file)); + try feed_writer.print( + \\ + \\ {s} + \\ + \\ zeg.random-projects.net/articles/{s}.htm + \\ {d:0>4}-{d:0>2}-{d:0>2}T00:00:00Z + \\ + \\ + , .{ + article.title, + uri_name, + uri_name, + article.date.year, + article.date.month, + article.date.day, + }); + } + + try feed_writer.writeAll(""); + } + + fn renderImages(self: Self, target_dir: std.fs.Dir) !void { + for (self.images.items) |img| { + try std.fs.Dir.copyFile( + std.fs.cwd(), + img, + target_dir, + std.fs.path.basename(img), + .{}, + ); + } + } + + // fn renderArticle(self: *Website, article: Article, dst_dir: std.fs.Dir, dst_name: []const u8) !void { + // var formatter = HtmlFormatter.init(allocator, options); + // defer formatter.deinit(); + + // try formatter.format(root, false); + + // return formatter.buffer.toOwnedSlice(); + // } +}; diff --git a/website/static/favicon.ico b/website/static/favicon.ico new file mode 100644 index 0000000..af56b80 Binary files /dev/null and b/website/static/favicon.ico differ diff --git a/website/static/img/articles.svg b/website/static/img/articles.svg new file mode 100644 index 0000000..a2daf51 --- /dev/null +++ b/website/static/img/articles.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/img/atom.svg b/website/static/img/atom.svg new file mode 100644 index 0000000..b870f3e --- /dev/null +++ b/website/static/img/atom.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/img/battery.svg b/website/static/img/battery.svg new file mode 100644 index 0000000..0545c03 --- /dev/null +++ b/website/static/img/battery.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/website/static/img/chat-discord.svg b/website/static/img/chat-discord.svg new file mode 100644 index 0000000..3efe1ec --- /dev/null +++ b/website/static/img/chat-discord.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/website/static/img/chat-irc.svg b/website/static/img/chat-irc.svg new file mode 100644 index 0000000..62ff396 --- /dev/null +++ b/website/static/img/chat-irc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/website/static/img/code.svg b/website/static/img/code.svg new file mode 100644 index 0000000..f92f207 --- /dev/null +++ b/website/static/img/code.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/website/static/img/community.svg b/website/static/img/community.svg new file mode 100644 index 0000000..9fb0f21 --- /dev/null +++ b/website/static/img/community.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/website/static/img/ember.svg b/website/static/img/ember.svg new file mode 100644 index 0000000..18aa542 --- /dev/null +++ b/website/static/img/ember.svg @@ -0,0 +1,70 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/website/static/img/goals.svg b/website/static/img/goals.svg new file mode 100644 index 0000000..137cb27 --- /dev/null +++ b/website/static/img/goals.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/img/members.svg b/website/static/img/members.svg new file mode 100644 index 0000000..324b268 --- /dev/null +++ b/website/static/img/members.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/website/static/img/pager.svg b/website/static/img/pager.svg new file mode 100644 index 0000000..fdca7b6 --- /dev/null +++ b/website/static/img/pager.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/img/read-more.svg b/website/static/img/read-more.svg new file mode 100644 index 0000000..95bb3a3 --- /dev/null +++ b/website/static/img/read-more.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/website/static/img/teacher.svg b/website/static/img/teacher.svg new file mode 100644 index 0000000..10f3236 --- /dev/null +++ b/website/static/img/teacher.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/style.css b/website/static/style.css new file mode 100644 index 0000000..0fa3eb7 --- /dev/null +++ b/website/static/style.css @@ -0,0 +1,213 @@ +/* Limit the text width of the body to roughly 40 characters +body { + max-width: 40em; + margin-left: auto; + margin-right: auto; + font-family: sans; +} + +@media screen and (max-width: 600px) { + body { + padding: 2em; + } +} +// Align top-level headings +h1 { + text-align: center; +} + +// Make images in headings and links exactly 1 character high. +h1 img, h2 img, h3 img, h3 img, h4 img, h5 img, h6 img, a img { + width: 1em; + height: 1em; + vertical-align: middle; +} + +// center images in a paragraph and display them as a block +p > img { + display: block; + max-width: 100%; + margin-left: auto; + margin-right: auto; +} + +// Make nice top-level codeblocks +body > pre { + background-color: #EEE; + padding: 0.5em; +} + +// Make nice top-level blockquotes +body > blockquote { + border-left: 3pt solid cornflowerblue; + padding-left: 0.5em; + margin-left: 0.5em; +} + +// Make links in headings invisible +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + text-decoration: none; + font-weight: lighter; + color: unset; + opacity: 10%; + margin-left: -1.5em; + padding-left: 0.5em; +} +h1:hover a, h2:hover a, h3:hover a, h4:hover a, h5:hover a, h6:hover a { + opacity: 50%; +}*/ + +@import "https://rsms.me/inter/inter.css"; +@import "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"; + +html { + font-family: "Inter", "Arial", sans-serif +} + +@supports(font-variation-settings: normal) { + html { + font-family: "Inter var", "Arial", sans-serif + } +} + +* { + box-sizing: border-box +} + +html, +body { + margin: 0 +} + +h1 { + margin: 0; + font-size: 24pt; + font-weight: 700; + letter-spacing: -2px +} + +h2 { + font-size: 18pt; + font-weight: 600 +} + +ul.bars { + margin: 0; + padding: 0; + list-style: none +} + +ul.bars li { + border-bottom: 1px solid gray; + margin-bottom: 15px; + padding-bottom: 15px; + font-size: 12pt +} + +a { + color: #5c8ebf +} + +strong, +strong a { + color: #f7a41d +} + +#content { + padding: 80px +} + +#intro-nav { + display: flex; + margin-bottom: 20px; + justify-content: space-between +} + +#intro-grid { + display: grid; + gap: 40px; + grid-auto-rows: 1fr; + grid-template-columns: 1fr 1fr 1fr +} + +@media only screen and (max-width: 1200px) { + #intro { + padding: 50px; + } + + #intro-grid { + grid-template-columns: 1fr 1fr + } +} + +@media only screen and (max-width: 700px) { + #intro { + padding: 20px; + } + + #intro-grid { + display: block + } + + #intro-grid>* { + display: block; + margin-bottom: 40px + } +} + +#intro-grid a { + text-decoration: none +} + +#docs { + display: flex +} + +#docs-nav { + border-right: 1px solid gray; + padding: 40px 0px; + width: 20% +} + +#docs-nav-header { + display: flex; + align-items: center; + flex-direction: column +} + +#docs-body { + width: 80%; + padding: 40px +} + +a[href^="http"]::after, +a[href^="https://"]::after { + content: ""; + width: 11px; + height: 11px; + margin-left: 4px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' stroke='%235c8ebf' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E"); + background-position: center; + background-repeat: no-repeat; + background-size: contain; + display: inline-block; +} + +img.inline { + display: inline; + max-height: 1.2em; + max-width: 1.2em; + vertical-align: middle; +} + +h2 svg { + display: inline; + max-height: 1.1em; + vertical-align: middle; + max-width: 1.1em; + margin-right: 0.25em; +} + +#intro-nav a { + text-decoration: none; +} \ No newline at end of file