├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── build └── camera.zig ├── flake.lock ├── flake.nix ├── frontmatter.ziggy-schema ├── src ├── AnsiRenderer.zig ├── Build.zig ├── Git.zig ├── PathTable.zig ├── StringTable.zig ├── Template.zig ├── Variant.zig ├── channel.zig ├── cli │ ├── debug.zig │ ├── init.zig │ ├── init │ │ ├── assets │ │ │ ├── Temml-Local.css │ │ │ ├── Temml.woff2 │ │ │ ├── highlight.css │ │ │ ├── render-mathtex.js │ │ │ ├── style.css │ │ │ ├── temml.min.js │ │ │ └── under-construction.gif │ │ ├── content │ │ │ ├── about.smd │ │ │ ├── blog │ │ │ │ ├── first-post │ │ │ │ │ ├── fanzine.jpg │ │ │ │ │ └── index.smd │ │ │ │ ├── index.smd │ │ │ │ └── second-post.smd │ │ │ ├── devlog │ │ │ │ ├── 1989.smd │ │ │ │ ├── 1990.smd │ │ │ │ └── index.smd │ │ │ └── index.smd │ │ ├── layouts │ │ │ ├── blog.shtml │ │ │ ├── blog.xml │ │ │ ├── devlog-archive.shtml │ │ │ ├── devlog.shtml │ │ │ ├── devlog.xml │ │ │ ├── index.shtml │ │ │ ├── page.shtml │ │ │ ├── post.shtml │ │ │ └── templates │ │ │ │ └── base.shtml │ │ └── zine.ziggy │ ├── release.zig │ ├── serve.zig │ └── serve │ │ ├── 404.html │ │ ├── error.html │ │ ├── outside.html │ │ ├── watcher │ │ ├── LinuxWatcher.zig │ │ ├── MacosWatcher.zig │ │ └── WindowsWatcher.zig │ │ ├── websocket.zig │ │ └── zinereload.js ├── context.zig ├── context │ ├── Array.zig │ ├── Asset.zig │ ├── Bool.zig │ ├── Build.zig │ ├── DateTime.zig │ ├── Float.zig │ ├── Git.zig │ ├── Int.zig │ ├── Iterator.zig │ ├── Map.zig │ ├── Optional.zig │ ├── Page.zig │ ├── Site.zig │ ├── Slice.zig │ ├── String.zig │ ├── Template.zig │ ├── Value.zig │ ├── doctypes.zig │ ├── markdown.zig │ └── utils.zig ├── docgen.zig ├── fatal.zig ├── fuzz │ └── scripty.zig ├── highlight.zig ├── main.zig ├── render.zig ├── render │ └── html.zig ├── root.zig ├── table.zig ├── worker.zig └── wuffs.zig └── tests ├── content-scanning ├── collisions │ ├── assets │ │ └── .keep │ ├── content │ │ ├── index.smd │ │ ├── nested │ │ │ └── path │ │ │ │ └── page.smd │ │ ├── page.smd │ │ └── page │ │ │ └── index.smd │ ├── layouts │ │ ├── index.shtml │ │ └── page.shtml │ ├── snapshot.txt │ └── zine.ziggy ├── frontmatter │ ├── assets │ │ └── .keep │ ├── content │ │ ├── index.smd │ │ ├── validation-errors.smd │ │ └── wrong-syntax.smd │ ├── layouts │ │ └── .keep │ ├── snapshot.txt │ └── zine.ziggy ├── page-analysis │ ├── assets │ │ ├── .keep │ │ ├── code.zig │ │ └── skater.webp │ ├── content │ │ ├── code.smd │ │ ├── index.smd │ │ ├── other.smd │ │ ├── parse.smd │ │ └── skater.webp │ ├── layouts │ │ ├── archive-entry.shtml │ │ ├── archive.shtml │ │ ├── index.shtml │ │ └── sections.shtml │ ├── snapshot.txt │ └── zine.ziggy ├── simple │ ├── assets │ │ └── .keep │ ├── content │ │ ├── archive │ │ │ ├── 2024 │ │ │ │ ├── first.smd │ │ │ │ └── index.smd │ │ │ ├── 2025 │ │ │ │ ├── index.smd │ │ │ │ └── second.smd │ │ │ └── index.smd │ │ ├── index.smd │ │ └── sections.smd │ ├── layouts │ │ ├── archive-entry.shtml │ │ ├── archive.shtml │ │ ├── index.shtml │ │ └── sections.shtml │ ├── snapshot.txt │ └── zine.ziggy └── templates │ ├── assets │ └── .keep │ ├── content │ ├── another.smd │ ├── badextend.smd │ ├── badhtml.smd │ ├── badshtml.smd │ ├── index.smd │ └── page.smd │ ├── layouts │ ├── .keep │ ├── badextend.shtml │ ├── badhtml.shtml │ ├── badshtml.shtml │ ├── index.shtml │ ├── oops.html │ ├── page.shtml │ └── templates │ │ ├── base.shtml │ │ └── withmenu.shtml │ ├── snapshot.txt │ └── zine.ziggy ├── drafts └── simple │ ├── assets │ └── .keep │ ├── content │ ├── archive │ │ ├── 2024 │ │ │ ├── first.smd │ │ │ └── index.smd │ │ ├── 2025 │ │ │ ├── index.smd │ │ │ └── second.smd │ │ └── index.smd │ ├── index.smd │ └── sections.smd │ ├── layouts │ ├── archive-entry.shtml │ ├── archive.shtml │ ├── index.shtml │ └── sections.shtml │ ├── snapshot.txt │ ├── snapshot │ ├── archive │ │ ├── 2024 │ │ │ ├── first │ │ │ │ └── index.html │ │ │ └── index.html │ │ ├── 2025 │ │ │ ├── index.html │ │ │ └── second │ │ │ │ └── index.html │ │ └── index.html │ ├── index.html │ └── sections │ │ └── index.html │ └── zine.ziggy ├── rendering ├── multi │ ├── content │ │ ├── de-DE │ │ │ ├── about.smd │ │ │ ├── contact-us.smd │ │ │ └── index.smd │ │ ├── en-US │ │ │ ├── about.smd │ │ │ ├── contact-us.smd │ │ │ └── index.smd │ │ └── it-IT │ │ │ ├── contattaci.smd │ │ │ └── index.smd │ ├── i18n │ │ ├── de-DE.ziggy │ │ ├── en-US.ziggy │ │ └── it-IT.ziggy │ ├── layouts │ │ ├── blog.shtml │ │ ├── blog.xml │ │ ├── devlog-archive.shtml │ │ ├── devlog.shtml │ │ ├── devlog.xml │ │ ├── index.shtml │ │ ├── page.shtml │ │ ├── post.shtml │ │ └── templates │ │ │ └── base.shtml │ ├── snapshot.txt │ ├── snapshot │ │ ├── about │ │ │ └── index.html │ │ ├── contact-us │ │ │ └── index.html │ │ ├── de-DE │ │ │ ├── about │ │ │ │ └── index.html │ │ │ ├── contact-us │ │ │ │ └── index.html │ │ │ └── index.html │ │ ├── index.html │ │ └── it-IT │ │ │ ├── contattaci │ │ │ └── index.html │ │ │ └── index.html │ └── zine.ziggy └── simple │ ├── assets │ └── .keep │ ├── content │ ├── archive │ │ ├── 2024 │ │ │ ├── first.smd │ │ │ └── index.smd │ │ ├── 2025 │ │ │ ├── index.smd │ │ │ └── second.smd │ │ └── index.smd │ ├── index.smd │ ├── nested │ │ └── aliases.smd │ ├── sections.smd │ └── syntax.smd │ ├── layouts │ ├── archive-entry.shtml │ ├── archive.shtml │ ├── index.shtml │ └── sections.shtml │ ├── snapshot.txt │ ├── snapshot │ ├── alias_absolute.html │ ├── aliases │ │ └── path │ │ │ └── with │ │ │ └── leading │ │ │ └── slash.html │ ├── archive │ │ ├── 2024 │ │ │ ├── first │ │ │ │ └── index.html │ │ │ └── index.html │ │ ├── 2025 │ │ │ ├── index.html │ │ │ └── second │ │ │ │ └── index.html │ │ └── index.html │ ├── index.html │ ├── nested │ │ └── aliases │ │ │ ├── alias_relative.html │ │ │ ├── aliases │ │ │ └── path │ │ │ │ └── without │ │ │ │ └── leading │ │ │ │ └── slash.html │ │ │ └── index.html │ ├── sections │ │ └── index.html │ └── syntax │ │ └── index.html │ └── zine.ziggy └── simple └── snapshot └── syntax └── index.html /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | concurrency: 8 | # Cancels pending runs when a PR gets updated. 9 | group: ${{ github.head_ref || github.run_id }}-${{ github.actor }} 10 | cancel-in-progress: true 11 | permissions: 12 | # Sets permission policy for `GITHUB_TOKEN` 13 | contents: read 14 | jobs: 15 | tests: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macos-latest] 20 | 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - name: No autocrlf 24 | run: git config --global core.autocrlf false 25 | 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 # Change if you need git info 29 | 30 | - name: Setup Zig 31 | uses: mlugg/setup-zig@v1 32 | with: 33 | version: 0.14.0 34 | 35 | - name: Build 36 | run: zig build test 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | public 2 | .zine-cache 3 | .zig-cache 4 | zig-cache 5 | zig-out 6 | scratch 7 | result 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Loris Cro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Zine

2 |

Fast, Scalable, Flexible Static Site Generator (SSG)

3 |

Zine is pronounced like in fanzine.

4 | 5 | ## Development Status 6 | Zine is still a young project, not yet at feature parity with more popular 7 | alternatives (e.g. Hugo), but it's perfectly able to handle a personal website 8 | with a blog. 9 | 10 | ## Getting Started 11 | Go to https://zine-ssg.io to get started. 12 | 13 | ## GitHub Actions 14 | If you plan to build your website with GitHub Actions, take a look at [kristoff-it/setup-zine](https://github.com/marketplace/actions/setup-zine), the official 15 | GitHub Action to get access to Zine in your runner. 16 | 17 | We also have more complete guides at https://zine-ssg.io/docs/. 18 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zine, 3 | .version = "0.9.0", 4 | .fingerprint = 0xa466bcb520a7eea2, 5 | .minimum_zig_version = "0.14.0", 6 | .dependencies = .{ 7 | .ziggy = .{ 8 | .url = "git+https://github.com/kristoff-it/ziggy#8a29017169f43dc2c3526817e98142eb9a335087", 9 | .hash = "ziggy-0.1.0-kTg8vwkbBgAOHreabwZtDDtNDi3U_RAiOMvuRDTJiy0I", 10 | }, 11 | .afl_kit = .{ 12 | .url = "git+https://github.com/kristoff-it/zig-afl-kit#f003bfe714f2964c90939fdc940d5993190a66ec", 13 | .hash = "1220f2d8402bb7bbc4786b9c0aad73910929ea209cbd3b063842371d68abfed33c1e", 14 | .lazy = true, 15 | }, 16 | .lsp_kit = .{ 17 | .url = "git+https://github.com/kristoff-it/zig-lsp-kit#87ff3d537a0c852442e180137d9557711963802c", 18 | .hash = "lsp_kit-0.1.0-hAAxO9S9AADv_5D0iplASFtNCFXAPk54M0u-3jj2MRFk", 19 | }, 20 | .scripty = .{ 21 | .url = "git+https://github.com/kristoff-it/scripty#57056571abcc6fe69fcb171c10b0c9e5962f53b0", 22 | .hash = "scripty-0.1.0-LKK5O9jDAADwZbkwkzYcmtTD3xIStr1SNYWL0kcGf8sk", 23 | }, 24 | .tracy = .{ 25 | .url = "git+https://github.com/kristoff-it/tracy#67d2d89e351048c76fc6d161e0ac09d8a831dc60", 26 | .hash = "tracy-0.0.0-4Xw-1pwwAABTfMgoDP1unCbZDZhJEfict7XCBGF6IdIn", 27 | }, 28 | .mime = .{ 29 | .url = "git+https://github.com/andrewrk/mime.git#0b676643886b1e2f19cf11b4e15b028768708342", 30 | .hash = "12209083b0c43d0f68a26a48a7b26ad9f93b22c9cff710c78ddfebb47b89cfb9c7a4", 31 | }, 32 | .zeit = .{ 33 | .url = "git+https://github.com/rockorager/zeit#52b100caa223d5cb1ff0d34f1b677f26e0ce8b84", 34 | .hash = "1220e97357cc39f4f9f053c763f3ec1623e0c7f3999f185746f2bd9bf9b5c5551392", 35 | }, 36 | .flow_syntax = .{ 37 | .url = "git+https://github.com/neurocyte/flow-syntax#d231728c92cb3c5a7139cb0d75a321a119b8e777", 38 | .hash = "flow_syntax-0.1.0-X8jOoUX-AADI9WdOuVSYK9yjyBOTFj4UicSapF7QQssd", 39 | }, 40 | .wuffs = .{ 41 | .url = "git+https://github.com/allyourcodebase/wuffs#818c8ad6607dd5c1ee571362fdb9813b744ee548", 42 | .hash = "1220e4ee09c4fa2d90a9cc7f34f14e04be55a779c84d486696fa9f9ab98ade35409d", 43 | }, 44 | .frameworks = .{ 45 | .url = "git+https://github.com/hexops/xcode-frameworks.git#8a1cfb373587ea4c9bb1468b7c986462d8d4e10e", 46 | .hash = "N-V-__8AALShqgXkvqYU6f__FrA22SMWmi2TXCJjNTO1m8XJ", 47 | .lazy = true, 48 | }, 49 | .superhtml = .{ 50 | .url = "git+https://github.com/kristoff-it/superhtml#16887e9fa3122c36a3d4942470e33c1c282fe859", 51 | .hash = "superhtml-0.4.0-Y7MdPKeXDQD6PoBdAJL8JlNYHk8kH0rcIbRjEfmKTj5r", 52 | }, 53 | .supermd = .{ 54 | .url = "git+https://github.com/kristoff-it/supermd#48500784d7706eaba2d5e1a35332353aca3fc04e", 55 | .hash = "supermd-0.1.0-3Mco3MySWADIsgRWDOxCc1wMJckAQa7yVW2cFzFrL4Fn", 56 | }, 57 | }, 58 | .paths = .{"."}, 59 | } 60 | -------------------------------------------------------------------------------- /build/camera.zig: -------------------------------------------------------------------------------- 1 | //! Runs a program that might or might not fail and appends to stdout what 2 | //! the actual exit code was, always returning a successful exit code under 3 | //! normal conditions (regardless of the child's exit code). 4 | //! 5 | //! This is useful for snapshot tests where some of which are meant to be 6 | //! successes, while others are meant to be failures. 7 | const std = @import("std"); 8 | 9 | pub fn main() !void { 10 | const gpa = std.heap.smp_allocator; 11 | const args = try std.process.argsAlloc(gpa); 12 | 13 | var cmd = std.process.Child.init(args[1..], gpa); 14 | const term = try cmd.spawnAndWait(); 15 | 16 | switch (term) { 17 | .Exited => |code| { 18 | const fmt = "\n\n ----- EXIT CODE: {} -----\n"; 19 | std.debug.print(fmt, .{code}); 20 | // try std.io.getStdOut().writer().print(fmt, .{code}); 21 | }, 22 | else => std.debug.panic("child process crashed: {}\n", .{term}), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1744932701, 24 | "narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "nixpkgs": "nixpkgs", 40 | "zig2nix": "zig2nix" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | }, 58 | "zig2nix": { 59 | "inputs": { 60 | "flake-utils": "flake-utils", 61 | "nixpkgs": [ 62 | "nixpkgs" 63 | ] 64 | }, 65 | "locked": { 66 | "lastModified": 1745026949, 67 | "narHash": "sha256-P+6DKKaZniG5xJIzKpZ7Kt5qdn3RCuFDDo2Atr0y5NU=", 68 | "owner": "Cloudef", 69 | "repo": "zig2nix", 70 | "rev": "d8730240de15020f8022b23d7d6d2fbb53cdae6d", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "Cloudef", 75 | "repo": "zig2nix", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | zig2nix = { 5 | url = "github:Cloudef/zig2nix"; 6 | inputs.nixpkgs.follows = "nixpkgs"; 7 | }; 8 | }; 9 | outputs = 10 | { 11 | self, 12 | nixpkgs, 13 | zig2nix, 14 | ... 15 | }: 16 | let 17 | inherit (nixpkgs) lib; 18 | forAllSystems = 19 | body: lib.genAttrs lib.systems.flakeExposed (system: body nixpkgs.legacyPackages.${system}); 20 | in 21 | { 22 | packages = forAllSystems ( 23 | pkgs: 24 | let 25 | env = zig2nix.outputs.zig-env.${pkgs.system} { 26 | nixpkgs = nixpkgs; 27 | zig = pkgs.zig; 28 | }; 29 | in 30 | { 31 | zine = env.package { 32 | src = lib.cleanSource ./.; 33 | nativeBuildInputs = [ ]; 34 | buildInputs = [ ]; 35 | zigPreferMusl = false; 36 | }; 37 | default = self.packages.${pkgs.system}.zine; 38 | } 39 | ); 40 | formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /frontmatter.ziggy-schema: -------------------------------------------------------------------------------- 1 | root = Frontmatter 2 | 3 | ///A RFC 3339 date string, eg "2024-10-24T00:00:00". 4 | @date = bytes, 5 | 6 | struct Frontmatter { 7 | ///The title of this page. 8 | title: ?bytes, 9 | ///A short description that the section page has 10 | ///access to. 11 | description: ?bytes, 12 | ///The main author of this page. 13 | author: ?bytes, 14 | date: ?@date, 15 | tags: ?[bytes], 16 | ///Alternative paths where this content will also be 17 | ///made available. 18 | aliases: ?[bytes], 19 | ///When set to true this file will be ignored when 20 | ///bulding the website. 21 | draft: ?bool, 22 | ///Path to a layout file inside of the configured 23 | ///layouts directory. 24 | layout: bytes, 25 | ///Alternative versions of this page, created by 26 | ///rendering the content using a different layout. 27 | ///Useful for creating RSS feeds, for example. 28 | alternatives: ?[Alternative], 29 | ///Ignore other markdown files in this directory and 30 | ///any sub-directory. Can only be meaningfully set to 31 | ///true for 'index.smd' pages. 32 | skip_subdirs: ?bool, 33 | ///User-defined properties that you can then reference 34 | ///in templates. 35 | custom: ?map[any], 36 | } 37 | 38 | struct Alternative { 39 | ///Path to a layout file inside of the configured 40 | ///layouts directory. 41 | layout: bytes, 42 | ///Output path, relative to the current directory. 43 | ///Use an absolute path to refer to the website's root 44 | ///directory. 45 | output: bytes, 46 | ///Useful when generating `` 47 | ///elements. 48 | title: ?bytes, 49 | ///Useful when generating `` 50 | ///elements. 51 | type: ?bytes, 52 | } 53 | -------------------------------------------------------------------------------- /src/AnsiRenderer.zig: -------------------------------------------------------------------------------- 1 | const AnsiRenderer = @This(); 2 | const std = @import("std"); 3 | 4 | state: State = .normal, 5 | current_style: Style = .{}, 6 | g0_charset: Charset = .ascii, 7 | 8 | const State = union(enum) { 9 | normal, 10 | escape, 11 | csi: ?u32, 12 | gzd4, 13 | }; 14 | 15 | const Style = struct { 16 | bold: bool = false, 17 | dim: bool = false, 18 | foreground: ?Color = null, 19 | 20 | const Color = enum { 21 | black, 22 | red, 23 | green, 24 | yellow, 25 | blue, 26 | magenta, 27 | cyan, 28 | white, 29 | }; 30 | 31 | fn print(style: Style, out: anytype, open: bool) !void { 32 | comptime var fields: [std.meta.fields(Style).len]std.builtin.Type.StructField = undefined; 33 | @memcpy(&fields, std.meta.fields(Style)); 34 | comptime std.mem.reverse(std.builtin.Type.StructField, &fields); 35 | const FieldEnum = std.meta.FieldEnum(Style); 36 | inline for (fields) |field| { 37 | const value = @field(style, field.name); 38 | const tag: ?std.meta.Tuple(&.{ []const u8, ?[]const u8 }) = switch (@field(FieldEnum, field.name)) { 39 | .bold => if (value) .{ "b", null } else null, 40 | .dim => if (value) .{ "span", "style=\"filter: brightness(75%)\"" } else null, 41 | .foreground => if (value) |color| .{ "span", switch (color) { 42 | inline else => |c| "style=\"color: " ++ @tagName(c) ++ "\"", 43 | } } else null, 44 | }; 45 | 46 | if (tag) |t| { 47 | if (open) { 48 | if (t[1]) |attrs| { 49 | try out.print("<{s} {s}>", .{ t[0], attrs }); 50 | } else { 51 | try out.print("<{s}>", .{t[0]}); 52 | } 53 | } else { 54 | try out.print("", .{t[0]}); 55 | } 56 | } 57 | } 58 | } 59 | 60 | fn printOpen(style: Style, out: anytype) !void { 61 | try style.print(out, true); 62 | } 63 | 64 | fn printClose(style: Style, out: anytype) !void { 65 | try style.print(out, false); 66 | } 67 | }; 68 | 69 | const Charset = enum { 70 | ascii, 71 | vt100_line_drawing, 72 | }; 73 | 74 | pub fn renderSlice(allocator: std.mem.Allocator, src: []const u8) ![]const u8 { 75 | var fbs = std.io.fixedBufferStream(src); 76 | 77 | var out = std.ArrayList(u8).init(allocator); 78 | defer out.deinit(); 79 | 80 | var renderer: AnsiRenderer = .{}; 81 | try renderer.render(fbs.reader(), out.writer()); 82 | 83 | return try out.toOwnedSlice(); 84 | } 85 | 86 | fn render(renderer: *AnsiRenderer, reader: anytype, writer: anytype) !void { 87 | try renderer.current_style.printOpen(writer); 88 | 89 | while (true) { 90 | const char = reader.readByte() catch break; 91 | 92 | switch (renderer.state) { 93 | .normal => switch (char) { 94 | '\x1b' => renderer.state = .escape, 95 | else => switch (renderer.g0_charset) { 96 | .ascii => { 97 | _ = try writer.write(switch (char) { 98 | '<' => "<", 99 | '>' => ">", 100 | else => &.{char}, 101 | }); 102 | }, 103 | .vt100_line_drawing => { 104 | _ = try writer.write(switch (char) { 105 | 'j' => "┘", 106 | 'k' => "┐", 107 | 'l' => "┌", 108 | 'm' => "└", 109 | 'n' => "┼", 110 | 'q' => "─", 111 | 't' => "├", 112 | 'u' => "┤", 113 | 'v' => "┴", 114 | 'w' => "┬", 115 | 'x' => "│", 116 | else => "�", 117 | }); 118 | }, 119 | }, 120 | }, 121 | .escape => switch (char) { 122 | '[' => renderer.state = .{ .csi = null }, 123 | '(' => renderer.state = .gzd4, 124 | else => renderer.state = .normal, 125 | }, 126 | .csi => |payload| switch (char) { 127 | '0'...'9' => { 128 | if (payload == null) { 129 | renderer.state.csi = char - '0'; 130 | } else { 131 | renderer.state.csi.? *= 10; 132 | renderer.state.csi.? += char - '0'; 133 | } 134 | }, 135 | 'm' => { 136 | const n = payload orelse 0; 137 | 138 | try renderer.current_style.printClose(writer); 139 | 140 | switch (n) { 141 | 0 => renderer.current_style = .{}, 142 | 1 => renderer.current_style.bold = true, 143 | 2 => renderer.current_style.dim = true, 144 | 30...37 => renderer.current_style.foreground = @enumFromInt(n - 30), 145 | else => {}, 146 | } 147 | 148 | try renderer.current_style.printOpen(writer); 149 | 150 | renderer.state = .normal; 151 | }, 152 | else => renderer.state = .normal, 153 | }, 154 | .gzd4 => { 155 | switch (char) { 156 | 'B' => renderer.g0_charset = .ascii, 157 | '0' => renderer.g0_charset = .vt100_line_drawing, 158 | else => {}, 159 | } 160 | renderer.state = .normal; 161 | }, 162 | } 163 | } 164 | 165 | try renderer.current_style.printClose(writer); 166 | } 167 | -------------------------------------------------------------------------------- /src/StringTable.zig: -------------------------------------------------------------------------------- 1 | const StringTable = @This(); 2 | 3 | const std = @import("std"); 4 | const mem = std.mem; 5 | const assert = std.debug.assert; 6 | const Allocator = std.mem.Allocator; 7 | 8 | string_bytes: std.ArrayListUnmanaged(u8), 9 | string_map: String.Map, 10 | 11 | pub const empty: StringTable = .{ 12 | .string_bytes = .empty, 13 | .string_map = .empty, 14 | }; 15 | 16 | pub fn deinit(st: *const StringTable, gpa: Allocator) void { 17 | var sb = st.string_bytes; 18 | sb.deinit(gpa); 19 | 20 | var sm = st.string_map; 21 | sm.deinit(gpa); 22 | } 23 | 24 | pub fn get(st: *const StringTable, bytes: []const u8) ?String { 25 | return st.string_map.getKeyAdapted( 26 | @as([]const u8, bytes), 27 | @as(String.MapIndexAdapter, .{ .bytes = st.string_bytes.items }), 28 | ); 29 | } 30 | 31 | pub fn intern( 32 | st: *StringTable, 33 | gpa: Allocator, 34 | bytes: []const u8, 35 | ) !String { 36 | const gop = try st.string_map.getOrPutContextAdapted( 37 | gpa, 38 | @as([]const u8, bytes), 39 | @as(String.MapIndexAdapter, .{ .bytes = st.string_bytes.items }), 40 | @as(String.MapContext, .{ .bytes = st.string_bytes.items }), 41 | ); 42 | if (gop.found_existing) return gop.key_ptr.*; 43 | 44 | try st.string_bytes.ensureUnusedCapacity(gpa, bytes.len + 1); 45 | const new_off: String = @enumFromInt(st.string_bytes.items.len); 46 | 47 | st.string_bytes.appendSliceAssumeCapacity(bytes); 48 | st.string_bytes.appendAssumeCapacity(0); 49 | 50 | gop.key_ptr.* = new_off; 51 | 52 | return new_off; 53 | } 54 | 55 | pub fn ArrayHashMap(T: type) type { 56 | return std.AutoArrayHashMapUnmanaged(String, T); 57 | } 58 | 59 | pub fn HashMap(T: type) type { 60 | return std.AutoHashMapUnmanaged(String, T); 61 | } 62 | 63 | pub const String = enum(u32) { 64 | _, 65 | 66 | const Map = std.HashMapUnmanaged( 67 | String, 68 | void, 69 | MapContext, 70 | std.hash_map.default_max_load_percentage, 71 | ); 72 | 73 | const MapContext = struct { 74 | bytes: []const u8, 75 | 76 | pub fn eql(_: @This(), a: String, b: String) bool { 77 | return a == b; 78 | } 79 | 80 | pub fn hash(ctx: @This(), key: String) u64 { 81 | return std.hash_map.hashString(mem.sliceTo(ctx.bytes[@intFromEnum(key)..], 0)); 82 | } 83 | }; 84 | 85 | const MapIndexAdapter = struct { 86 | bytes: []const u8, 87 | 88 | pub fn eql(ctx: @This(), a: []const u8, b: String) bool { 89 | return mem.eql(u8, a, mem.sliceTo(ctx.bytes[@intFromEnum(b)..], 0)); 90 | } 91 | 92 | pub fn hash(_: @This(), adapted_key: []const u8) u64 { 93 | assert(mem.indexOfScalar(u8, adapted_key, 0) == null); 94 | return std.hash_map.hashString(adapted_key); 95 | } 96 | }; 97 | 98 | pub fn slice(index: String, st: *const StringTable) [:0]const u8 { 99 | const start_slice = st.string_bytes.items[@intFromEnum(index)..]; 100 | return start_slice[0..mem.indexOfScalar(u8, start_slice, 0).? :0]; 101 | } 102 | }; 103 | 104 | test StringTable { 105 | const gpa = std.testing.allocator; 106 | 107 | var string_table: StringTable = .empty; 108 | defer string_table.deinit(gpa); 109 | 110 | const banana = try string_table.intern(gpa, "banana"); 111 | const apple = try string_table.intern(gpa, "apple"); 112 | const melon = try string_table.intern(gpa, "melon"); 113 | 114 | try std.testing.expectEqual(banana, string_table.get("banana").?); 115 | try std.testing.expectEqual(apple, string_table.get("apple").?); 116 | try std.testing.expectEqual(melon, string_table.get("melon").?); 117 | 118 | try std.testing.expectEqual(banana, try string_table.intern(gpa, "banana")); 119 | try std.testing.expectEqual(apple, try string_table.intern(gpa, "apple")); 120 | try std.testing.expectEqual(melon, try string_table.intern(gpa, "melon")); 121 | 122 | try std.testing.expect(banana != apple); 123 | try std.testing.expect(apple != melon); 124 | try std.testing.expect(melon != banana); 125 | 126 | try std.testing.expectEqual(null, string_table.get("strawberry")); 127 | try std.testing.expectEqual(null, string_table.get("coconut")); 128 | try std.testing.expectEqual(null, string_table.get("lemon")); 129 | } 130 | 131 | test HashMap { 132 | const gpa = std.testing.allocator; 133 | 134 | const Color = enum { yellow, red, orange }; 135 | 136 | inline for (&.{ ArrayHashMap, HashMap }) |Map| { 137 | var fruit_color: Map(Color) = .empty; 138 | defer fruit_color.deinit(gpa); 139 | 140 | var string_table: StringTable = .empty; 141 | defer string_table.deinit(gpa); 142 | 143 | const banana = try string_table.intern(gpa, "banana"); 144 | const apple = try string_table.intern(gpa, "apple"); 145 | const melon = try string_table.intern(gpa, "melon"); 146 | 147 | try fruit_color.put(gpa, banana, .yellow); 148 | try fruit_color.put(gpa, apple, .red); 149 | try fruit_color.put(gpa, melon, .orange); 150 | 151 | try std.testing.expectEqual(fruit_color.get(banana).?, .yellow); 152 | try std.testing.expectEqual(fruit_color.get(apple).?, .red); 153 | try std.testing.expectEqual(fruit_color.get(melon).?, .orange); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Template.zig: -------------------------------------------------------------------------------- 1 | const Template = @This(); 2 | 3 | const std = @import("std"); 4 | const superhtml = @import("superhtml"); 5 | const tracy = @import("tracy"); 6 | const root = @import("root.zig"); 7 | const fatal = @import("fatal.zig"); 8 | const worker = @import("worker.zig"); 9 | const Build = @import("Build.zig"); 10 | const StringTable = @import("StringTable.zig"); 11 | const String = StringTable.String; 12 | const PathTable = @import("PathTable.zig"); 13 | const Path = PathTable.Path; 14 | const PathName = PathTable.PathName; 15 | const Allocator = std.mem.Allocator; 16 | const assert = std.debug.assert; 17 | 18 | src: []const u8 = undefined, 19 | html_ast: superhtml.html.Ast = undefined, 20 | // Only present if html_ast.errors.len == 0 21 | ast: superhtml.Ast = undefined, 22 | missing_parent: bool = false, 23 | layout: bool, 24 | 25 | pub fn deinit(t: *const Template, gpa: Allocator) void { 26 | gpa.free(t.src); 27 | t.html_ast.deinit(gpa); 28 | t.ast.deinit(gpa); 29 | } 30 | 31 | pub fn parse( 32 | t: *Template, 33 | gpa: Allocator, 34 | arena: Allocator, 35 | build: *const Build, 36 | pn: PathName, 37 | ) void { 38 | const zone = tracy.trace(@src()); 39 | defer zone.end(); 40 | 41 | errdefer |err| switch (err) { 42 | error.OutOfMemory => fatal.oom(), 43 | }; 44 | 45 | const path = try std.fmt.allocPrint(arena, "{/}", .{ 46 | pn.fmt(&build.st, &build.pt, null), 47 | }); 48 | 49 | const max = std.math.maxInt(u32); 50 | const src = build.layouts_dir.readFileAlloc( 51 | gpa, 52 | path, 53 | max, 54 | ) catch |err| fatal.file(path, err); 55 | 56 | t.src = src; 57 | 58 | t.html_ast = try .init( 59 | gpa, 60 | src, 61 | if (std.mem.endsWith(u8, path, ".xml")) .xml else .superhtml, 62 | ); 63 | if (t.html_ast.errors.len > 0) return; 64 | 65 | t.ast = try .init(gpa, t.html_ast, src); 66 | 67 | if (t.ast.errors.len == 0 and t.ast.extends_idx != 0) { 68 | const parent_name = t.ast.nodes[t.ast.extends_idx].templateValue().span.slice(src); 69 | const parent_path = try root.join(arena, &.{ "templates", parent_name }, '/'); 70 | const parent_pn = PathName.get(&build.st, &build.pt, parent_path) orelse { 71 | t.missing_parent = true; 72 | return; 73 | }; 74 | if (!build.templates.contains(parent_pn)) { 75 | t.missing_parent = true; 76 | return; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/channel.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn Channel(comptime T: type) type { 4 | return struct { 5 | lock: std.Thread.Mutex = .{}, 6 | fifo: Fifo, 7 | writeable: std.Thread.Condition = .{}, 8 | readable: std.Thread.Condition = .{}, 9 | 10 | const Fifo = std.fifo.LinearFifo(T, .Slice); 11 | const Self = @This(); 12 | 13 | pub fn init(buffer: []T) Self { 14 | return Self{ .fifo = Fifo.init(buffer) }; 15 | } 16 | 17 | pub fn put(self: *Self, item: T) void { 18 | self.lock.lock(); 19 | defer { 20 | self.lock.unlock(); 21 | self.readable.signal(); 22 | } 23 | 24 | while (true) return self.fifo.writeItem(item) catch { 25 | self.writeable.wait(&self.lock); 26 | continue; 27 | }; 28 | } 29 | 30 | pub fn tryPut(self: *Self, item: T) !void { 31 | self.lock.lock(); 32 | defer self.lock.unlock(); 33 | 34 | try self.fifo.writeItem(item); 35 | 36 | // only signal on success 37 | self.readable.signal(); 38 | } 39 | 40 | pub fn get(self: *Self) T { 41 | self.lock.lock(); 42 | defer { 43 | self.lock.unlock(); 44 | self.writeable.signal(); 45 | } 46 | 47 | while (true) return self.fifo.readItem() orelse { 48 | self.readable.wait(&self.lock); 49 | continue; 50 | }; 51 | } 52 | 53 | pub fn getOrNull(self: *Self) ?T { 54 | self.lock.lock(); 55 | defer self.lock.unlock(); 56 | 57 | if (self.fifo.readItem()) |item| return item; 58 | 59 | // signal on empty queue 60 | self.writeable.signal(); 61 | } 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/cli/init.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const tracy = @import("tracy"); 4 | const fatal = @import("../fatal.zig"); 5 | const Allocator = std.mem.Allocator; 6 | 7 | const log = std.log.scoped(.init); 8 | 9 | pub fn init(gpa: Allocator, args: []const []const u8) bool { 10 | _ = gpa; 11 | 12 | const cmd: Command = .parse(args); 13 | if (cmd.multilingual) @panic("TODO: multilingual init"); 14 | 15 | const File = struct { path: []const u8, src: []const u8 }; 16 | const files = [_]File{ 17 | .{ 18 | .path = "zine.ziggy", 19 | .src = @embedFile("init/zine.ziggy"), 20 | }, 21 | .{ 22 | .path = "content/index.smd", 23 | .src = @embedFile("init/content/index.smd"), 24 | }, 25 | .{ 26 | .path = "content/about.smd", 27 | .src = @embedFile("init/content/about.smd"), 28 | }, 29 | .{ 30 | .path = "content/blog/index.smd", 31 | .src = @embedFile("init/content/blog/index.smd"), 32 | }, 33 | .{ 34 | .path = "content/blog/first-post/index.smd", 35 | .src = @embedFile("init/content/blog/first-post/index.smd"), 36 | }, 37 | .{ 38 | .path = "content/blog/first-post/fanzine.jpg", 39 | .src = @embedFile("init/content/blog/first-post/fanzine.jpg"), 40 | }, 41 | .{ 42 | .path = "content/blog/second-post.smd", 43 | .src = @embedFile("init/content/blog/second-post.smd"), 44 | }, 45 | .{ 46 | .path = "content/devlog/index.smd", 47 | .src = @embedFile("init/content/devlog/index.smd"), 48 | }, 49 | .{ 50 | .path = "content/devlog/1990.smd", 51 | .src = @embedFile("init/content/devlog/1990.smd"), 52 | }, 53 | .{ 54 | .path = "content/devlog/1989.smd", 55 | .src = @embedFile("init/content/devlog/1989.smd"), 56 | }, 57 | .{ 58 | .path = "layouts/index.shtml", 59 | .src = @embedFile("init/layouts/index.shtml"), 60 | }, 61 | .{ 62 | .path = "layouts/page.shtml", 63 | .src = @embedFile("init/layouts/page.shtml"), 64 | }, 65 | .{ 66 | .path = "layouts/post.shtml", 67 | .src = @embedFile("init/layouts/post.shtml"), 68 | }, 69 | .{ 70 | .path = "layouts/blog.shtml", 71 | .src = @embedFile("init/layouts/blog.shtml"), 72 | }, 73 | .{ 74 | .path = "layouts/blog.xml", 75 | .src = @embedFile("init/layouts/blog.xml"), 76 | }, 77 | .{ 78 | .path = "layouts/devlog.shtml", 79 | .src = @embedFile("init/layouts/devlog.shtml"), 80 | }, 81 | .{ 82 | .path = "layouts/devlog.xml", 83 | .src = @embedFile("init/layouts/devlog.xml"), 84 | }, 85 | .{ 86 | .path = "layouts/devlog-archive.shtml", 87 | .src = @embedFile("init/layouts/devlog-archive.shtml"), 88 | }, 89 | .{ 90 | .path = "layouts/templates/base.shtml", 91 | .src = @embedFile("init/layouts/templates/base.shtml"), 92 | }, 93 | .{ 94 | .path = "assets/style.css", 95 | .src = @embedFile("init/assets/style.css"), 96 | }, 97 | .{ 98 | .path = "assets/highlight.css", 99 | .src = @embedFile("init/assets/highlight.css"), 100 | }, 101 | .{ 102 | .path = "assets/under-construction.gif", 103 | .src = @embedFile("init/assets/under-construction.gif"), 104 | }, 105 | 106 | .{ 107 | .path = "assets/render-mathtex.js", 108 | .src = @embedFile("init/assets/render-mathtex.js"), 109 | }, 110 | .{ 111 | .path = "assets/Temml-Local.css", 112 | .src = @embedFile("init/assets/Temml-Local.css"), 113 | }, 114 | .{ 115 | .path = "assets/Temml.woff2", 116 | .src = @embedFile("init/assets/Temml.woff2"), 117 | }, 118 | .{ 119 | .path = "assets/temml.min.js", 120 | .src = @embedFile("init/assets/temml.min.js"), 121 | }, 122 | }; 123 | 124 | for (files) |file| { 125 | const dirname = std.fs.path.dirnamePosix(file.path); 126 | const basename = std.fs.path.basenamePosix(file.path); 127 | 128 | const base_dir = if (dirname) |dn| 129 | std.fs.cwd().makeOpenPath(dn, .{}) catch |err| fatal.dir(dn, err) 130 | else 131 | std.fs.cwd(); 132 | 133 | const f = base_dir.createFile(basename, .{ 134 | .exclusive = true, 135 | }) catch |err| switch (err) { 136 | else => fatal.file(basename, err), 137 | error.PathAlreadyExists => { 138 | std.debug.print( 139 | "WARNING: '{s}' already exists, skipping.\n", 140 | .{file.path}, 141 | ); 142 | continue; 143 | }, 144 | }; 145 | std.debug.print("Created: {s}\n", .{file.path}); 146 | f.writeAll(file.src) catch |err| fatal.file(file.path, err); 147 | } 148 | 149 | std.debug.print( 150 | \\ 151 | \\Run `zine` to run the Zine development server. 152 | \\Run `zine release` to build your website in 'public/'. 153 | \\Run `zine help` for more commands and options. 154 | \\ 155 | \\Read https://zine-ssg.io/docs/ to learn more about Zine. 156 | \\ 157 | , .{}); 158 | 159 | return false; 160 | } 161 | 162 | const Command = struct { 163 | multilingual: bool, 164 | fn parse(args: []const []const u8) Command { 165 | var multilingual: ?bool = null; 166 | for (args) |a| { 167 | if (std.mem.eql(u8, a, "--multilingual")) { 168 | multilingual = true; 169 | } 170 | 171 | if (std.mem.eql(u8, a, "-h") or std.mem.eql(u8, a, "--help")) { 172 | fatal.msg( 173 | \\Usage: zine init [OPTIONS] 174 | \\ 175 | \\Command specific options: 176 | \\ --multilingual Setup a sample multilingual website 177 | \\ 178 | \\General Options: 179 | \\ --help, -h Print command specific usage 180 | \\ 181 | \\ 182 | , .{}); 183 | } 184 | } 185 | 186 | return .{ .multilingual = multilingual orelse false }; 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /src/cli/init/assets/Temml.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/src/cli/init/assets/Temml.woff2 -------------------------------------------------------------------------------- /src/cli/init/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-yellow: #e5c07b; 3 | --dark-yellow: #d19a66; 4 | --blue: #61afef; 5 | --cyan: #56b6c2; 6 | --light-red: #e06c75; 7 | --dark-red: #be5046; 8 | --comment-gray: #5c6370; 9 | --magenta: #c678dd; 10 | } 11 | 12 | pre { 13 | border-top: 1px solid white; 14 | border-bottom: 1px solid white; 15 | padding: 10px 5px; 16 | } 17 | 18 | code.ziggy { 19 | color: var(--cyan); 20 | } 21 | 22 | code.ziggy .keyword, 23 | code.ziggy .type { 24 | color: var(--light-yellow); 25 | } 26 | 27 | code.ziggy .string { 28 | color: var(--dark-yellow); 29 | } 30 | 31 | code.ziggy .numeric.constant { 32 | color: var(--magenta); 33 | } 34 | 35 | code.ziggy .function { 36 | color: var(--blue); 37 | } -------------------------------------------------------------------------------- /src/cli/init/assets/render-mathtex.js: -------------------------------------------------------------------------------- 1 | let eqns = document.querySelectorAll("script[type='math/tex']"); 2 | for (let i=eqns.length-1; i>=0; i--) { 3 | let eqn = eqns[i]; 4 | let src = eqn.text; 5 | let d = eqn.closest('p') == null; 6 | eqn.outerHTML = temml.renderToString(src, { displayMode: d }); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/cli/init/assets/style.css: -------------------------------------------------------------------------------- 1 | h1, 2 | h2, 3 | h3, 4 | h4, 5 | h5 { 6 | color: #ddd; 7 | font-family: "Verdana", sans-serif; 8 | } 9 | 10 | b, 11 | strong { 12 | color: #fff; 13 | } 14 | 15 | a { 16 | color: #eee; 17 | } 18 | 19 | html { 20 | color: #ccc; 21 | font-family: "Georgia", serif; 22 | font-size: 1.2em; 23 | display: flex; 24 | flex-direction: row; 25 | justify-content: center; 26 | background-color: #111; 27 | } 28 | 29 | body { 30 | width: 800px; 31 | padding: 15px; 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | .site-title { 37 | font-size: 2.5em; 38 | margin-bottom: 10px; 39 | /* text-align: center; */ 40 | } 41 | 42 | nav { 43 | display: flex; 44 | flex-direction: row; 45 | justify-content: left; 46 | font-size: 1.2em; 47 | margin-bottom: 20px; 48 | } 49 | 50 | .block { 51 | border: 1px dotted white; 52 | padding: 5px 15px; 53 | margin: 0 10px; 54 | text-align: center; 55 | display: flex; 56 | flex-direction: column; 57 | justify-content: center; 58 | } 59 | 60 | .block h1 { 61 | font-size: 1em; 62 | text-align: center; 63 | margin-bottom: 0; 64 | } 65 | 66 | .small { 67 | font-size: 0.8em; 68 | } 69 | 70 | .wave { 71 | background: #111; 72 | color: #fff; 73 | text-shadow: 1px 1px 10px #fff, 1px 1px 10px #ccc; 74 | } 75 | 76 | footer { 77 | margin-top: 30px; 78 | display: flex; 79 | flex-direction: column; 80 | align-items: center; 81 | } 82 | 83 | footer hr { 84 | width: 100%; 85 | } -------------------------------------------------------------------------------- /src/cli/init/assets/under-construction.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/src/cli/init/assets/under-construction.gif -------------------------------------------------------------------------------- /src/cli/init/content/about.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "About", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | ## About Zine 10 | Zine is an MIT-licensed project created by [Loris Cro](https://kristoff.it) and 11 | other contributors listed on the [official repository](https://github.com/kristoff-it/zine/contributors). 12 | 13 | Zine is inspired by [Hugo](https://gohugo.io) but features an entirely custom set 14 | of authoring languages: 15 | 16 | - [Scripty](https://zine-ssg.io/docs/scripty/) is the small expression 17 | language that both SuperHTML and SuperMD share to express templating logic. 18 | 19 | - [SuperHTML](https://zine-ssg.io/docs/superhtml/) is the HTML templating 20 | language used by Zine. Unlike most `{{curly braced}}` templating languages, 21 | SuperHTML uses valid HTML syntax to express the templating logic, adding only 22 | minor extensions to normal HTML. 23 | Thanks to this approach, it offers instant 24 | syntax checking and autoformatting via [a CLI tool](https://github.com/kristoff-it/superhtml) as well as Language Server support ([VScode Extension](https://marketplace.visualstudio.com/items?itemName=LorisCro.super)). 25 | 26 | ># [NOTE]($block) 27 | >The correct file extension for SuperHTML templates is `.shtml`. 28 | 29 | - [SuperMD](https://zine-ssg.io/docs/supermd/) is a superset of Markdown 30 | that, instead of relying on inline HTML, offers new constructs for expressing 31 | content embeds without pulling into your content needless layouting concerns. 32 | A CLI tool and language server for SuperMD is in the works. 33 | 34 | ># [NOTE]($block) 35 | >The correct file extension for SuperMD pages is `.smd`. 36 | 37 | ## Zine is alpha software 38 | 39 | Zine is not yet complete. The main functionality is present and you will be able 40 | to build even moderately complex static websites without issue. 41 | 42 | That said using Zine today does imply participating in the development process 43 | to some degree, which usually means inquiring about the development status of 44 | a feature you need, or reporting a bug. 45 | 46 | 47 | Here are some quicklinks related to Zine: 48 | 49 | - Official Website: https://zine-ssg.io/ 50 | - Source Code: https://github.com/kristoff-it/zine/ 51 | - Discord: https://discord.com/invite/B73sGxF 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/cli/init/content/blog/first-post/fanzine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/src/cli/init/content/blog/first-post/fanzine.jpg -------------------------------------------------------------------------------- /src/cli/init/content/blog/first-post/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "First Post: What's A Zine?", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "post.shtml", 6 | .draft = false, 7 | --- 8 | 9 | This is a sample first post for your blog. 10 | 11 | This post is defined in `content/blog/first-post/` and contains the following 12 | files: 13 | 14 | - `index.smd` 15 | - `fanzine.jpg` 16 | 17 | Another interesting thing about this post is that it uses the `layouts/post.shtml` 18 | template which adds at the bottom page navigation within the blog section. 19 | 20 | Enough about Zine, let's talk about zines. 21 | 22 | ## Fanzines 23 | 24 | A zine (short for *fanzine*, from "fan" + "magazine") is a non-professional 25 | publication created by people that want to express themselves in paper form, 26 | usually in relation to a cultural phenomenon of some kind. 27 | 28 | [An example of zine from 1976.](<$image.asset("fanzine.jpg").alt("A photo of a black and white flyer. The main title reads 'A night of pure energy' and the rest of the page contains a mix of pictures of guitarists interleaved with other text snippets, some seemingly taken from a professional publication of some kind, and some handwritten to announce concert night dates, presumably for the musicians in the picture.")>) 29 | 30 | ## Zines in the digital era 31 | 32 | In the digital age, some of the cultural impetus behind zines got redirected to 33 | personal blogs and similar digital, non-professional publications. The 90's and 34 | early 00's are famous for their whacky websites full of clip art, wordart text 35 | and "under construction" animated gifs. 36 | 37 | This initial organic exploration didn't last for too long as the rise of social 38 | media diverted a lot of self-expression energy towards walled gardens, a change 39 | that was also fueled by the much tighter and intense feedback loop that those 40 | platform enable. 41 | 42 | In the 90's the stongest dopamine hit you could get was adding a visit counter 43 | to your altavista website and watch it go up, slowly, mostly because of your own 44 | accesses. In modern times views are almost meaningless and interactions such as 45 | *likes*, *retweets* and *comments* provide much stronger positive feedback. 46 | 47 | An unfortunate side effect of this new cultural wave centered around social media 48 | is that not only you end up gifting your content to platform owners, but you also 49 | participate in a system where the *language* of the social media site shapes your 50 | thoughts and experience in specific, and often user-hostile, ways. 51 | 52 | Art, just like liquids, takes the shape of the container you put it in. A mobile 53 | game that lives off of in app purchases will never be truly great because of the 54 | tension between making the game entertaining enough to keep players engaged, and 55 | the need to make it boring enough so that they will want to buy upgrades to make 56 | the game more fun. 57 | 58 | Similarly, self-expression on Twitter is encouraged to take the shape of short, 59 | hyperbolic hot takes that forgo nuance in order to create catchy quips that can 60 | be used for hasty decision making. 61 | 62 | Likewise, [corpowave]($text.attrs('wave')) never goes out of style on LinkedIn. 63 | Spend enough time in there and you will become a character from Severance. 64 | 65 | ## We're not in 1990 anymore 66 | 67 | Despite all the issues with social media, there is no point in thinking of the 68 | 90's as a better time. It was not. And despite the winks at the past, Zine is 69 | not a tool for indulging in nostalgia. 70 | 71 | **The goal is to make art**: the act of inducing a change in others through 72 | our self-expression. 73 | 74 | You could argue that the 90's excelled at self-expression, but in doing so you 75 | would also have to accept that social media is infinitely more effective at 76 | inducing change in others (albeit at the expense of freedom of expression). 77 | 78 | Once you realize that, the path forward is clear: 79 | 80 | 1. Own your content. 81 | 2. Create new social systems that optimize for creating art over engagement. 82 | 83 | Owning your content means that you will be unaffected by enshittification of 84 | platforms that would otherwise keep your data hostage. It also is the single 85 | most effective thing you can do as an individual to take power away from 86 | platforms, all while protecting your own immediate interests. 87 | 88 | Creation of new social systems is a *slightly more hairy* problem than self 89 | hosting a static website, but it's something that can be done. Over the years 90 | we've had plenty of social outlets that have allowed people to socialize through 91 | their homemade games, music, drawings, fanfics, etc; and chances are that we 92 | have yet many more of these outlets ahead of us to create. 93 | 94 | Zine gives you a small puzzle piece to help you inch closer toward a better 95 | future, partially by providing you with a new iteration over tried and true 96 | patterns (e.g. by facilitating content creation by separating content from 97 | layouting concerns as much as possible), and also by being a bit experimental 98 | with the concept of a devlog, something that you wouldn't normally expect to 99 | find on a static website. 100 | 101 | Lastly, Zine makes sure your content (both blog and devlog, but also any 102 | other content format you might come up with yourself) is available via RSS 103 | syndication. RSS feeds are far from a winning technology in the fight against 104 | the ebb and *enshitty*flow of social media, but they are another small puzzle 105 | piece that costs nothing to maintain and that might turn out to be critical once 106 | enough other preconditions are met. 107 | 108 | With that in mind, **go make art with your words**. 109 | 110 | -- Loris 111 | -------------------------------------------------------------------------------- /src/cli/init/content/blog/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Blog", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "blog.shtml", 6 | .alternatives = [{ 7 | .name = "rss", 8 | .layout = "blog.xml", 9 | .output = "index.xml", 10 | }], 11 | .draft = false, 12 | --- 13 | 14 | This page defines the blog section and lists all posts in it. 15 | 16 | A "site section" in Zine is a group of pages that form a logical subtree of the 17 | website. It's related to directory structure, but it's not an entirely 1:1 mapping. 18 | 19 | What defines a site section in Zine is the presence of `index.smd` files. You 20 | can learn more [in the official Zine docs](https://zine-ssg.io/docs/). 21 | 22 | Take also a look at `layouts/blog.shtml` to get an idea of how to render a page 23 | list in a SuperHTML template. 24 | 25 | The blog section also has an [RSS feed]($link.alternative('rss')). 26 | 27 | In Zine, RSS feeds are considered "alternative" versions of an existing page. In 28 | concrete defines the blog section and that lists all pages in it, is rendered in 29 | two versions: HTML for human readers, and XML for RSS readers. 30 | 31 | This is the SuperMD frontmatter code that defines the RSS feed: 32 | 33 | ```ziggy 34 | .alternatives = [{ 35 | .name = "rss", 36 | .layout = "rss.xml", 37 | .output = "index.xml", 38 | }], 39 | ``` 40 | [(btw syntax highlighting is done statically in Zine, no need for javascript libraries, unless you want to)]($text.attrs('small')) 41 | -------------------------------------------------------------------------------- /src/cli/init/content/blog/second-post.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Second Post", 3 | .date = @date("1990-01-02T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "post.shtml", 6 | .draft = false, 7 | --- 8 | 9 | This second post is mainly here to show you that you can also create single file 10 | posts for convenience. The first post contains more interesting content. 11 | 12 | Don't forget to read [the official SuperMD 13 | docs](https://zine-ssg.io/docs/supermd/) to know how to *style* your content. 14 | 15 | 16 | Btw this sample website also includes the JS/CSS dependencies required to render 17 | math: 18 | 19 | ```=mathtex 20 | \begin{aligned} 21 | f(t) &= \int_{-\infty}^\infty F(\omega) \cdot (-1)^{2 \omega t} \mathrm{d}\omega \\ 22 | F(\omega) &= \int_{-\infty}^\infty f(t) \div (-1)^{2 \omega t} \mathrm{d}t \\ 23 | \end{aligned} 24 | ``` 25 | 26 | This: [`(-1)^x = \cos(\pi x) + i\sin(\pi x)`]($mathtex) is an inline equation 27 | instead! 28 | 29 | -------------------------------------------------------------------------------- /src/cli/init/content/devlog/1989.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Devlog - 1989", 3 | .date = @date("1989-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "devlog.shtml", 6 | .draft = false, 7 | --- 8 | []($section.id('about')) 9 | ## About this Devlog 10 | This is a non-exhaustive, curated list of changes meant to help users quickly see what has improved since they last checked. 11 | 12 | You can [subscribe the latest devlog via RSS]($link.page('devlog').alternative('rss')). 13 | 14 | This page lists entries for the year 1989, for past or future entries consult the 15 | [devlog archive](/devlog/). 16 | 17 | ## [Hello 1989]($section.id("1989-01-01T00:00:00")) 18 | This is a sample entry. 19 | -------------------------------------------------------------------------------- /src/cli/init/content/devlog/1990.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Devlog - 1990", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "devlog.shtml", 6 | .draft = false, 7 | --- 8 | []($section.id('about')) 9 | ## About this Devlog 10 | 11 | This is where you should describe to your users what your devlog is about. 12 | Refer to the corresponding section in the home page to learn more about devlogs. 13 | 14 | For example this is how the Zine devlog describes itself: 15 | 16 | > This is a non-exhaustive, curated list of changes meant to help users quickly see what has improved since they last checked. 17 | > 18 | > You can [subscribe to this page via RSS]($link.page('devlog').alternative('rss')). 19 | > 20 | > This page lists entries for the current year, for past entries consult the 21 | [devlog archive](/devlog/). 22 | 23 | Feel free to tweak it to your specific use case or replace it entirely as you 24 | see fit. 25 | 26 | Regardless of what you decide, you might want to make sure you preserve the 27 | links to the RSS feed and to the devlog archive from the copy above. 28 | 29 | 30 | ## [Third Entry]($section.id("1990-01-03T00:00:00")) 31 | This is the third entry. 32 | 33 | ## [Second Entry]($section.id("1990-01-02T00:00:00")) 34 | This is the second entry. 35 | 36 | ## [Hello Zine]($section.id("1990-01-01T00:00:00")) 37 | This is the first entry in this year's devlog created with 38 | [Zine](https://zine-ssg.io)! 39 | -------------------------------------------------------------------------------- /src/cli/init/content/devlog/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Devlog Archive", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "devlog-archive.shtml", 6 | .alternatives = [{ 7 | .name = "rss", 8 | .layout = "devlog.xml", 9 | .output = "index.xml", 10 | }], 11 | .draft = false, 12 | --- 13 | 14 | This page lists all the devlog years that are present on this site. 15 | Note how the top navigation menu doesn't link to this page, but instead links 16 | to the latest devlog year directly. 17 | 18 | The reason for adopting this structure is to make sure that links that exist in 19 | the wild to entries to our devlog don't become invalidated when a new year comes 20 | around and we rotate the devlog feed. 21 | 22 | Devlog rotation ensures that you don't have a single page that grows 23 | indefinitely, eventually compromising your editing expreience and worsening your 24 | user's browsing experience. You can use any arbitrary policy (even deleting old 25 | entries if you are fine with having a more ephemeral devlog), but cutting them 26 | by year is a good default option. 27 | 28 | When rotating a devlog all you have to do is create a new page with a newer 29 | date set in the frontmatter `date` field, and update the page description to let 30 | people know that this page is not the current year's devlog anymore. 31 | 32 | The latest devlog year is also available [via RSS feed]($link.alternative('rss')). 33 | -------------------------------------------------------------------------------- /src/cli/init/content/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | This sample website showcases: 10 | 11 | - A couple simple pages 12 | - `content/index.smd` 13 | - `content/about.smd` 14 | - A blog 15 | - `content/blog/index.smd` 16 | - `content/blog/first-post/index.smd` 17 | - `content/blog/second-post.smd` 18 | - A devlog 19 | - `content/devlog/index.smd` 20 | - `content/devlog/1990.smd` 21 | - `content/devlog/1989.smd` 22 | 23 | 24 | 25 | ## About Devlogs 26 | 27 | While a blog has each entry be a separate page, a devlog is one single page with 28 | a list of smaller entries in it, making it a form of microblogging. The relative 29 | RSS feed will generate a separate item per devlog entry, creating a 30 | "twitter-like" feed. 31 | 32 | This can be a useful pattern for thoughts that are too small for a full blog post 33 | so consider giving it a try! 34 | 35 | The name "devlog" comes from the fact that this kind of microblogging feed works 36 | well when you have an open source project and you want to give small updates to 37 | your users, but it might work equally well for other domains, depending on your 38 | interests and audience. Maybe a "foodlog", a "catlog" or a "treklog" could also 39 | work well. 40 | 41 | The devlog section contains more information about how devlogs can be implemented 42 | in Zine. 43 | 44 | Some examples of devlogs in the wild: 45 | - https://zine-ssg.io/log/ 46 | - https://ziglang.org/devlog/ 47 | 48 | 49 | ## Next steps 50 | 51 | Make sure to read the [official Zine docs](https://zine-ssg.io/docs/) 52 | and then start editing this website! 53 | 54 | Start by putting the correct information in `zine.ziggy` and then start editing 55 | the existing pages. Before deleting existing copy consider giving it a brief 56 | look as it will show you some SuperMD specific syntax. 57 | 58 | HTML markup in Zine is defined via SuperHTML templates: 59 | - `layouts/index.shtml` 60 | - `layouts/page.shtml` 61 | - `layouts/post.shtml` 62 | - `layouts/blog.shtml` 63 | - `layouts/blog.xml` 64 | - `layouts/devlog.shtml` 65 | - `layouts/devlog.xml` 66 | - `layouts/devlog-archive.shtml` 67 | - `layouts/templates/base.shtml` 68 | 69 | **If you're running the Zine development server (by running `zine`), then all 70 | changes you make will be picked up immediately, causing the website to rebuild 71 | and the page in your browser to refresh**. 72 | 73 | You can learn more about SuperMD and SuperHTML in [/about/](/about/). 74 | 75 | Lastly, this sample website also includes the following asset files: 76 | 77 | - `assets/style.css` 78 | - `assets/hightlight.css` 79 | - `assets/under-construction.gif` 80 | - `assets/katex-tag.js` 81 | - `assets/katex0.16.21.css` 82 | - `assets/katex0.16.21.js` 83 | - `content/blog/first-post/fanzine.jpg` 84 | 85 | The first few asset files are **site assets**, while the last is a **page asset** that belongs to the "first post" page. 86 | 87 | The Zine docs contain more information about [dealing with assets](https://zine-ssg.io/docs/assets/). 88 | 89 | -------------------------------------------------------------------------------- /src/cli/init/layouts/blog.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 |

15 |
16 |
17 |

Post list

18 |
19 | 20 | 21 |

22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /src/cli/init/layouts/blog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zine -- https://zine-ssg.io 7 | en-US 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/cli/init/layouts/devlog-archive.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 |

15 |
16 |
17 |

Past years

18 |
19 | 20 | 21 |

22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /src/cli/init/layouts/devlog.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | 36 |

37 |
38 |
39 |
40 | 41 |

42 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /src/cli/init/layouts/devlog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zine -- https://zine-ssg.io 7 | en-US 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/cli/init/layouts/index.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

6 |
7 | -------------------------------------------------------------------------------- /src/cli/init/layouts/page.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

6 |
7 | -------------------------------------------------------------------------------- /src/cli/init/layouts/post.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 |

25 |
26 |
27 | 33 | 39 |
40 | -------------------------------------------------------------------------------- /src/cli/init/layouts/templates/base.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 29 | 30 |
31 |
32 |
SITE UNDER CONSTRUCTION
33 | 34 |
35 | 36 | -------------------------------------------------------------------------------- /src/cli/init/zine.ziggy: -------------------------------------------------------------------------------- 1 | Site { 2 | .title = "Welcome to Zine!", 3 | .host_url = "https://example.com", 4 | .content_dir_path = "content", 5 | .layouts_dir_path = "layouts", 6 | .assets_dir_path = "assets", 7 | .static_assets = [ 8 | "Temml.woff2", 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/release.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const tracy = @import("tracy"); 4 | const fatal = @import("../fatal.zig"); 5 | const root = @import("../root.zig"); 6 | const worker = @import("../worker.zig"); 7 | const Allocator = std.mem.Allocator; 8 | const BuildAsset = root.BuildAsset; 9 | 10 | pub fn release(gpa: Allocator, args: []const []const u8) bool { 11 | errdefer |err| switch (err) { 12 | error.OutOfMemory => fatal.oom(), 13 | }; 14 | 15 | const cmd: Command = try .parse(gpa, args); 16 | const cfg, const base_dir_path = root.Config.load(gpa); 17 | 18 | worker.start(); 19 | defer if (builtin.mode == .Debug) worker.stopWaitAndDeinit(); 20 | 21 | const build = root.run(gpa, &cfg, .{ 22 | .base_dir_path = base_dir_path, 23 | .build_assets = &cmd.build_assets, 24 | .drafts = cmd.drafts, 25 | .mode = .{ 26 | .disk = .{ 27 | .output_dir_path = cmd.output_dir_path, 28 | }, 29 | }, 30 | }); 31 | 32 | defer if (builtin.mode == .Debug) build.deinit(gpa); 33 | 34 | if (tracy.enable) { 35 | tracy.frameMarkNamed("waiting for tracy"); 36 | var progress_tracy = root.progress.start("Tracy", 0); 37 | std.Thread.sleep(100 * std.time.ns_per_ms); 38 | progress_tracy.end(); 39 | } 40 | 41 | if (build.any_prerendering_error or 42 | build.any_rendering_error.load(.acquire)) 43 | { 44 | return true; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | pub const Command = struct { 51 | output_dir_path: ?[]const u8, 52 | build_assets: std.StringArrayHashMapUnmanaged(BuildAsset), 53 | drafts: bool, 54 | 55 | pub fn deinit(co: *const Command, gpa: Allocator) void { 56 | var ba = co.build_assets; 57 | ba.deinit(gpa); 58 | } 59 | 60 | pub fn parse(gpa: Allocator, args: []const []const u8) !Command { 61 | var output_dir_path: ?[]const u8 = null; 62 | var build_assets: std.StringArrayHashMapUnmanaged(BuildAsset) = .empty; 63 | var drafts = false; 64 | 65 | const eql = std.mem.eql; 66 | const startsWith = std.mem.startsWith; 67 | var idx: usize = 0; 68 | while (idx < args.len) : (idx += 1) { 69 | const arg = args[idx]; 70 | if (eql(u8, arg, "-h") or eql(u8, arg, "--help")) { 71 | fatal.msg(help_message, .{}); 72 | } else if (eql(u8, arg, "-i") or eql(u8, arg, "--install")) { 73 | idx += 1; 74 | if (idx >= args.len) fatal.msg( 75 | "error: missing argument to '{s}'", 76 | .{arg}, 77 | ); 78 | output_dir_path = args[idx]; 79 | } else if (startsWith(u8, arg, "--install=")) { 80 | output_dir_path = arg["--install=".len..]; 81 | } else if (startsWith(u8, arg, "--build-asset=")) { 82 | const name = arg["--build-asset=".len..]; 83 | 84 | idx += 1; 85 | if (idx >= args.len) fatal.msg( 86 | "error: missing build asset sub-argument for '{s}'", 87 | .{name}, 88 | ); 89 | 90 | const input_path = args[idx]; 91 | 92 | idx += 1; 93 | var output_path: ?[]const u8 = null; 94 | var output_always = false; 95 | if (idx < args.len) { 96 | const next = args[idx]; 97 | if (startsWith(u8, next, "--output=")) { 98 | output_path = next["--output=".len..]; 99 | } else if (startsWith(u8, next, "--output-always=")) { 100 | output_always = true; 101 | output_path = next["--output-always=".len..]; 102 | } else { 103 | idx -= 1; 104 | } 105 | } 106 | 107 | const gop = try build_assets.getOrPut(gpa, name); 108 | if (gop.found_existing) fatal.msg( 109 | "error: duplicate build asset name '{s}'", 110 | .{name}, 111 | ); 112 | 113 | gop.value_ptr.* = .{ 114 | .input_path = input_path, 115 | .output_path = output_path, 116 | .output_always = output_always, 117 | .rc = .{ .raw = @intFromBool(output_always) }, 118 | }; 119 | } else if (eql(u8, arg, "--drafts")) { 120 | drafts = true; 121 | } else { 122 | fatal.msg("error: unexpected cli argument '{s}'\n", .{arg}); 123 | } 124 | } 125 | 126 | return .{ 127 | .output_dir_path = output_dir_path, 128 | .build_assets = build_assets, 129 | .drafts = drafts, 130 | }; 131 | } 132 | }; 133 | 134 | const help_message = 135 | \\Usage: zine release [OPTIONS] 136 | \\ 137 | \\Command specific options: 138 | \\ --install DIR Directory where to install the website (default 'public/') 139 | // \\ --build-assets FILE Path to a file containing a list of build assets 140 | \\ --help, -h Show this help menu 141 | \\ 142 | \\ 143 | ; 144 | -------------------------------------------------------------------------------- /src/cli/serve/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

404 not found

8 |

{s}

9 | 10 | -------------------------------------------------------------------------------- /src/cli/serve/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

This page contains errors!

8 |

{s}

9 |

This page should show the exact errors in an overlay.

10 |

If that doesn't work for some reason, the terminal will contain all the info you need.

11 | 12 | -------------------------------------------------------------------------------- /src/cli/serve/outside.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Error

8 |

Outside of the url path prefix!

9 |

This website defines a url_path_prefix set to '{0s}' and you are outside of it.

10 |

Click here to navigate to the url path prefix.

11 | 12 | -------------------------------------------------------------------------------- /src/cli/serve/watcher/MacosWatcher.zig: -------------------------------------------------------------------------------- 1 | const MacosWatcher = @This(); 2 | 3 | const std = @import("std"); 4 | const fatal = @import("../../../fatal.zig"); 5 | const Debouncer = @import("../../serve.zig").Debouncer; 6 | 7 | const c = @cImport({ 8 | @cInclude("CoreServices/CoreServices.h"); 9 | }); 10 | 11 | const log = std.log.scoped(.watcher); 12 | 13 | gpa: std.mem.Allocator, 14 | debouncer: *Debouncer, 15 | dir_paths: []const []const u8, 16 | 17 | pub fn init( 18 | gpa: std.mem.Allocator, 19 | debouncer: *Debouncer, 20 | dir_paths: []const []const u8, 21 | ) MacosWatcher { 22 | return .{ 23 | .gpa = gpa, 24 | .debouncer = debouncer, 25 | .dir_paths = dir_paths, 26 | }; 27 | } 28 | 29 | pub fn start(watcher: *MacosWatcher) !void { 30 | const t = try std.Thread.spawn(.{}, MacosWatcher.listen, .{watcher}); 31 | t.detach(); 32 | } 33 | 34 | pub fn listen(watcher: *MacosWatcher) void { 35 | errdefer |err| switch (err) { 36 | error.OutOfMemory => fatal.oom(), 37 | }; 38 | 39 | const macos_paths = try watcher.gpa.alloc( 40 | c.CFStringRef, 41 | watcher.dir_paths.len, 42 | ); 43 | defer watcher.gpa.free(macos_paths); 44 | 45 | for (watcher.dir_paths, macos_paths) |str, *ref| { 46 | ref.* = c.CFStringCreateWithCString( 47 | null, 48 | str.ptr, 49 | c.kCFStringEncodingUTF8, 50 | ); 51 | } 52 | 53 | const paths_to_watch: c.CFArrayRef = c.CFArrayCreate( 54 | null, 55 | @ptrCast(macos_paths.ptr), 56 | @intCast(macos_paths.len), 57 | null, 58 | ); 59 | 60 | var stream_context: c.FSEventStreamContext = .{ .info = watcher }; 61 | const stream: c.FSEventStreamRef = c.FSEventStreamCreate( 62 | null, 63 | &macosCallback, 64 | &stream_context, 65 | paths_to_watch, 66 | c.kFSEventStreamEventIdSinceNow, 67 | 0.05, 68 | c.kFSEventStreamCreateFlagFileEvents, 69 | ); 70 | 71 | c.FSEventStreamScheduleWithRunLoop( 72 | stream, 73 | c.CFRunLoopGetCurrent(), 74 | c.kCFRunLoopDefaultMode, 75 | ); 76 | 77 | if (c.FSEventStreamStart(stream) == 0) { 78 | fatal.msg("error: macos watcher FSEventStreamStart failed", .{}); 79 | } 80 | 81 | c.CFRunLoopRun(); 82 | 83 | c.FSEventStreamStop(stream); 84 | c.FSEventStreamInvalidate(stream); 85 | c.FSEventStreamRelease(stream); 86 | 87 | c.CFRelease(paths_to_watch); 88 | } 89 | 90 | pub fn macosCallback( 91 | streamRef: c.ConstFSEventStreamRef, 92 | clientCallBackInfo: ?*anyopaque, 93 | numEvents: usize, 94 | eventPaths: ?*anyopaque, 95 | eventFlags: ?[*]const c.FSEventStreamEventFlags, 96 | eventIds: ?[*]const c.FSEventStreamEventId, 97 | ) callconv(.C) void { 98 | _ = eventIds; 99 | _ = eventFlags; 100 | _ = streamRef; 101 | const watcher: *MacosWatcher = @alignCast(@ptrCast(clientCallBackInfo)); 102 | 103 | const paths: [*][*:0]u8 = @alignCast(@ptrCast(eventPaths)); 104 | for (paths[0..numEvents]) |p| { 105 | const path = std.mem.span(p); 106 | log.debug("Changed: {s}\n", .{path}); 107 | 108 | // const basename = std.fs.path.basename(path); 109 | // var base_path = path[0 .. path.len - basename.len]; 110 | // if (std.mem.endsWith(u8, base_path, "/")) 111 | // base_path = base_path[0 .. base_path.len - 1]; 112 | watcher.debouncer.newEvent(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/cli/serve/zinereload.js: -------------------------------------------------------------------------------- 1 | function log() { 2 | console.log("[Zine Reloader]", ...arguments); 3 | } 4 | 5 | function zineConnect() { 6 | let socket = new WebSocket("ws://" + window.location.host + "/__zine/ws"); 7 | 8 | socket.addEventListener("open", (event) => { 9 | log("connected"); 10 | }); 11 | 12 | // Listen for messages 13 | socket.addEventListener("message", (event) => { 14 | const msg = JSON.parse(event.data); 15 | 16 | if (msg.command == "reload_all") { 17 | location.reload(); 18 | } 19 | 20 | if (msg.command == "reload") { 21 | log("reload", msg.path); 22 | 23 | if (msg.path.endsWith(".html")) { 24 | let path = window.location.pathname; 25 | if (path.endsWith('/')) path = path + 'index.html'; 26 | 27 | if (path == msg.path) { 28 | location.reload(); 29 | } 30 | } else if (msg.path.endsWith(".css")) { 31 | const links = document.querySelectorAll("link"); 32 | for (let i = 0; i < links.length; i++) { 33 | const link = links[i]; 34 | if (link._zine_temp) continue; 35 | 36 | let url = new URL(link.href); 37 | if (url.pathname == msg.path) { 38 | const now = Date.now(); 39 | url.search = now; 40 | let copy = link.cloneNode(false); 41 | copy.href = url; 42 | link._zine_temp = true; 43 | link.parentElement.appendChild(copy); 44 | setTimeout(function(){ 45 | link.remove(); 46 | }, 200); 47 | 48 | break; 49 | } 50 | } 51 | } else if (msg.path.match(/\.(jpe?g|png|gif|svg|webp)$/i)) { 52 | for (let i = 0; i < document.images.length; i++) { 53 | const img = document.images[i]; 54 | let url = new URL(img.src); 55 | if (url.pathname == msg.path) { 56 | const now = Date.now(); 57 | url.search = now; 58 | img.src = url; 59 | } 60 | } 61 | 62 | for (let i = 0; i < document.styleSheets.length; i++) { 63 | const style = document.styleSheets[i]; 64 | log("TODO: implement image reload in stylesheets"); 65 | } 66 | 67 | const inlines = this.document.querySelectorAll("[style*=background]"); 68 | for (let i = 0; i < inlines.length; i++) { 69 | const style = inlines[i]; 70 | log("TODO: implement image reload in inline stylesheets"); 71 | } 72 | } 73 | } else if (msg.command == "build"){ 74 | const id = "__zine_build_box"; 75 | if(msg.err != "") { 76 | let box = document.getElementById(id); 77 | if (box == null) { 78 | box = document.createElement("pre"); 79 | box.style = "position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;color: white; background-color: black;z-index:100; overflow-y: scroll; margin: 0; padding: 5px; font-family: monospace;"; 80 | box.id = id; 81 | document.body.appendChild(box); 82 | box.innerHTML = "

ZINE BUILD ERROR

" ; 83 | } 84 | 85 | box.innerHTML += "\n\n" + msg.err.replace(/ { 97 | log("close", event); 98 | setTimeout(zineConnect, 3000); 99 | }); 100 | 101 | socket.addEventListener("error", (event) => { 102 | log("error", event); 103 | }); 104 | return socket; 105 | } 106 | 107 | { 108 | const socket = zineConnect(); 109 | 110 | // Keep sending messages to circumvent an issue related to windows 111 | // networking, see https://github.com/ziglang/zig/issues/14233 112 | function zinewin() { 113 | if (socket.readyState === WebSocket.OPEN) { 114 | socket.send("https://github.com/ziglang/zig/issues/14233"); 115 | } 116 | } 117 | setInterval(zinewin, 100); 118 | } 119 | -------------------------------------------------------------------------------- /src/context/Bool.zig: -------------------------------------------------------------------------------- 1 | const Bool = @This(); 2 | 3 | const std = @import("std"); 4 | const utils = @import("utils.zig"); 5 | const context = @import("../context.zig"); 6 | const Signature = @import("doctypes.zig").Signature; 7 | const Allocator = std.mem.Allocator; 8 | const Value = context.Value; 9 | const String = context.String; 10 | 11 | value: bool, 12 | 13 | pub fn init(b: bool) Value { 14 | return .{ .bool = .{ .value = b } }; 15 | } 16 | 17 | fn not(b: Bool) Value { 18 | return .{ .bool = .{ .value = !b.value } }; 19 | } 20 | 21 | pub const True = Bool.init(true); 22 | pub const False = Bool.init(false); 23 | 24 | pub const PassByRef = false; 25 | pub const docs_description = "A boolean value"; 26 | pub const Builtins = struct { 27 | pub const then = struct { 28 | pub const signature: Signature = .{ 29 | .params = &.{ .String, .{ .Opt = .String } }, 30 | .ret = .String, 31 | }; 32 | pub const docs_description = 33 | \\If the boolean is `true`, returns the first argument. 34 | \\Otherwise, returns the second argument. 35 | \\ 36 | \\The second argument defaults to an empty string. 37 | \\ 38 | ; 39 | pub const examples = 40 | \\$page.draft.then("DRAFT!") 41 | ; 42 | pub fn call( 43 | b: Bool, 44 | _: Allocator, 45 | _: *const context.Template, 46 | args: []const Value, 47 | ) !Value { 48 | if (args.len < 1 or args.len > 2) return .{ 49 | .err = "expected 1 or 2 string arguments", 50 | }; 51 | 52 | if (b.value) { 53 | return args[0]; 54 | } else { 55 | if (args.len < 2) return String.init(""); 56 | return args[1]; 57 | } 58 | } 59 | }; 60 | pub const not = struct { 61 | pub const signature: Signature = .{ .ret = .Bool }; 62 | pub const docs_description = 63 | \\Negates a boolean value. 64 | \\ 65 | ; 66 | pub const examples = 67 | \\$page.draft.not() 68 | ; 69 | pub fn call( 70 | b: Bool, 71 | _: Allocator, 72 | _: *const context.Template, 73 | args: []const Value, 74 | ) !Value { 75 | if (args.len != 0) return .{ .err = "expected 0 arguments" }; 76 | return b.not(); 77 | } 78 | }; 79 | pub const @"and" = struct { 80 | pub const signature: Signature = .{ 81 | .params = &.{ .Bool, .{ .Many = .Bool } }, 82 | .ret = .Bool, 83 | }; 84 | 85 | pub const docs_description = 86 | \\Computes logical `and` between the receiver value and any other 87 | \\value passed as argument. 88 | ; 89 | pub const examples = 90 | \\$page.draft.and($site.tags.len().eq(10)) 91 | ; 92 | pub fn call( 93 | b: Bool, 94 | _: Allocator, 95 | _: *const context.Template, 96 | args: []const Value, 97 | ) !Value { 98 | if (args.len == 0) return .{ .err = "expected 1 or more boolean argument(s)" }; 99 | for (args) |a| switch (a) { 100 | .bool => {}, 101 | else => return .{ .err = "wrong argument type" }, 102 | }; 103 | if (!b.value) return False; 104 | for (args) |a| if (!a.bool.value) return False; 105 | 106 | return True; 107 | } 108 | }; 109 | pub const @"or" = struct { 110 | pub const signature: Signature = .{ 111 | .params = &.{ .Bool, .{ .Many = .Bool } }, 112 | .ret = .Bool, 113 | }; 114 | pub const docs_description = 115 | \\Computes logical `or` between the receiver value and any other value passed as argument. 116 | \\ 117 | ; 118 | pub const examples = 119 | \\$page.draft.or($site.tags.len().eq(0)) 120 | ; 121 | pub fn call( 122 | b: Bool, 123 | _: Allocator, 124 | _: *const context.Template, 125 | args: []const Value, 126 | ) !Value { 127 | if (args.len == 0) return .{ .err = "'or' wants at least one argument" }; 128 | for (args) |a| switch (a) { 129 | .bool => {}, 130 | else => return .{ .err = "wrong argument type" }, 131 | }; 132 | if (b.value) return True; 133 | for (args) |a| if (a.bool.value) return True; 134 | 135 | return False; 136 | } 137 | }; 138 | }; 139 | -------------------------------------------------------------------------------- /src/context/Build.zig: -------------------------------------------------------------------------------- 1 | const Build = @This(); 2 | 3 | const std = @import("std"); 4 | const scripty = @import("scripty"); 5 | const utils = @import("utils.zig"); 6 | const context = @import("../context.zig"); 7 | const Signature = @import("doctypes.zig").Signature; 8 | const Allocator = std.mem.Allocator; 9 | const Value = context.Value; 10 | const Optional = context.Optional; 11 | const uninitialized = utils.uninitialized; 12 | 13 | pub const dot = scripty.defaultDot(Build, Value, false); 14 | pub const PassByRef = true; 15 | 16 | generated: context.DateTime, 17 | _git_data_path: []const u8, 18 | _git: context.Git, 19 | 20 | pub fn init( 21 | git_data_path: []const u8, 22 | git: context.Git, 23 | ) Build { 24 | return .{ 25 | .generated = context.DateTime.initNow(), 26 | ._git_data_path = git_data_path, 27 | ._git = git, 28 | }; 29 | } 30 | 31 | pub const docs_description = 32 | \\Gives you access to build-time assets and other build related info. 33 | \\When inside of a git repository it also gives git-related metadata. 34 | ; 35 | 36 | pub const Fields = struct { 37 | pub const generated = 38 | \\Returns the current date when the build is taking place. 39 | \\ 40 | \\># [Note]($block.attrs('note')) 41 | \\>Using this function will not add a dependency on the current time 42 | \\>for the page, hence the name `generated`. 43 | \\> 44 | \\>To get the best results, use in conjunction with caching as otherwise 45 | \\>the page will be regenerated anew every single time. 46 | ; 47 | }; 48 | 49 | pub const Builtins = struct { 50 | pub const asset = struct { 51 | pub const signature: Signature = .{ 52 | .params = &.{.String}, 53 | .ret = .Asset, 54 | }; 55 | pub const docs_description = 56 | \\Retuns a build-time asset (i.e. an asset generated through your 'build.zig' file) by name. 57 | ; 58 | pub const examples = 59 | \\
60 | ; 61 | pub fn call( 62 | _: *const Build, 63 | gpa: Allocator, 64 | ctx: *const context.Template, 65 | args: []const Value, 66 | ) !Value { 67 | const bad_arg: Value = .{ 68 | .err = "expected 1 string argument", 69 | }; 70 | if (args.len != 1) return bad_arg; 71 | 72 | const ref = switch (args[0]) { 73 | .string => |s| s.value, 74 | else => return bad_arg, 75 | }; 76 | 77 | const ok = ctx._meta.build.build_assets.contains(ref); 78 | if (!ok) return Value.errFmt(gpa, "unknown build asset '{s}'", .{ 79 | ref, 80 | }); 81 | 82 | return .{ 83 | .asset = .{ 84 | ._meta = .{ 85 | .ref = ref, 86 | .kind = .build, 87 | .url = undefined, 88 | }, 89 | }, 90 | }; 91 | } 92 | }; 93 | 94 | pub const git = struct { 95 | pub const signature: Signature = .{ .ret = .Git }; 96 | pub const docs_description = 97 | \\Returns git-related metadata if you are inside a git repository. 98 | \\If you are not or the parsing failes, it will return an error. 99 | \\Packed object are not supported, commit anything to get the metadata. 100 | ; 101 | pub const examples = 102 | \\
103 | ; 104 | pub fn call( 105 | build: *const Build, 106 | _: Allocator, 107 | _: *const context.Template, 108 | args: []const Value, 109 | ) Value { 110 | const bad_arg: Value = .{ 111 | .err = "expected 0 arguments", 112 | }; 113 | if (args.len != 0) return bad_arg; 114 | 115 | return if (build._git._in_repo) .{ 116 | .git = build._git, 117 | } else .{ 118 | .err = "Not in a git repository", 119 | }; 120 | } 121 | }; 122 | 123 | pub const @"git?" = struct { 124 | pub const signature: Signature = .{ .ret = .Git }; 125 | pub const docs_description = 126 | \\Returns git-related metadata if you are inside a git repository. 127 | \\If you are not or the parsing failes, it will return null. 128 | \\Packed object are not supported, commit anything to get the metadata. 129 | ; 130 | pub const examples = 131 | \\
...
132 | ; 133 | pub fn call( 134 | build: *const Build, 135 | gpa: Allocator, 136 | _: *const context.Template, 137 | args: []const Value, 138 | ) !Value { 139 | const bad_arg: Value = .{ 140 | .err = "expected 0 arguments", 141 | }; 142 | if (args.len != 0) return bad_arg; 143 | 144 | return if (build._git._in_repo) 145 | Optional.init(gpa, build._git) 146 | else 147 | Optional.Null; 148 | } 149 | }; 150 | }; 151 | -------------------------------------------------------------------------------- /src/context/Float.zig: -------------------------------------------------------------------------------- 1 | const Float = @This(); 2 | 3 | f: f64, 4 | 5 | pub const PassByRef = false; 6 | pub const docs_description = "A 64bit float value."; 7 | pub const Builtins = struct {}; 8 | -------------------------------------------------------------------------------- /src/context/Git.zig: -------------------------------------------------------------------------------- 1 | const Git = @This(); 2 | 3 | const std = @import("std"); 4 | const builtin = @import("builtin"); 5 | const scripty = @import("scripty"); 6 | const context = @import("../context.zig"); 7 | const Signature = @import("doctypes.zig").Signature; 8 | const Allocator = std.mem.Allocator; 9 | const DateTime = context.DateTime; 10 | const String = context.String; 11 | const Optional = context.Optional; 12 | const Bool = context.Bool; 13 | const Value = context.Value; 14 | 15 | pub const dot = scripty.defaultDot(Git, Value, false); 16 | 17 | _in_repo: bool = false, 18 | 19 | commit_hash: []const u8 = undefined, 20 | commit_date: DateTime = undefined, 21 | commit_message: []const u8 = undefined, 22 | author_name: []const u8 = undefined, 23 | author_email: []const u8 = undefined, 24 | 25 | _tag: ?[]const u8 = null, 26 | _branch: ?[]const u8 = null, 27 | 28 | pub const docs_description = 29 | \\Information about the current git repository. 30 | ; 31 | 32 | pub const Fields = struct { 33 | pub const commit_hash = 34 | \\The current commit hash. 35 | ; 36 | pub const commit_date = 37 | \\The date of the current commit. 38 | ; 39 | pub const commit_message = 40 | \\The commit message of the current commit. 41 | ; 42 | pub const author_name = 43 | \\The name of the author of the current commit. 44 | ; 45 | pub const author_email = 46 | \\The email of the author of the current commit. 47 | ; 48 | }; 49 | 50 | pub const Builtins = struct { 51 | pub const tag = struct { 52 | pub const signature: Signature = .{ .ret = .String }; 53 | pub const docs_description = 54 | \\Returns the tag of the current commit. 55 | \\If the current commit does not have a tag, an error is returned. 56 | ; 57 | pub const examples = 58 | \\
59 | \\
60 | ; 61 | pub fn call( 62 | git: Git, 63 | gpa: Allocator, 64 | _: *const context.Template, 65 | args: []const Value, 66 | ) !Value { 67 | const bad_arg: Value = .{ 68 | .err = "expected 0 arguments", 69 | }; 70 | if (args.len != 0) return bad_arg; 71 | 72 | return if (git._tag) |_tag| Value.from(gpa, _tag) else .{ .err = "No tag for this commit" }; 73 | } 74 | }; 75 | 76 | pub const @"tag?" = struct { 77 | pub const signature: Signature = .{ .ret = .String }; 78 | pub const docs_description = 79 | \\Returns the tag of the current commit. 80 | \\If the current commit does not have a tag, null is returned. 81 | ; 82 | pub const examples = 83 | \\
84 | \\
85 | ; 86 | pub fn call( 87 | git: Git, 88 | gpa: Allocator, 89 | _: *const context.Template, 90 | args: []const Value, 91 | ) !Value { 92 | const bad_arg: Value = .{ 93 | .err = "expected 0 arguments", 94 | }; 95 | if (args.len != 0) return bad_arg; 96 | 97 | return if (git._tag) |_tag| Optional.init(gpa, _tag) else Optional.Null; 98 | } 99 | }; 100 | 101 | pub const branch = struct { 102 | pub const signature: Signature = .{ .ret = .String }; 103 | pub const docs_description = 104 | \\Returns the branch of the current commit. 105 | \\If the current commit does not have a branch, an error is returned. 106 | ; 107 | pub const examples = 108 | \\
109 | \\
110 | ; 111 | pub fn call( 112 | git: Git, 113 | gpa: Allocator, 114 | _: *const context.Template, 115 | args: []const Value, 116 | ) !Value { 117 | const bad_arg: Value = .{ 118 | .err = "expected 0 arguments", 119 | }; 120 | if (args.len != 0) return bad_arg; 121 | 122 | return if (git._branch) |_branch| Value.from(gpa, _branch) else .{ .err = "No branch for this commit" }; 123 | } 124 | }; 125 | 126 | pub const @"branch?" = struct { 127 | pub const signature: Signature = .{ .ret = .String }; 128 | pub const docs_description = 129 | \\Returns the branch of the current commit. 130 | \\If the current commit does not have a branch, null is returned. 131 | ; 132 | pub const examples = 133 | \\
134 | \\
135 | ; 136 | pub fn call( 137 | git: Git, 138 | gpa: Allocator, 139 | _: *const context.Template, 140 | args: []const Value, 141 | ) !Value { 142 | const bad_arg: Value = .{ 143 | .err = "expected 0 arguments", 144 | }; 145 | if (args.len != 0) return bad_arg; 146 | 147 | return if (git._branch) |_branch| Optional.init(gpa, _branch) else Optional.Null; 148 | } 149 | }; 150 | }; 151 | -------------------------------------------------------------------------------- /src/context/Int.zig: -------------------------------------------------------------------------------- 1 | const Int = @This(); 2 | 3 | const std = @import("std"); 4 | const utils = @import("utils.zig"); 5 | const context = @import("../context.zig"); 6 | const Signature = @import("doctypes.zig").Signature; 7 | const Allocator = std.mem.Allocator; 8 | const Value = context.Value; 9 | const Bool = context.Bool; 10 | const String = context.String; 11 | 12 | value: i64, 13 | 14 | pub fn init(i: i64) Value { 15 | return .{ .int = .{ .value = i } }; 16 | } 17 | 18 | pub const PassByRef = false; 19 | pub const docs_description = "A signed 64-bit integer."; 20 | pub const Builtins = struct { 21 | pub const eq = struct { 22 | pub const signature: Signature = .{ 23 | .params = &.{.Int}, 24 | .ret = .Bool, 25 | }; 26 | pub const docs_description = 27 | \\Tests if two integers have the same value. 28 | \\ 29 | ; 30 | pub const examples = 31 | \\$page.wordCount().eq(200) 32 | ; 33 | pub fn call( 34 | int: Int, 35 | _: Allocator, 36 | _: *const context.Template, 37 | args: []const Value, 38 | ) !Value { 39 | const argument_error: Value = .{ .err = "'plus' wants one int argument" }; 40 | if (args.len != 1) return argument_error; 41 | 42 | switch (args[0]) { 43 | .int => |rhs| return Bool.init(int.value == rhs.value), 44 | else => return argument_error, 45 | } 46 | } 47 | }; 48 | pub const gt = struct { 49 | pub const signature: Signature = .{ 50 | .params = &.{.Int}, 51 | .ret = .Bool, 52 | }; 53 | pub const docs_description = 54 | \\Returns true if lhs is greater than rhs (the argument). 55 | \\ 56 | ; 57 | pub const examples = 58 | \\$page.wordCount().gt(200) 59 | ; 60 | pub fn call( 61 | int: Int, 62 | _: Allocator, 63 | _: *const context.Template, 64 | args: []const Value, 65 | ) !Value { 66 | const argument_error: Value = .{ .err = "'gt' wants one int argument" }; 67 | if (args.len != 1) return argument_error; 68 | 69 | switch (args[0]) { 70 | .int => |rhs| return Bool.init(int.value > rhs.value), 71 | else => return argument_error, 72 | } 73 | } 74 | }; 75 | 76 | pub const plus = struct { 77 | pub const signature: Signature = .{ 78 | .params = &.{.Int}, 79 | .ret = .Int, 80 | }; 81 | pub const docs_description = 82 | \\Sums two integers. 83 | \\ 84 | ; 85 | pub const examples = 86 | \\$page.wordCount().plus(10) 87 | ; 88 | pub fn call( 89 | int: Int, 90 | _: Allocator, 91 | _: *const context.Template, 92 | args: []const Value, 93 | ) !Value { 94 | const argument_error: Value = .{ .err = "expected 1 int argument" }; 95 | if (args.len != 1) return argument_error; 96 | 97 | switch (args[0]) { 98 | .int => |add| return Int.init(int.value +| add.value), 99 | .float => @panic("TODO: int with float argument"), 100 | else => return argument_error, 101 | } 102 | } 103 | }; 104 | pub const div = struct { 105 | pub const signature: Signature = .{ 106 | .params = &.{.Int}, 107 | .ret = .Int, 108 | }; 109 | pub const docs_description = 110 | \\Divides the receiver by the argument. 111 | \\ 112 | ; 113 | pub const examples = 114 | \\$page.wordCount().div(10) 115 | ; 116 | pub fn call( 117 | int: Int, 118 | _: Allocator, 119 | _: *const context.Template, 120 | args: []const Value, 121 | ) !Value { 122 | const argument_error: Value = .{ .err = "'div' wants one (int|float) argument" }; 123 | if (args.len != 1) return argument_error; 124 | 125 | switch (args[0]) { 126 | .int => |den| { 127 | const res = std.math.divTrunc(i64, int.value, den.value) catch |err| { 128 | return .{ .err = @errorName(err) }; 129 | }; 130 | 131 | return Int.init(res); 132 | }, 133 | .float => @panic("TODO: div with float argument"), 134 | else => return argument_error, 135 | } 136 | } 137 | }; 138 | 139 | pub const byteSize = struct { 140 | pub const signature: Signature = .{ .ret = .String }; 141 | pub const docs_description = 142 | \\Turns a raw number of bytes into a human readable string that 143 | \\appropriately uses Kilo, Mega, Giga, etc. 144 | \\ 145 | ; 146 | pub const examples = 147 | \\$page.asset('photo.jpg').size().byteSize() 148 | ; 149 | pub fn call( 150 | int: Int, 151 | gpa: Allocator, 152 | _: *const context.Template, 153 | args: []const Value, 154 | ) !Value { 155 | if (args.len != 0) return .{ .err = "expected 0 arguments" }; 156 | 157 | const size: usize = if (int.value > 0) @intCast(int.value) else return Value.errFmt( 158 | gpa, 159 | "cannot represent {} (a negative value) as a size", 160 | .{int.value}, 161 | ); 162 | 163 | return String.init(try std.fmt.allocPrint(gpa, "{:.0}", .{ 164 | std.fmt.fmtIntSizeBin(size), 165 | })); 166 | } 167 | }; 168 | 169 | pub const str = struct { 170 | pub const signature: Signature = .{ .ret = .String }; 171 | pub const docs_description = 172 | \\Converts the number into a string, so that can be used for 173 | \\functions that require a string argument. 174 | ; 175 | 176 | pub const examples = 177 | \\$i18n.get!("current_page").fmt($loop.idx.str()) 178 | ; 179 | 180 | pub fn call( 181 | int: Int, 182 | gpa: Allocator, 183 | _: *const context.Template, 184 | args: []const Value, 185 | ) !Value { 186 | if (args.len != 0) return .{ .err = "expected 0 arguments" }; 187 | return String.init(try std.fmt.allocPrint(gpa, "{}", .{int.value})); 188 | } 189 | }; 190 | }; 191 | -------------------------------------------------------------------------------- /src/context/Iterator.zig: -------------------------------------------------------------------------------- 1 | const Iterator = @This(); 2 | 3 | const std = @import("std"); 4 | const ziggy = @import("ziggy"); 5 | const superhtml = @import("superhtml"); 6 | const scripty = @import("scripty"); 7 | const context = @import("../context.zig"); 8 | const doctypes = @import("doctypes.zig"); 9 | const Signature = doctypes.Signature; 10 | const Allocator = std.mem.Allocator; 11 | const Value = context.Value; 12 | const Template = context.Template; 13 | const Site = context.Site; 14 | const Page = context.Page; 15 | const Map = context.Map; 16 | const Array = context.Array; 17 | 18 | it: Value = undefined, 19 | idx: usize = 0, 20 | first: bool = undefined, 21 | last: bool = undefined, 22 | len: usize, 23 | 24 | _superhtml_context: superhtml.utils.IteratorContext(Value, Template) = .{}, 25 | _impl: Impl, 26 | 27 | pub const Impl = union(enum) { 28 | value_it: SliceIterator(Value), 29 | 30 | pub fn len(impl: Impl) usize { 31 | switch (impl) { 32 | inline else => |v| return v.len(), 33 | } 34 | } 35 | }; 36 | 37 | pub fn init(gpa: Allocator, impl: Impl) !*Iterator { 38 | const res = try gpa.create(Iterator); 39 | res.* = .{ ._impl = impl, .len = impl.len() }; 40 | return res; 41 | } 42 | 43 | pub fn deinit(iter: *const Iterator, gpa: Allocator) void { 44 | gpa.destroy(iter); 45 | } 46 | 47 | pub fn next(iter: *Iterator, gpa: Allocator) !bool { 48 | switch (iter._impl) { 49 | inline else => |*v| { 50 | const item = try v.next(gpa); 51 | iter.it = try Value.from(gpa, item orelse return false); 52 | iter.idx += 1; 53 | iter.first = iter.idx == 1; 54 | iter.last = iter.idx == iter.len; 55 | return true; 56 | }, 57 | } 58 | } 59 | 60 | pub fn fromArray(gpa: Allocator, arr: Array) !*Iterator { 61 | return init(gpa, .{ 62 | .value_it = .{ .items = arr._items }, 63 | }); 64 | } 65 | 66 | pub const dot = scripty.defaultDot(Iterator, Value, false); 67 | pub const docs_description = "An iterator."; 68 | pub const Fields = struct { 69 | pub const it = 70 | \\The current iteration variable. 71 | ; 72 | pub const idx = 73 | \\The current iteration index. 74 | ; 75 | pub const len = 76 | \\The length of the sequence being iterated. 77 | ; 78 | pub const first = 79 | \\True on the first iteration loop. 80 | ; 81 | pub const last = 82 | \\True on the last iteration loop. 83 | ; 84 | }; 85 | pub const Builtins = struct { 86 | pub const up = struct { 87 | pub const signature: Signature = .{ .ret = .Iterator }; 88 | pub const docs_description = 89 | \\In nested loops, accesses the upper `$loop` 90 | \\ 91 | ; 92 | pub const examples = 93 | \\$loop.up().it 94 | ; 95 | pub fn call( 96 | it: *Iterator, 97 | _: Allocator, 98 | _: *const context.Template, 99 | args: []const Value, 100 | ) !Value { 101 | const bad_arg: Value = .{ .err = "expected 0 arguments" }; 102 | if (args.len != 0) return bad_arg; 103 | return it._superhtml_context.up(); 104 | } 105 | }; 106 | }; 107 | 108 | fn SliceIterator(comptime Element: type) type { 109 | return struct { 110 | idx: usize = 0, 111 | items: []const Element, 112 | 113 | pub fn len(self: @This()) usize { 114 | return self.items.len; 115 | } 116 | 117 | pub fn next(self: *@This(), gpa: Allocator) !?Element { 118 | _ = gpa; 119 | if (self.idx == self.items.len) return null; 120 | defer self.idx += 1; 121 | return self.items[self.idx]; 122 | } 123 | }; 124 | } 125 | -------------------------------------------------------------------------------- /src/context/Optional.zig: -------------------------------------------------------------------------------- 1 | const Optional = @This(); 2 | 3 | const std = @import("std"); 4 | const context = @import("../context.zig"); 5 | const Allocator = std.mem.Allocator; 6 | const Value = context.Value; 7 | 8 | value: Value, 9 | 10 | pub const Null: Value = .{ .optional = null }; 11 | pub fn init(gpa: Allocator, v: anytype) !Value { 12 | const box = try gpa.create(Optional); 13 | box.value = try Value.from(gpa, v); 14 | return .{ .optional = box }; 15 | } 16 | 17 | // pub fn dot(opt: Optional, gpa: Allocator, path: []const u8) !Value { 18 | // _ = opt; 19 | // _ = gpa; 20 | // _ = path; 21 | // return .{ .err = "todo" }; 22 | // } 23 | pub const PassByRef = false; 24 | pub const docs_description = "An optional value, to be used in conjunction with `if` attributes."; 25 | pub const Builtins = struct {}; 26 | -------------------------------------------------------------------------------- /src/context/Slice.zig: -------------------------------------------------------------------------------- 1 | const Slice = @This(); 2 | 3 | const std = @import("std"); 4 | const context = @import("../context.zig"); 5 | const Allocator = std.mem.Allocator; 6 | const Value = context.Value; 7 | 8 | value: []const Value, 9 | 10 | pub fn dot(s: Slice, gpa: Allocator, path: []const u8) !Value { 11 | _ = s; 12 | _ = gpa; 13 | _ = path; 14 | return .{ .err = "todo" }; 15 | } 16 | pub const docs_description = "TODO"; 17 | pub const Builtins = struct {}; 18 | -------------------------------------------------------------------------------- /src/context/Template.zig: -------------------------------------------------------------------------------- 1 | const Template = @This(); 2 | 3 | const std = @import("std"); 4 | const superhtml = @import("superhtml"); 5 | const scripty = @import("scripty"); 6 | const ziggy = @import("ziggy"); 7 | const ZineBuild = @import("../Build.zig"); 8 | const context = @import("../context.zig"); 9 | const Value = context.Value; 10 | const Site = context.Site; 11 | const Page = context.Page; 12 | const Build = context.Build; 13 | const Map = context.Map; 14 | const Iterator = context.Iterator; 15 | const Optional = context.Optional; 16 | const Ctx = superhtml.utils.Ctx; 17 | 18 | site: *const Site, 19 | page: *const Page, 20 | build: Build, 21 | i18n: Map.ZiggyMap, 22 | 23 | _meta: struct { 24 | build: *const ZineBuild, 25 | // Indexed by language code, empty when building a simple site 26 | // Get by key when you have a language code, get by idx when you 27 | // have a variant_id. 28 | sites: *const std.StringArrayHashMapUnmanaged(Site), 29 | }, 30 | 31 | // Globals specific to SuperHTML 32 | ctx: Ctx(Value) = .{}, 33 | loop: ?*Iterator = null, 34 | @"if": ?*const Optional = null, 35 | 36 | pub fn printLinkPrefix( 37 | ctx: *const Template, 38 | w: anytype, 39 | other_variant_id: u32, 40 | /// When set to true the full host url will be always printed 41 | /// otherwise it will only be added in multilingual websites when 42 | /// linking to content across variants that have different host url 43 | /// overrides. 44 | force_host_url: bool, 45 | ) error{OutOfMemory}!void { 46 | const other_site = ctx._meta.sites.entries.items(.value)[other_variant_id]; 47 | switch (other_site._meta.kind) { 48 | .simple => |url_path_prefix| { 49 | if (force_host_url) try w.print("{s}", .{ 50 | ctx._meta.build.cfg.Site.host_url, 51 | }); 52 | if (url_path_prefix.len > 0) { 53 | try w.print("/{s}/", .{url_path_prefix}); 54 | } else { 55 | try w.writeAll("/"); 56 | } 57 | }, 58 | .multi => |loc| { 59 | const our_variant_id = ctx.page._scan.variant_id; 60 | if (other_variant_id != our_variant_id) { 61 | const sites = ctx._meta.sites.entries.items(.value); 62 | const our_host_url = sites[our_variant_id].host_url; 63 | const other_host_url = sites[other_variant_id].host_url; 64 | if (force_host_url or our_host_url.ptr != other_host_url.ptr) { 65 | try w.print("{s}", .{other_host_url}); 66 | } 67 | } 68 | try w.writeAll("/"); 69 | const path_prefix = loc.output_prefix_override orelse loc.code; 70 | if (path_prefix.len > 0) try w.print("{s}/", .{path_prefix}); 71 | }, 72 | } 73 | } 74 | 75 | pub const dot = scripty.defaultDot(Template, Value, false); 76 | pub const docs_description = ""; 77 | pub const Fields = struct { 78 | pub const site = 79 | \\The current website. In a multilingual website, 80 | \\each locale will have its own separate instance of $site 81 | ; 82 | 83 | pub const page = 84 | \\The page being currently rendered. 85 | ; 86 | 87 | pub const i18n = 88 | \\In a multilingual website it contains the translations 89 | \\defined in the corresponding i18n file. 90 | \\ 91 | \\See the i18n docs for more info. 92 | ; 93 | 94 | pub const build = 95 | \\Gives you access to build-time assets (i.e. assets built 96 | \\ via the Zig build system) alongside other information 97 | \\relative to the current build. 98 | ; 99 | 100 | pub const ctx = 101 | \\A key-value mapping that contains data defined in `` 102 | \\nodes. 103 | ; 104 | 105 | pub const loop = 106 | \\The current iterator, only available within elements 107 | \\that have a `loop` attribute. 108 | ; 109 | 110 | pub const @"if" = 111 | \\The current branching variable, only available within elements 112 | \\that have an `if` attribute used to unwrap an optional value. 113 | ; 114 | }; 115 | pub const Builtins = struct {}; 116 | -------------------------------------------------------------------------------- /src/context/Value.zig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/src/context/Value.zig -------------------------------------------------------------------------------- /src/context/doctypes.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ziggy = @import("ziggy"); 3 | const superhtml = @import("superhtml"); 4 | const context = @import("../context.zig"); 5 | const Value = context.Value; 6 | 7 | pub const Signature = struct { 8 | params: []const ScriptyParam = &.{}, 9 | ret: ScriptyParam, 10 | 11 | pub fn format( 12 | s: Signature, 13 | comptime fmt: []const u8, 14 | options: std.fmt.FormatOptions, 15 | out_stream: anytype, 16 | ) !void { 17 | _ = fmt; 18 | _ = options; 19 | try out_stream.writeAll("("); 20 | for (s.params, 0..) |p, idx| { 21 | try out_stream.writeAll(p.link(true)); 22 | if (idx < s.params.len - 1) { 23 | try out_stream.writeAll(", "); 24 | } 25 | } 26 | try out_stream.writeAll(") -> "); 27 | try out_stream.writeAll(s.ret.link(false)); 28 | } 29 | }; 30 | 31 | pub const ScriptyParam = union(enum) { 32 | Site, 33 | Page, 34 | Build, 35 | Git, 36 | Asset, 37 | Alternative, 38 | ContentSection, 39 | Footnote, 40 | Iterator, 41 | Array, 42 | String, 43 | Int, 44 | Float, 45 | Bool, 46 | Date, 47 | Ctx, 48 | KV, 49 | any, 50 | err, 51 | Map: Base, 52 | Opt: Base, 53 | Many: Base, 54 | 55 | pub const Base = union(enum) { 56 | Site, 57 | Page, 58 | Alternative, 59 | ContentSection, 60 | Footnote, 61 | Iterator, 62 | String, 63 | Int, 64 | Bool, 65 | Date, 66 | KV, 67 | any, 68 | Many: Base2, 69 | 70 | pub const Base2 = enum { 71 | Footnote, 72 | }; 73 | }; 74 | 75 | pub fn fromType(t: type) ScriptyParam { 76 | return switch (t) { 77 | context.Template => .any, 78 | ?context.Value => .any, 79 | context.Page, *const context.Page => .Page, 80 | context.Site, *const context.Site => .Site, 81 | context.Build => .Build, 82 | context.Git => .Git, 83 | superhtml.utils.Ctx(context.Value) => .Ctx, 84 | context.Page.Alternative => .Alternative, 85 | context.Page.ContentSection => .ContentSection, 86 | context.Page.Footnote => .Footnote, 87 | context.Asset => .Asset, 88 | // context.Slice => .any, 89 | context.Optional, ?*const context.Optional => .{ .Opt = .any }, 90 | context.String => .String, 91 | context.Bool => .Bool, 92 | context.Int => .Int, 93 | context.Float => .Float, 94 | context.DateTime => .Date, 95 | context.Map, context.Map.ZiggyMap => .{ .Map = .any }, 96 | context.Map.KV => .KV, 97 | context.Array => .Array, 98 | context.Iterator => .Iterator, 99 | ?*context.Iterator => .{ .Opt = .Iterator }, 100 | []const context.Page.Alternative => .{ .Many = .Alternative }, 101 | []const context.Page.Footnote => .{ .Many = .Footnote }, 102 | ?[]const context.Page.Footnote => .{ .Opt = .{ .Many = .Footnote } }, 103 | []const u8 => .String, 104 | ?[]const u8 => .{ .Opt = .String }, 105 | []const []const u8 => .{ .Many = .String }, 106 | bool => .Bool, 107 | usize => .Int, 108 | ziggy.dynamic.Value => .any, 109 | context.Value => .any, 110 | else => @compileError("TODO: add support for " ++ @typeName(t)), 111 | }; 112 | } 113 | 114 | pub fn string( 115 | p: ScriptyParam, 116 | comptime is_fn_param: bool, 117 | ) []const u8 { 118 | switch (p) { 119 | inline .Many => |m| switch (m) { 120 | inline else => { 121 | const dots = if (is_fn_param) "..." else ""; 122 | return "[" ++ @tagName(m) ++ dots ++ "]"; 123 | }, 124 | }, 125 | .Opt => |o| switch (o) { 126 | .Many => |om| switch (om) { 127 | inline else => |omm| return "?[" ++ @tagName(omm) ++ "]", 128 | }, 129 | inline else => return "?" ++ @tagName(o), 130 | }, 131 | inline else => return @tagName(p), 132 | } 133 | } 134 | pub fn link( 135 | p: ScriptyParam, 136 | comptime is_fn_param: bool, 137 | ) []const u8 { 138 | switch (p) { 139 | inline .Many => |m| switch (m) { 140 | inline else => { 141 | const dots = if (is_fn_param) "..." else ""; 142 | return std.fmt.comptimePrint( 143 | \\[[{0s}]($link.ref("{0s}")){1s}]{2s} 144 | , .{ 145 | @tagName(m), dots, if (is_fn_param or m == .any) "" else 146 | \\ *(see also [[any]]($link.ref("Array")))* 147 | }); 148 | }, 149 | }, 150 | inline .Opt => |o| switch (o) { 151 | inline .Many => |om| switch (om) { 152 | inline else => |omm| return comptime std.fmt.comptimePrint( 153 | \\?[[{0s}]($link.ref("{0s}"))] 154 | , .{@tagName(omm)}), 155 | }, 156 | inline else => return comptime std.fmt.comptimePrint( 157 | \\?[{0s}]($link.ref("{0s}")) 158 | , .{@tagName(o)}), 159 | }, 160 | inline else => |_, t| return comptime std.fmt.comptimePrint( 161 | \\[{0s}]($link.ref("{0s}")) 162 | , .{@tagName(t)}), 163 | } 164 | } 165 | 166 | pub fn id(p: ScriptyParam) []const u8 { 167 | switch (p) { 168 | .Opt, .Many => |o| switch (o) { 169 | inline else => return @tagName(o), 170 | }, 171 | inline else => return @tagName(p), 172 | } 173 | } 174 | }; 175 | -------------------------------------------------------------------------------- /src/context/markdown.zig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/src/context/markdown.zig -------------------------------------------------------------------------------- /src/context/utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ziggy = @import("ziggy"); 3 | const super = @import("superhtml"); 4 | const Allocator = std.mem.Allocator; 5 | const Asset = @import("Asset.zig"); 6 | const Value = @import("../context.zig").Value; 7 | 8 | pub const log = std.log.scoped(.builtin); 9 | -------------------------------------------------------------------------------- /src/fatal.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const main = @import("main.zig"); 3 | const builtin = @import("builtin"); 4 | 5 | pub fn msg(comptime fmt: []const u8, args: anytype) noreturn { 6 | std.debug.print(fmt, args); 7 | if (builtin.mode == .Debug) std.debug.panic("\n\n(Zine debug stack trace)\n", .{}); 8 | std.process.exit(1); 9 | } 10 | 11 | pub fn oom() noreturn { 12 | msg("oom\n", .{}); 13 | } 14 | 15 | pub fn dir(path: []const u8, err: anyerror) noreturn { 16 | msg("error accessing dir '{s}': {s}\n", .{ 17 | path, @errorName(err), 18 | }); 19 | } 20 | 21 | pub fn file(path: []const u8, err: anyerror) noreturn { 22 | msg("error accessing file '{s}': {s}\n", .{ 23 | path, @errorName(err), 24 | }); 25 | } 26 | 27 | pub fn help() noreturn { 28 | std.debug.print( 29 | \\Usage: zine [COMMAND] [OPTIONS] 30 | \\ 31 | \\Commands: 32 | \\ (no command) Start the development web server 33 | \\ init Initialize a Zine site in the current directory 34 | \\ release Create a release of a Zine site 35 | \\ help Show this menu and exit 36 | \\ version Print the Zine version and exit 37 | \\ 38 | \\General Options: 39 | \\ --drafts Enable draft pages 40 | \\ --help, -h Print command specific usage and extra options 41 | \\ 42 | \\Development web server options: 43 | \\ --host HOST Listening host (default 'localhost') 44 | \\ --port PORT Listening port (default 1990) 45 | \\ --debounce Rebuild delay after a file change (default 25) 46 | \\ 47 | \\ 48 | , .{}); 49 | std.process.exit(1); 50 | } 51 | -------------------------------------------------------------------------------- /src/fuzz/scripty.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zine = @import("zine"); 3 | 4 | export fn zig_fuzz_init() void {} 5 | 6 | export fn zig_fuzz_test(buf: [*]u8, len: isize) void {} 7 | -------------------------------------------------------------------------------- /src/highlight.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const options = @import("options"); 3 | const syntax = @import("syntax"); 4 | const treez = @import("treez"); 5 | const tracy = @import("tracy"); 6 | const HtmlSafe = @import("superhtml").HtmlSafe; 7 | 8 | const log = std.log.scoped(.highlight); 9 | 10 | pub const DotsToUnderscores = struct { 11 | bytes: []const u8, 12 | 13 | pub fn format( 14 | self: DotsToUnderscores, 15 | comptime fmt: []const u8, 16 | _: std.fmt.FormatOptions, 17 | out_stream: anytype, 18 | ) !void { 19 | _ = fmt; 20 | for (self.bytes) |b| { 21 | switch (b) { 22 | '.' => try out_stream.writeAll("_"), 23 | else => try out_stream.writeByte(b), 24 | } 25 | } 26 | } 27 | }; 28 | 29 | var query_cache: syntax.QueryCache = .{ 30 | .allocator = @import("main.zig").gpa, 31 | .mutex = std.Thread.Mutex{}, 32 | }; 33 | 34 | const ClassSet = struct { 35 | classes: std.StringHashMap(void), 36 | 37 | const Self = @This(); 38 | 39 | pub fn init(allocator: std.mem.Allocator) ClassSet { 40 | return .{ 41 | .classes = std.StringHashMap(void).init(allocator), 42 | }; 43 | } 44 | 45 | pub fn deinit(self: *Self) void { 46 | self.classes.deinit(); 47 | } 48 | 49 | pub fn addClass(self: *Self, class: []const u8) !void { 50 | try self.classes.put(class, {}); 51 | } 52 | 53 | pub fn removeClass(self: *Self, class: []const u8) void { 54 | _ = self.classes.remove(class); 55 | } 56 | 57 | pub fn getClasses(self: Self, result: *std.ArrayList([]const u8)) !void { 58 | result.clearRetainingCapacity(); 59 | var it = self.classes.keyIterator(); 60 | while (it.next()) |key| try result.append(key.*); 61 | } 62 | }; 63 | 64 | const ClassChange = struct { 65 | position: usize, 66 | is_add: bool, 67 | class: []const u8, 68 | 69 | pub fn lessThan(_: void, a: ClassChange, b: ClassChange) bool { 70 | return a.position < b.position; 71 | } 72 | }; 73 | 74 | fn printSpan( 75 | writer: anytype, 76 | code: []const u8, 77 | start: usize, 78 | end: usize, 79 | classes: []const []const u8, 80 | arena: std.mem.Allocator, 81 | ) !void { 82 | if (classes.len == 0) { 83 | try writer.print("{s}", .{HtmlSafe{ .bytes = code[start..end] }}); 84 | return; 85 | } 86 | 87 | var class_str = std.ArrayList(u8).init(arena); 88 | defer class_str.deinit(); 89 | 90 | for (classes, 0..) |class, i| { 91 | if (i > 0) try class_str.append(' '); 92 | try class_str.appendSlice(class); 93 | } 94 | 95 | try writer.print( 96 | \\{s} 97 | , .{ 98 | DotsToUnderscores{ .bytes = class_str.items }, 99 | HtmlSafe{ .bytes = code[start..end] }, 100 | }); 101 | } 102 | 103 | pub fn highlightCode( 104 | arena: std.mem.Allocator, 105 | lang_name: []const u8, 106 | code: []const u8, 107 | writer: anytype, 108 | ) !void { 109 | const zone = tracy.traceNamed(@src(), "highlightCode"); 110 | defer zone.end(); 111 | tracy.messageCopy(lang_name); 112 | 113 | if (!options.enable_treesitter) { 114 | try writer.print("{s}", .{HtmlSafe{ .bytes = code }}); 115 | return; 116 | } 117 | 118 | const lang = blk: { 119 | const query_zone = tracy.traceNamed(@src(), "syntax"); 120 | defer query_zone.end(); 121 | 122 | break :blk syntax.create_file_type( 123 | arena, 124 | lang_name, 125 | &query_cache, 126 | ) catch { 127 | const syntax_fallback_zone = tracy.traceNamed(@src(), "syntax fallback"); 128 | defer syntax_fallback_zone.end(); 129 | const fake_filename = try std.fmt.allocPrint(arena, "file.{s}", .{lang_name}); 130 | break :blk try syntax.create_guess_file_type(arena, "", fake_filename, &query_cache); 131 | }; 132 | }; 133 | 134 | { 135 | const refresh_zone = tracy.traceNamed(@src(), "refresh"); 136 | defer refresh_zone.end(); 137 | try lang.refresh_full(code); 138 | } 139 | // we don't want to free any resource from the query cache 140 | // defer lang.destroy(); 141 | 142 | const tree = lang.tree orelse return; 143 | const cursor = try treez.Query.Cursor.create(); 144 | defer cursor.destroy(); 145 | 146 | { 147 | const query_zone = tracy.traceNamed(@src(), "exec query"); 148 | defer query_zone.end(); 149 | cursor.execute(lang.query, tree.getRootNode()); 150 | } 151 | 152 | const match_zone = tracy.traceNamed(@src(), "render"); 153 | defer match_zone.end(); 154 | 155 | cursor.execute(lang.query, tree.getRootNode()); 156 | 157 | var changes = std.ArrayList(ClassChange).init(arena); 158 | 159 | while (cursor.nextMatch()) |match| { 160 | for (match.captures()) |capture| { 161 | const range = capture.node.getRange(); 162 | const capture_name = lang.query.getCaptureNameForId(capture.id); 163 | 164 | try changes.append(ClassChange{ 165 | .position = range.start_byte, 166 | .is_add = true, 167 | .class = capture_name, 168 | }); 169 | 170 | try changes.append(ClassChange{ 171 | .position = range.end_byte, 172 | .is_add = false, 173 | .class = capture_name, 174 | }); 175 | } 176 | } 177 | 178 | std.sort.insertion(ClassChange, changes.items, {}, ClassChange.lessThan); 179 | 180 | var current_classes = ClassSet.init(arena); 181 | defer current_classes.deinit(); 182 | 183 | var class_list = std.ArrayList([]const u8).init(arena); 184 | defer class_list.deinit(); 185 | 186 | var current_pos: usize = 0; 187 | 188 | for (changes.items) |change| { 189 | if (change.position > current_pos) { 190 | try current_classes.getClasses(&class_list); 191 | try printSpan(writer, code, current_pos, change.position, class_list.items, arena); 192 | current_pos = change.position; 193 | } 194 | 195 | if (change.is_add) { 196 | try current_classes.addClass(change.class); 197 | continue; 198 | } 199 | 200 | current_classes.removeClass(change.class); 201 | } 202 | 203 | if (current_pos < code.len) { 204 | try current_classes.getClasses(&class_list); 205 | try printSpan(writer, code, current_pos, code.len, class_list.items, arena); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const options = @import("options"); 4 | const tracy = @import("tracy"); 5 | const fatal = @import("fatal.zig"); 6 | const worker = @import("worker.zig"); 7 | const root = @import("root.zig"); 8 | const Allocator = std.mem.Allocator; 9 | 10 | const log = std.log.scoped(.main); 11 | 12 | pub const std_options: std.Options = .{ 13 | .log_level = .err, 14 | .log_scope_levels = options.log_scope_levels, 15 | }; 16 | 17 | const Command = enum { 18 | init, 19 | release, 20 | debug, 21 | help, 22 | @"-h", 23 | @"--help", 24 | version, 25 | @"-v", 26 | @"--version", 27 | // Because other ssgs have them: 28 | serve, 29 | server, 30 | dev, 31 | develop, 32 | }; 33 | 34 | var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 35 | pub const gpa = if (builtin.single_threaded) 36 | debug_allocator.allocator() 37 | else 38 | std.heap.smp_allocator; 39 | 40 | pub fn main() u8 { 41 | errdefer |err| switch (err) { 42 | error.OutOfMemory, error.Overflow => fatal.oom(), 43 | }; 44 | 45 | root.progress = std.Progress.start(.{ .draw_buffer = &root.progress_buf }); 46 | defer root.progress.end(); 47 | 48 | if (builtin.mode == .Debug) { 49 | std.debug.print( 50 | \\*-----------------------------------------------* 51 | \\| WARNING: THIS IS A DEBUG BUILD OF ZINE | 52 | \\|-----------------------------------------------| 53 | \\| Debug builds enable expensive sanity checks | 54 | \\| that reduce performance. | 55 | \\| | 56 | \\| To create a release build, run: | 57 | \\| | 58 | \\| zig build --release=fast | 59 | \\| | 60 | \\| If you're investigating a bug in Zine, then a | 61 | \\| debug build might turn confusing behavior | 62 | \\| into a crash. | 63 | \\| | 64 | \\| To disable all forms of concurrency, you can | 65 | \\| add the following flag to your build command: | 66 | \\| | 67 | \\| -Dsingle-threaded | 68 | \\| | 69 | \\*-----------------------------------------------* 70 | \\ 71 | \\ 72 | , .{}); 73 | } 74 | if (tracy.enable) { 75 | std.debug.print( 76 | \\*-----------------------------------------------* 77 | \\| WARNING: TRACING ENABLED | 78 | \\|-----------------------------------------------| 79 | \\| Tracing introduces a significant performance | 80 | \\| overhead. | 81 | \\| | 82 | \\| If you're not interested in tracing Zine, | 83 | \\| remove `-Dtracy` when building again. | 84 | \\*-----------------------------------------------* 85 | \\ 86 | \\ 87 | , .{}); 88 | } 89 | 90 | if (options.tsan) { 91 | std.debug.print( 92 | \\*-----------------------------------------------* 93 | \\| WARNING: TSAN ENABLED | 94 | \\|-----------------------------------------------| 95 | \\| Thread sanitizer introduces a significant | 96 | \\| performance overhead. | 97 | \\| | 98 | \\| If you're not interested in debugging | 99 | \\| concurrency bugs in Zine, remove `-Dtsan` | 100 | \\| when building again. | 101 | \\*-----------------------------------------------* 102 | \\ 103 | \\ 104 | , .{}); 105 | } 106 | 107 | const args = try std.process.argsAlloc(gpa); 108 | defer std.process.argsFree(gpa, args); 109 | 110 | const cmd = blk: { 111 | if (args.len >= 2) { 112 | if (std.meta.stringToEnum(Command, args[1])) |cmd| { 113 | break :blk cmd; 114 | } 115 | } 116 | 117 | @import("cli/serve.zig").serve(gpa, args[1..]); 118 | }; 119 | 120 | const any_error = switch (cmd) { 121 | .init => @import("cli/init.zig").init(gpa, args[2..]), 122 | .release => @import("cli/release.zig").release(gpa, args[2..]), 123 | .debug => @import("cli/debug.zig").debug(gpa, args[2..]), 124 | .help, .@"-h", .@"--help" => fatal.help(), 125 | .version, .@"-v", .@"--version" => printVersion(), 126 | .serve, .server, .dev, .develop => { 127 | std.debug.print( 128 | "error: run zine without any subcommand to start the development web server\n\n", 129 | .{}, 130 | ); 131 | fatal.help(); 132 | }, 133 | }; 134 | 135 | return @intFromBool(any_error); 136 | } 137 | 138 | fn printVersion() noreturn { 139 | std.debug.print("{s}\n", .{options.version}); 140 | std.process.exit(0); 141 | } 142 | -------------------------------------------------------------------------------- /src/render.zig: -------------------------------------------------------------------------------- 1 | pub const html = @import("render/html.zig").html; 2 | pub const htmlToc = @import("render/html.zig").htmlToc; 3 | pub const htmlTocDetails = @import("render/html.zig").htmlTocDetails; 4 | -------------------------------------------------------------------------------- /tests/content-scanning/collisions/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/collisions/assets/.keep -------------------------------------------------------------------------------- /tests/content-scanning/collisions/content/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .aliases = [ 7 | "index.html", 8 | "foo/bar/baz.html", 9 | "foo/bar.html", 10 | "README.html", 11 | "another_index.html", 12 | ], 13 | .alternatives = [{ 14 | .name = "readme", 15 | .output = "nested/path/page/README.html", 16 | .layout = "", 17 | }], 18 | .draft = false, 19 | --- 20 | Your **SuperMD** content goes here. 21 | -------------------------------------------------------------------------------- /tests/content-scanning/collisions/content/nested/path/page.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "foo", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "page.shtml", 6 | .aliases = [ 7 | "/another_index.html", 8 | // None of these aliases should collide because they're all relative: 9 | "foo/bar/baz.html", 10 | "foo/bar.html", 11 | ], 12 | .alternatives = [{ 13 | .name = "readme", 14 | .output = "README.html", 15 | .layout = "", 16 | }], 17 | .draft = false, 18 | --- 19 | 20 | -------------------------------------------------------------------------------- /tests/content-scanning/collisions/content/page.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Sections", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "page.shtml", 6 | .aliases = [ 7 | "/foo/bar/baz.html", 8 | "/foo/bar.html", 9 | "/README.html", 10 | "/another_page.html", 11 | "/nested/path/page/index.html" 12 | ], 13 | .alternatives = [{ 14 | .name = "readme", 15 | .output = "README.html", 16 | .layout = "", 17 | }], 18 | .draft = false, 19 | --- 20 | 21 | -------------------------------------------------------------------------------- /tests/content-scanning/collisions/content/page/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Page", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "page.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | -------------------------------------------------------------------------------- /tests/content-scanning/collisions/layouts/index.shtml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/collisions/layouts/index.shtml -------------------------------------------------------------------------------- /tests/content-scanning/collisions/layouts/page.shtml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/collisions/layouts/page.shtml -------------------------------------------------------------------------------- /tests/content-scanning/collisions/snapshot.txt: -------------------------------------------------------------------------------- 1 | *-----------------------------------------------* 2 | | WARNING: THIS IS A DEBUG BUILD OF ZINE | 3 | |-----------------------------------------------| 4 | | Debug builds enable expensive sanity checks | 5 | | that reduce performance. | 6 | | | 7 | | To create a release build, run: | 8 | | | 9 | | zig build --release=fast | 10 | | | 11 | | If you're investigating a bug in Zine, then a | 12 | | debug build might turn confusing behavior | 13 | | into a crash. | 14 | | | 15 | | To disable all forms of concurrency, you can | 16 | | add the following flag to your build command: | 17 | | | 18 | | -Dsingle-threaded | 19 | | | 20 | *-----------------------------------------------* 21 | 22 | content/index.smd:16:15: error: invalid layout in alternatives 23 | | .layout = "", 24 | | ^^ 25 | 26 | content/page.smd:16:13: error: invalid layout in alternatives 27 | | .layout = "", 28 | | ^^ 29 | 30 | content/nested/path/page.smd:15:15: error: invalid layout in alternatives 31 | | .layout = "", 32 | | ^^ 33 | 34 | page/index.html: error: output url collision detected 35 | between page.smd (main output) 36 | and page/index.smd (main output) 37 | 38 | index.html: error: output url collision detected 39 | between index.smd (main output) 40 | and index.smd (page alias) 41 | 42 | foo/bar/baz.html: error: output url collision detected 43 | between index.smd (page alias) 44 | and page.smd (page alias) 45 | 46 | foo/bar.html: error: output url collision detected 47 | between index.smd (page alias) 48 | and page.smd (page alias) 49 | 50 | README.html: error: output url collision detected 51 | between index.smd (page alias) 52 | and page.smd (page alias) 53 | 54 | nested/path/page/index.html: error: output url collision detected 55 | between nested/path/page.smd (main output) 56 | and page.smd (page alias) 57 | 58 | another_index.html: error: output url collision detected 59 | between index.smd (page alias) 60 | and nested/path/page.smd (page alias) 61 | 62 | nested/path/page/README.html: error: output url collision detected 63 | between index.smd (page alternative 'readme') 64 | and nested/path/page.smd (page alternative 'readme') 65 | 66 | ---------------------------- 67 | -- VARIANT -- 68 | ---------------------------- 69 | .id = 0, 70 | .content_dir_path = content 71 | 72 | ------- SECTION ------- 73 | .index = 1, 74 | .section_path = content/, 75 | .pages = [ 76 | content/page.smd 77 | content/page/index.smd 78 | content/nested/path/page.smd 79 | ], 80 | 81 | 82 | ------- SECTION ------- 83 | .index = 2, 84 | .section_path = content/page/, 85 | .pages = [ 86 | ], 87 | 88 | 89 | 90 | ----- EXIT CODE: 1 ----- 91 | -------------------------------------------------------------------------------- /tests/content-scanning/collisions/zine.ziggy: -------------------------------------------------------------------------------- 1 | Site { 2 | .title = "Sample Site", 3 | .host_url = "https://example.com", 4 | .content_dir_path = "content", 5 | .layouts_dir_path = "layouts", 6 | .assets_dir_path = "content", 7 | } -------------------------------------------------------------------------------- /tests/content-scanning/frontmatter/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/frontmatter/assets/.keep -------------------------------------------------------------------------------- /tests/content-scanning/frontmatter/content/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "", 6 | .aliases = [ 7 | "good.html", 8 | "/also/good.html", 9 | ], 10 | .alternatives = [{ 11 | .name = "good", 12 | .output = "also/good/but/not/a/collision.html", 13 | .layout = "foo.html", 14 | },{ 15 | .name = "also good", 16 | .output = "good/url/though.html", 17 | .layout = "foo.html", 18 | },{ 19 | .name = "good name and good url", 20 | .layout = "foo.html", 21 | .output = "foobar.html", 22 | }], 23 | .draft = false, 24 | --- 25 | Your **SuperMD** content goes here. 26 | -------------------------------------------------------------------------------- /tests/content-scanning/frontmatter/content/validation-errors.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "", 6 | .aliases = [ 7 | "good.html", 8 | "/also/good.html", 9 | "bad path 💩", 10 | ], 11 | .alternatives = [{ 12 | .name = "good", 13 | .output = "also/good/but/not/a/collision.html", 14 | .layout = "foo.html", 15 | },{ 16 | .name = "", 17 | .output = "good/url/though.html", 18 | .layout = "foo.html", 19 | },{ 20 | .name = "good name but bad url", 21 | .output = "", 22 | .layout = "foo.html", 23 | }], 24 | .draft = false, 25 | --- 26 | Your **SuperMD** content goes here. 27 | -------------------------------------------------------------------------------- /tests/content-scanning/frontmatter/content/wrong-syntax.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "", 6 | .aliases = [ 7 | "good.html", 8 | "/also/good.html", 9 | "bad path 💩", 10 | ], 11 | .alternatives = [{ 12 | .name = "good", 13 | .output = "also/good/but/not/a/collision.html", 14 | .layout = "foo.html", 15 | },{ 16 | .name = "", 17 | .output = "good/url/though.html" 18 | .layout = "foo.html", 19 | },{ 20 | .name = "good name but bad url", 21 | .output = "", 22 | .layout = "foo.html", 23 | }], 24 | .draft = false, 25 | --- 26 | Your **SuperMD** content goes here. 27 | -------------------------------------------------------------------------------- /tests/content-scanning/frontmatter/layouts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/frontmatter/layouts/.keep -------------------------------------------------------------------------------- /tests/content-scanning/frontmatter/snapshot.txt: -------------------------------------------------------------------------------- 1 | *-----------------------------------------------* 2 | | WARNING: THIS IS A DEBUG BUILD OF ZINE | 3 | |-----------------------------------------------| 4 | | Debug builds enable expensive sanity checks | 5 | | that reduce performance. | 6 | | | 7 | | To create a release build, run: | 8 | | | 9 | | zig build --release=fast | 10 | | | 11 | | If you're investigating a bug in Zine, then a | 12 | | debug build might turn confusing behavior | 13 | | into a crash. | 14 | | | 15 | | To disable all forms of concurrency, you can | 16 | | add the following flag to your build command: | 17 | | | 18 | | -Dsingle-threaded | 19 | | | 20 | *-----------------------------------------------* 21 | 22 | content/wrong-syntax.smd:18:5: 23 | .layout = "foo.html", 24 | ^ 25 | unexpected '.', expected: ',' or '}' 26 | 27 | 28 | content/index.smd:5:11: error: missing layout file 29 | | .layout = "", 30 | | ^^ 31 | 32 | content/validation-errors.smd:5:11: error: missing layout file 33 | | .layout = "", 34 | | ^^ 35 | 36 | content/validation-errors.smd:9:4: error: invalid value in 'aliases' 37 | | "bad path 💩", 38 | | ^^^^^^^^^^^^^^^ 39 | 40 | content/validation-errors.smd:16:13: error: invalid name in alternatives 41 | | .name = "", 42 | | ^^ 43 | 44 | content/validation-errors.smd:21:15: error: invalid path in alternatives 45 | | .output = "", 46 | | ^^ 47 | 48 | ---------------------------- 49 | -- VARIANT -- 50 | ---------------------------- 51 | .id = 0, 52 | .content_dir_path = content 53 | 54 | ------- SECTION ------- 55 | .index = 1, 56 | .section_path = content/, 57 | .pages = [ 58 | content/validation-errors.smd 59 | content/wrong-syntax.smd 60 | ], 61 | 62 | 63 | 64 | ----- EXIT CODE: 1 ----- 65 | -------------------------------------------------------------------------------- /tests/content-scanning/frontmatter/zine.ziggy: -------------------------------------------------------------------------------- 1 | Site { 2 | .title = "Sample Site", 3 | .host_url = "https://example.com", 4 | .content_dir_path = "content", 5 | .layouts_dir_path = "layouts", 6 | .assets_dir_path = "content", 7 | } -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/page-analysis/assets/.keep -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/assets/code.zig: -------------------------------------------------------------------------------- 1 | const foo = 42; 2 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/assets/skater.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/page-analysis/assets/skater.webp -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/content/code.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Code", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | ``` 10 | correct! 11 | ``` 12 | 13 | ```=html 14 | correct! 15 | ``` 16 | 17 | ```=html 18 | wrong! 19 | ``` 20 | 21 | ```rust 22 | // Correct! 23 | ``` 24 | 25 | ```zig 26 | // correct 27 | ``` 28 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/content/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .alternatives = [{ 7 | .name = "correct-alt", 8 | .output = "arst.html", 9 | .layout = "index.shtml", 10 | }], 11 | .draft = false, 12 | --- 13 | 14 | # [Ok Ref]($heading.id('okref')) // correct 15 | 16 | # Wrong asset 17 | []($image.siteAsset('doesntexist.jpg')) //wrong 18 | 19 | # Self page, correct alternative 20 | []($link.alternative('correct-alt')) //correct 21 | 22 | # Self page, wrong alternative 23 | []($link.alternative('doesntexist-alternative')) //wrong 24 | 25 | # Self page, correct ref 26 | []($link.ref('okref')) //correct 27 | 28 | # Correct page 29 | []($link.page('other')) //correct 30 | 31 | # Correct page 32 | []($link.sub('other')) //correct 33 | 34 | # Wrong page 35 | []($link.page('doesntexist')) //wrong 36 | 37 | # Correct page, wrong alternative 38 | []($link.page('other').alternative('doesntexist')) // wrong 39 | 40 | # Markdown syntax 41 | ![](/bad.jpg) //wrong 42 | ![](./bad.jpg) //wrong 43 | ![](bad.jpg) // wrong 44 | ![](/skater.webp) // correct 45 | ![](skater.webp) // correct 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/content/other.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Sections", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "sections.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # [H1]($section.id('h1')) 10 | Lorem Ipsum 1 11 | 12 | ## [H2]($section.id('h2')) 13 | Lorem Ipsum 2 14 | 15 | ### [H3]($section.id('h3')) 16 | Lorem Ipsum 3 17 | 18 | 19 | ```zig++ wrong 20 | ``` 21 | 22 | [](<$code.siteAsset('code.zig').language('zig++')>) //wrong 23 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/content/parse.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "parsing errors", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | # Inline html 9 | wrong 10 | 11 |
wrong block-level html
12 | 13 | []($link.page(/other)) //wrong 14 | []($link.page('/other)) //wrong 15 | 16 | # Correct page but wrong path (no abs paths in scripty arguments) 17 | []($link.page('/other')) //wrong 18 | 19 | # Wrong page (the dot makes it wrong) 20 | []($link.page('./other')) //wrong 21 | 22 | # Bad paths 23 | []($link.page('foo//bar')) //wrong 1/9 24 | 25 | []($link.page('foo/./bar')) //wrong 2/9 26 | 27 | []($link.page('foo/../bar')) //wrong 3/9 28 | 29 | []($link.page('foo/.')) //wrong 4/9 30 | 31 | []($link.page('foo/..')) //wrong 5/9 32 | 33 | []($link.page('a//foo/./bar')) //wrong 6/9 34 | 35 | []($link.page('a//foo/../bar')) //wrong 7/9 36 | 37 | []($link.page('a//foo/.')) //wrong 8/9 38 | 39 | []($link.page('a//foo/..')) //wrong 9/9 40 | 41 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/content/skater.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/page-analysis/content/skater.webp -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/layouts/archive-entry.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 |
11 | 12 |
13 | Prev: 14 | 15 |
16 |
17 | 18 | 19 | 20 | Prev: 21 | 22 | 23 | 24 | 25 | 26 |
27 | Next: 28 | 29 |
30 |
31 | 32 | 33 | 34 | Next: 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/layouts/archive.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 |

11 | 12 | 13 |

14 |
15 |

16 | 17 | 18 |

19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/layouts/index.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 | 11 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/layouts/sections.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 |

-- TABLE OF CONTENTS --

11 | 12 |
13 |
14 |

-- SECTION BEGIN --

15 |
16 |

-- SECTION END --

17 |
18 | 19 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/snapshot.txt: -------------------------------------------------------------------------------- 1 | *-----------------------------------------------* 2 | | WARNING: THIS IS A DEBUG BUILD OF ZINE | 3 | |-----------------------------------------------| 4 | | Debug builds enable expensive sanity checks | 5 | | that reduce performance. | 6 | | | 7 | | To create a release build, run: | 8 | | | 9 | | zig build --release=fast | 10 | | | 11 | | If you're investigating a bug in Zine, then a | 12 | | debug build might turn confusing behavior | 13 | | into a crash. | 14 | | | 15 | | To disable all forms of concurrency, you can | 16 | | add the following flag to your build command: | 17 | | | 18 | | -Dsingle-threaded | 19 | | | 20 | *-----------------------------------------------* 21 | 22 | content/code.smd:18:3: [erroneous_end_tag] 23 | | wrong! 24 | | ^^^^ 25 | 26 | content/parse.smd:10:1: [html_is_forbidden] 27 | |
wrong block-level html
28 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 29 | 30 | content/parse.smd:12:1: [scripty] syntax error 31 | | []($link.page(/other)) //wrong 32 | | ^^^^^^^^^^^^^^^^^^^^^^ 33 | 34 | content/parse.smd:13:1: [scripty] syntax error 35 | | []($link.page('/other)) //wrong 36 | | ^^^^^^^^^^^^^^^^^^^^^^^ 37 | 38 | content/parse.smd:16:1: [scripty] path must be relative 39 | | []($link.page('/other')) //wrong 40 | | ^^^^^^^^^^^^^^^^^^^^^^^^ 41 | 42 | content/parse.smd:19:1: [scripty] '.' and '..' are not allowed in paths 43 | | []($link.page('./other')) //wrong 44 | | ^^^^^^^^^^^^^^^^^^^^^^^^^ 45 | 46 | content/parse.smd:22:1: [scripty] empty component in path 47 | | []($link.page('foo//bar')) //wrong 1/9 48 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 49 | 50 | content/parse.smd:24:1: [scripty] '.' and '..' are not allowed in paths 51 | | []($link.page('foo/./bar')) //wrong 2/9 52 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 53 | 54 | content/parse.smd:26:1: [scripty] '.' and '..' are not allowed in paths 55 | | []($link.page('foo/../bar')) //wrong 3/9 56 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 57 | 58 | content/parse.smd:28:1: [scripty] '.' and '..' are not allowed in paths 59 | | []($link.page('foo/.')) //wrong 4/9 60 | | ^^^^^^^^^^^^^^^^^^^^^^^ 61 | 62 | content/parse.smd:30:1: [scripty] '.' and '..' are not allowed in paths 63 | | []($link.page('foo/..')) //wrong 5/9 64 | | ^^^^^^^^^^^^^^^^^^^^^^^^ 65 | 66 | content/parse.smd:32:1: [scripty] empty component in path 67 | | []($link.page('a//foo/./bar')) //wrong 6/9 68 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 69 | 70 | content/parse.smd:34:1: [scripty] empty component in path 71 | | []($link.page('a//foo/../bar')) //wrong 7/9 72 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 73 | 74 | content/parse.smd:36:1: [scripty] empty component in path 75 | | []($link.page('a//foo/.')) //wrong 8/9 76 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 77 | 78 | content/parse.smd:38:1: [scripty] empty component in path 79 | | []($link.page('a//foo/..')) //wrong 9/9 80 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 81 | 82 | content/index.smd:18:1: error: missing site asset 83 | | []($image.siteAsset('doesntexist.jpg')) //wrong 84 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 85 | 86 | content/index.smd:24:1: error: unknown alternative 87 | | []($link.alternative('doesntexist-alternative')) //wrong 88 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 89 | 90 | content/index.smd:36:1: error: unknown page 91 | | []($link.page('doesntexist')) //wrong 92 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 93 | 94 | content/index.smd:39:1: error: unknown alternative 95 | | []($link.page('other').alternative('doesntexist')) // wrong 96 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 97 | 98 | content/index.smd:42:1: error: missing site asset 99 | | ![](/bad.jpg) //wrong 100 | | ^^^^^^^^^^^^^ 101 | 102 | content/index.smd:43:1: error: missing site asset 103 | | ![](./bad.jpg) //wrong 104 | | ^^^^^^^^^^^^^^ 105 | 106 | content/index.smd:44:1: error: missing site asset 107 | | ![](bad.jpg) // wrong 108 | | ^^^^^^^^^^^^ 109 | 110 | content/other.smd:20:1: error: unknown language code 111 | | ```zig++ wrong 112 | | ^^^^^^^^^^^^^^^ 113 | 114 | content/other.smd:23:1: error: unknown language code 115 | | [](<$code.siteAsset('code.zig').language('zig++')>) //wrong 116 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 117 | 118 | ---------------------------- 119 | -- VARIANT -- 120 | ---------------------------- 121 | .id = 0, 122 | .content_dir_path = content 123 | 124 | ------- SECTION ------- 125 | .index = 1, 126 | .section_path = content/, 127 | .pages = [ 128 | content/parse.smd 129 | content/other.smd 130 | content/code.smd 131 | ], 132 | 133 | skater.webp (0) 134 | 135 | 136 | ----- EXIT CODE: 1 ----- 137 | -------------------------------------------------------------------------------- /tests/content-scanning/page-analysis/zine.ziggy: -------------------------------------------------------------------------------- 1 | Site { 2 | .title = "Sample Site", 3 | .host_url = "https://example.com", 4 | .content_dir_path = "content", 5 | .layouts_dir_path = "layouts", 6 | .assets_dir_path = "assets", 7 | } -------------------------------------------------------------------------------- /tests/content-scanning/simple/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/simple/assets/.keep -------------------------------------------------------------------------------- /tests/content-scanning/simple/content/archive/2024/first.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "First post (2024)", 3 | .date = @date("2024-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "archive-entry.shtml", 6 | .draft = false, 7 | --- 8 | Lorem ipsum 9 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/content/archive/2024/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "2024", 3 | .date = @date("2024-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # 2024 10 | 11 | Lorem ipsum 12 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/content/archive/2025/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "2025", 3 | .date = @date("2025-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # 2025 10 | 11 | Lorem ipsum 12 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/content/archive/2025/second.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Second post (2025)", 3 | .date = @date("2024-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "archive-entry.shtml", 6 | .draft = false, 7 | --- 8 | dolor something something 9 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/content/archive/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Archive", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "archive.shtml", 6 | .draft = false, 7 | --- 8 | 9 | Archive 10 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/content/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | 10 | 11 | # H1 12 | Lorem Ipsum 1 13 | 14 | ## H2 15 | Lorem Ipsum 2 16 | 17 | #### H3 18 | Lorem Ipsum 3 19 | 20 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/content/sections.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Sections", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "sections.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # [H1]($section.id('h1')) 10 | Lorem Ipsum 1 11 | 12 | ## [H2]($section.id('h2')) 13 | Lorem Ipsum 2 14 | 15 | ### [H3]($section.id('h3')) 16 | Lorem Ipsum 3 17 | 18 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/layouts/archive-entry.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 |
11 | 12 |
13 | Prev: 14 | 15 |
16 |
17 | 18 | 19 | 20 | Prev: 21 | 22 | 23 | 24 | 25 | 26 |
27 | Next: 28 | 29 |
30 |
31 | 32 | 33 | 34 | Next: 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/layouts/archive.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 |

11 | 12 | 13 |

14 |
15 |

16 | 17 | 18 |

19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/layouts/index.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 | 11 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/layouts/sections.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 |

-- TABLE OF CONTENTS --

11 | 12 |
13 |
14 |

-- SECTION BEGIN --

15 |
16 |

-- SECTION END --

17 |
18 | 19 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/snapshot.txt: -------------------------------------------------------------------------------- 1 | *-----------------------------------------------* 2 | | WARNING: THIS IS A DEBUG BUILD OF ZINE | 3 | |-----------------------------------------------| 4 | | Debug builds enable expensive sanity checks | 5 | | that reduce performance. | 6 | | | 7 | | To create a release build, run: | 8 | | | 9 | | zig build --release=fast | 10 | | | 11 | | If you're investigating a bug in Zine, then a | 12 | | debug build might turn confusing behavior | 13 | | into a crash. | 14 | | | 15 | | To disable all forms of concurrency, you can | 16 | | add the following flag to your build command: | 17 | | | 18 | | -Dsingle-threaded | 19 | | | 20 | *-----------------------------------------------* 21 | 22 | ---------------------------- 23 | -- VARIANT -- 24 | ---------------------------- 25 | .id = 0, 26 | .content_dir_path = content 27 | 28 | ------- SECTION ------- 29 | .index = 1, 30 | .section_path = content/, 31 | .pages = [ 32 | content/sections.smd 33 | content/archive/index.smd 34 | ], 35 | 36 | 37 | ------- SECTION ------- 38 | .index = 2, 39 | .section_path = content/archive/, 40 | .pages = [ 41 | content/archive/2025/index.smd 42 | content/archive/2024/index.smd 43 | ], 44 | 45 | 46 | ------- SECTION ------- 47 | .index = 3, 48 | .section_path = content/archive/2024/, 49 | .pages = [ 50 | content/archive/2024/first.smd 51 | ], 52 | 53 | 54 | ------- SECTION ------- 55 | .index = 4, 56 | .section_path = content/archive/2025/, 57 | .pages = [ 58 | content/archive/2025/second.smd 59 | ], 60 | 61 | 62 | 63 | ----- EXIT CODE: 0 ----- 64 | -------------------------------------------------------------------------------- /tests/content-scanning/simple/zine.ziggy: -------------------------------------------------------------------------------- 1 | Site { 2 | .title = "Sample Site", 3 | .host_url = "https://example.com", 4 | .content_dir_path = "content", 5 | .layouts_dir_path = "layouts", 6 | .assets_dir_path = "content", 7 | } -------------------------------------------------------------------------------- /tests/content-scanning/templates/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/templates/assets/.keep -------------------------------------------------------------------------------- /tests/content-scanning/templates/content/another.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Page", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "doesntexist-layout.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/content/badextend.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Page", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "badextend.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/content/badhtml.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Page", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "badhtml.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/content/badshtml.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Page", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "badshtml.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/content/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/content/page.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Page", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "page.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/layouts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/templates/layouts/.keep -------------------------------------------------------------------------------- /tests/content-scanning/templates/layouts/badextend.shtml: -------------------------------------------------------------------------------- 1 | 2 |
-------------------------------------------------------------------------------- /tests/content-scanning/templates/layouts/badhtml.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/layouts/badshtml.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 |
7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/layouts/index.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello World!

4 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/layouts/oops.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/templates/layouts/oops.html -------------------------------------------------------------------------------- /tests/content-scanning/templates/layouts/page.shtml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/content-scanning/templates/layouts/page.shtml -------------------------------------------------------------------------------- /tests/content-scanning/templates/layouts/templates/base.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/layouts/templates/withmenu.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/snapshot.txt: -------------------------------------------------------------------------------- 1 | *-----------------------------------------------* 2 | | WARNING: THIS IS A DEBUG BUILD OF ZINE | 3 | |-----------------------------------------------| 4 | | Debug builds enable expensive sanity checks | 5 | | that reduce performance. | 6 | | | 7 | | To create a release build, run: | 8 | | | 9 | | zig build --release=fast | 10 | | | 11 | | If you're investigating a bug in Zine, then a | 12 | | debug build might turn confusing behavior | 13 | | into a crash. | 14 | | | 15 | | To disable all forms of concurrency, you can | 16 | | add the following flag to your build command: | 17 | | | 18 | | -Dsingle-threaded | 19 | | | 20 | *-----------------------------------------------* 21 | 22 | WARNING: found plain HTML file layouts/oops.html, did you mean to give it a shtml extension? 23 | content/another.smd:5:11: error: missing layout file 24 | | .layout = "doesntexist-layout.shtml", 25 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 26 | 27 | layouts/badextend.shtml: error: extending a template that doesn't exist 28 | template 'doesntexist-template.shtml' does not exist 29 | layouts/badhtml.shtml:1:4: erroneous_end_tag 30 | layouts/badshtml.shtml:0:1: extend_without_template_attr 31 | layouts/badshtml.shtml:2:3: super_parent_element_missing_id 32 | layouts/badshtml.shtml:8:3: unexpected_extend 33 | layouts/badshtml.shtml:10:1: top_level_super 34 | layouts/badshtml.shtml:6:4: super_under_branching 35 | ---------------------------- 36 | -- VARIANT -- 37 | ---------------------------- 38 | .id = 0, 39 | .content_dir_path = content 40 | 41 | ------- SECTION ------- 42 | .index = 1, 43 | .section_path = content/, 44 | .pages = [ 45 | content/page.smd 46 | content/badshtml.smd 47 | content/badhtml.smd 48 | content/badextend.smd 49 | content/another.smd 50 | ], 51 | 52 | 53 | 54 | ----- EXIT CODE: 1 ----- 55 | -------------------------------------------------------------------------------- /tests/content-scanning/templates/zine.ziggy: -------------------------------------------------------------------------------- 1 | Site { 2 | .title = "Sample Site", 3 | .host_url = "https://example.com", 4 | .content_dir_path = "content", 5 | .layouts_dir_path = "layouts", 6 | .assets_dir_path = "content", 7 | } -------------------------------------------------------------------------------- /tests/drafts/simple/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/drafts/simple/assets/.keep -------------------------------------------------------------------------------- /tests/drafts/simple/content/archive/2024/first.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "First post (2024)", 3 | .date = @date("2024-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "archive-entry.shtml", 6 | .draft = true, 7 | --- 8 | Lorem ipsum 9 | -------------------------------------------------------------------------------- /tests/drafts/simple/content/archive/2024/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "2024", 3 | .date = @date("2024-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # 2024 10 | 11 | Lorem ipsum 12 | -------------------------------------------------------------------------------- /tests/drafts/simple/content/archive/2025/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "2025", 3 | .date = @date("2025-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # 2025 10 | 11 | Lorem ipsum 12 | -------------------------------------------------------------------------------- /tests/drafts/simple/content/archive/2025/second.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Second post (2025)", 3 | .date = @date("2024-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "archive-entry.shtml", 6 | .draft = false, 7 | --- 8 | dolor something something 9 | -------------------------------------------------------------------------------- /tests/drafts/simple/content/archive/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Archive", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "archive.shtml", 6 | .draft = true, 7 | --- 8 | 9 | Archive 10 | -------------------------------------------------------------------------------- /tests/drafts/simple/content/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | 10 | 11 | # H1 12 | Lorem Ipsum 1 13 | 14 | ## H2 15 | Lorem Ipsum 2 16 | 17 | #### H3 18 | Lorem Ipsum 3 19 | 20 | -------------------------------------------------------------------------------- /tests/drafts/simple/content/sections.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Sections", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "sections.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # [H1]($section.id('h1')) 10 | Lorem Ipsum 1 11 | 12 | ## [H2]($section.id('h2')) 13 | Lorem Ipsum 2 14 | 15 | ### [H3]($section.id('h3')) 16 | Lorem Ipsum 3 17 | 18 | This is a footnote[^1] 19 | 20 | [^1]: That should not break this page 21 | -------------------------------------------------------------------------------- /tests/drafts/simple/layouts/archive-entry.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 |
11 | 12 |
13 | Prev: 14 | 15 |
16 |
17 | 18 | 19 | 20 | Prev: 21 | 22 | 23 | 24 | 25 | 26 |
27 | Next: 28 | 29 |
30 |
31 | 32 | 33 | 34 | Next: 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/drafts/simple/layouts/archive.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 |

11 | 12 | 13 |

14 |
15 |

16 | 17 | 18 |

19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /tests/drafts/simple/layouts/index.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 | 11 | -------------------------------------------------------------------------------- /tests/drafts/simple/layouts/sections.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 |

-- TABLE OF CONTENTS --

11 | 12 |
13 |
14 |

-- SECTION BEGIN --

15 |
16 |

-- SECTION END --

17 |
18 | 19 | -------------------------------------------------------------------------------- /tests/drafts/simple/snapshot.txt: -------------------------------------------------------------------------------- 1 | *-----------------------------------------------* 2 | | WARNING: THIS IS A DEBUG BUILD OF ZINE | 3 | |-----------------------------------------------| 4 | | Debug builds enable expensive sanity checks | 5 | | that reduce performance. | 6 | | | 7 | | To create a release build, run: | 8 | | | 9 | | zig build --release=fast | 10 | | | 11 | | If you're investigating a bug in Zine, then a | 12 | | debug build might turn confusing behavior | 13 | | into a crash. | 14 | | | 15 | | To disable all forms of concurrency, you can | 16 | | add the following flag to your build command: | 17 | | | 18 | | -Dsingle-threaded | 19 | | | 20 | *-----------------------------------------------* 21 | 22 | -------------------------------------------------------------------------------- /tests/drafts/simple/snapshot/archive/2024/first/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Test Website 7 | 8 | 9 |

First post (2024)

10 |

Lorem ipsum

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Next: 20 | Second post (2025) 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/drafts/simple/snapshot/archive/2024/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

2024

9 |

2024

Lorem ipsum

10 | 11 | -------------------------------------------------------------------------------- /tests/drafts/simple/snapshot/archive/2025/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

2025

9 |

2025

Lorem ipsum

10 | 11 | -------------------------------------------------------------------------------- /tests/drafts/simple/snapshot/archive/2025/second/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Test Website 7 | 8 | 9 |

Second post (2025)

10 |

dolor something something

11 | 12 | 13 | 14 | 15 | Prev: 16 | First post (2024) 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/drafts/simple/snapshot/archive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Archive

9 |
10 |

11 | 2025 12 | 13 |

14 |
15 |

16 | Second post (2025) 17 | 18 |

19 |
20 | 21 |

22 | 2024 23 | 24 |

25 |
26 |

27 | First post (2024) 28 | 29 |

30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /tests/drafts/simple/snapshot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Homepage

9 |

Your SuperMD content goes here.

H1

Lorem Ipsum 1

H2

Lorem Ipsum 2

H3

Lorem Ipsum 3

10 | 11 | -------------------------------------------------------------------------------- /tests/drafts/simple/snapshot/sections/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Sections

9 |
10 |

-- TABLE OF CONTENTS --

11 | 15 |
16 |
17 |

-- SECTION BEGIN --

18 |

H1

Lorem Ipsum 1

19 |

-- SECTION END --

20 | 21 |

-- SECTION BEGIN --

22 |

H2

Lorem Ipsum 2

23 |

-- SECTION END --

24 | 25 |

-- SECTION BEGIN --

26 |

H3

Lorem Ipsum 3

This is a footnote1

27 |

-- SECTION END --

28 |
29 | 30 | -------------------------------------------------------------------------------- /tests/drafts/simple/zine.ziggy: -------------------------------------------------------------------------------- 1 | Site { 2 | .title = "Simple Test Website", 3 | .host_url = "https://example.com", 4 | .content_dir_path = "content", 5 | .layouts_dir_path = "layouts", 6 | .assets_dir_path = "content", 7 | } -------------------------------------------------------------------------------- /tests/rendering/multi/content/de-DE/about.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "About", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | ## About Page (en-US) 10 | -------------------------------------------------------------------------------- /tests/rendering/multi/content/de-DE/contact-us.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Contact Us", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .translation_key = "contact-us-page", 7 | .draft = false, 8 | --- 9 | 10 | ## Contact us (de-DE) 11 | -------------------------------------------------------------------------------- /tests/rendering/multi/content/de-DE/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | ## Home (en-US) 10 | -------------------------------------------------------------------------------- /tests/rendering/multi/content/en-US/about.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "About", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | ## About Page (en-US) 10 | -------------------------------------------------------------------------------- /tests/rendering/multi/content/en-US/contact-us.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Contact us", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .translation_key = "contact-us-page", 7 | .draft = false, 8 | --- 9 | 10 | ## Contact us (en-US) 11 | -------------------------------------------------------------------------------- /tests/rendering/multi/content/en-US/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | ## Home (en-US) 10 | -------------------------------------------------------------------------------- /tests/rendering/multi/content/it-IT/contattaci.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Contattaci", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .translation_key = "contact-us-page", 7 | .draft = false, 8 | --- 9 | 10 | ## Contattaci (it-IT) 11 | -------------------------------------------------------------------------------- /tests/rendering/multi/content/it-IT/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("1990-01-01T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | ## Home (it-IT) 10 | -------------------------------------------------------------------------------- /tests/rendering/multi/i18n/de-DE.ziggy: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/rendering/multi/i18n/en-US.ziggy: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/rendering/multi/i18n/it-IT.ziggy: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/rendering/multi/layouts/blog.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 |

15 |
16 |
17 |

Post list

18 |
19 | 20 | 21 |

22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /tests/rendering/multi/layouts/blog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zine -- https://zine-ssg.io 7 | en-US 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/rendering/multi/layouts/devlog-archive.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 |

15 |
16 |
17 |

Past years

18 |
19 | 20 | 21 |

22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /tests/rendering/multi/layouts/devlog.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | 36 |

37 |
38 |
39 |
40 | 41 |

42 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /tests/rendering/multi/layouts/devlog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zine -- https://zine-ssg.io 7 | en-US 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/rendering/multi/layouts/index.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

6 |
7 | -------------------------------------------------------------------------------- /tests/rendering/multi/layouts/page.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

6 |
7 | -------------------------------------------------------------------------------- /tests/rendering/multi/layouts/post.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 |

25 |
26 |
27 | 33 | 39 |
40 | -------------------------------------------------------------------------------- /tests/rendering/multi/layouts/templates/base.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

11 | 15 | 16 |
17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /tests/rendering/multi/snapshot.txt: -------------------------------------------------------------------------------- 1 | *-----------------------------------------------* 2 | | WARNING: THIS IS A DEBUG BUILD OF ZINE | 3 | |-----------------------------------------------| 4 | | Debug builds enable expensive sanity checks | 5 | | that reduce performance. | 6 | | | 7 | | To create a release build, run: | 8 | | | 9 | | zig build --release=fast | 10 | | | 11 | | If you're investigating a bug in Zine, then a | 12 | | debug build might turn confusing behavior | 13 | | into a crash. | 14 | | | 15 | | To disable all forms of concurrency, you can | 16 | | add the following flag to your build command: | 17 | | | 18 | | -Dsingle-threaded | 19 | | | 20 | *-----------------------------------------------* 21 | 22 | -------------------------------------------------------------------------------- /tests/rendering/multi/snapshot/about/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 8 | 9 | 10 | 11 |

Test

12 | 22 | 23 |

About

24 |

About Page (en-US)

25 | 26 |
27 | en-US 28 | 29 | de-DE 30 |
31 | 32 | -------------------------------------------------------------------------------- /tests/rendering/multi/snapshot/contact-us/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 8 | 9 | 10 | 11 |

Test

12 | 22 | 23 |

Contact us

24 |

Contact us (en-US)

25 | 26 |
27 | en-US 28 | 29 | it-IT 30 | 31 | de-DE 32 |
33 | 34 | -------------------------------------------------------------------------------- /tests/rendering/multi/snapshot/de-DE/about/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 8 | 9 | 10 | 11 |

Test

12 | 22 | 23 |

About

24 |

About Page (en-US)

25 | 26 |
27 | en-US 28 | 29 | de-DE 30 |
31 | 32 | -------------------------------------------------------------------------------- /tests/rendering/multi/snapshot/de-DE/contact-us/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 8 | 9 | 10 | 11 |

Test

12 | 22 | 23 |

Contact Us

24 |

Contact us (de-DE)

25 | 26 |
27 | en-US 28 | 29 | it-IT 30 | 31 | de-DE 32 |
33 | 34 | -------------------------------------------------------------------------------- /tests/rendering/multi/snapshot/de-DE/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 8 | 9 | 10 | 11 |

Test

12 | 22 | 23 |

Homepage

24 |

Home (en-US)

25 | 26 |
27 | en-US 28 | 29 | it-IT 30 | 31 | de-DE 32 |
33 | 34 | -------------------------------------------------------------------------------- /tests/rendering/multi/snapshot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 8 | 9 | 10 | 11 |

Test

12 | 22 | 23 |

Homepage

24 |

Home (en-US)

25 | 26 |
27 | en-US 28 | 29 | it-IT 30 | 31 | de-DE 32 |
33 | 34 | -------------------------------------------------------------------------------- /tests/rendering/multi/snapshot/it-IT/contattaci/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Prova 7 | 8 | 9 | 10 | 11 |

Prova

12 | 19 | 20 |

Contattaci

21 |

Contattaci (it-IT)

22 | 23 |
24 | en-US 25 | 26 | it-IT 27 | 28 | de-DE 29 |
30 | 31 | -------------------------------------------------------------------------------- /tests/rendering/multi/snapshot/it-IT/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Prova 7 | 8 | 9 | 10 | 11 |

Prova

12 | 19 | 20 |

Homepage

21 |

Home (it-IT)

22 | 23 |
24 | en-US 25 | 26 | it-IT 27 | 28 | de-DE 29 |
30 | 31 | -------------------------------------------------------------------------------- /tests/rendering/multi/zine.ziggy: -------------------------------------------------------------------------------- 1 | Multilingual { 2 | .host_url = "https://example,.org", 3 | .i18n_dir_path = "i18n", 4 | .layouts_dir_path = "layouts", 5 | .assets_dir_path = "assets", 6 | .locales = [ 7 | { 8 | .code = "en-US", 9 | .name = "English (original)", 10 | .site_title = "Test", 11 | .content_dir_path = "content/en-US", 12 | .output_prefix_override = "", 13 | }, 14 | { 15 | .code = "it-IT", 16 | .name = "Italiano", 17 | .site_title = "Prova", 18 | .content_dir_path = "content/it-IT", 19 | }, 20 | { 21 | .code = "de-DE", 22 | .name = "German", 23 | .site_title = "Test", 24 | .content_dir_path = "content/de-DE", 25 | // .host_url_override = "de.example.org", 26 | 27 | }, 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /tests/rendering/simple/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/zine/aa43d44c2d271cb309549e8d7eab0a7fe6bb06f2/tests/rendering/simple/assets/.keep -------------------------------------------------------------------------------- /tests/rendering/simple/content/archive/2024/first.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "First post (2024)", 3 | .date = @date("2024-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "archive-entry.shtml", 6 | .draft = false, 7 | --- 8 | Lorem ipsum 9 | -------------------------------------------------------------------------------- /tests/rendering/simple/content/archive/2024/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "2024", 3 | .date = @date("2024-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # 2024 10 | 11 | Lorem ipsum 12 | -------------------------------------------------------------------------------- /tests/rendering/simple/content/archive/2025/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "2025", 3 | .date = @date("2025-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # 2025 10 | 11 | Lorem ipsum 12 | -------------------------------------------------------------------------------- /tests/rendering/simple/content/archive/2025/second.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Second post (2025)", 3 | .date = @date("2024-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "archive-entry.shtml", 6 | .draft = false, 7 | --- 8 | dolor something something 9 | -------------------------------------------------------------------------------- /tests/rendering/simple/content/archive/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Archive", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "archive.shtml", 6 | .draft = false, 7 | --- 8 | 9 | Archive 10 | -------------------------------------------------------------------------------- /tests/rendering/simple/content/index.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | Your **SuperMD** content goes here. 9 | 10 | 11 | # H1 12 | Lorem Ipsum 1 13 | 14 | ## H2 15 | Lorem Ipsum 2 16 | 17 | #### H3 18 | Lorem Ipsum 3 19 | 20 | -------------------------------------------------------------------------------- /tests/rendering/simple/content/nested/aliases.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Homepage", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | .aliases = [ 8 | "alias_relative.html", 9 | "/alias_absolute.html", 10 | "/aliases/path/with/leading/slash.html", 11 | "aliases/path/without/leading/slash.html", 12 | ], 13 | --- 14 | Your **SuperMD** content goes here. 15 | 16 | 17 | # H1 18 | Lorem Ipsum 1 19 | 20 | ## H2 21 | Lorem Ipsum 2 22 | 23 | #### H3 24 | Lorem Ipsum 3 25 | -------------------------------------------------------------------------------- /tests/rendering/simple/content/sections.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Sections", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "sections.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # [H1]($section.id('h1')) 10 | Lorem Ipsum 1 11 | 12 | ## [H2]($section.id('h2')) 13 | Lorem Ipsum 2 14 | 15 | ### [H3]($section.id('h3')) 16 | Lorem Ipsum 3 17 | 18 | This is a footnote[^1] 19 | 20 | [^1]: That should not break this page 21 | -------------------------------------------------------------------------------- /tests/rendering/simple/content/syntax.smd: -------------------------------------------------------------------------------- 1 | --- 2 | .title = "Sections", 3 | .date = @date("2020-07-06T00:00:00"), 4 | .author = "Sample Author", 5 | .layout = "index.shtml", 6 | .draft = false, 7 | --- 8 | 9 | # [Zig Code]($section.id('zig')) 10 | ```zig 11 | const Sample = struct { 12 | a: i32, 13 | b: i32, 14 | }; 15 | 16 | pub extern "sampleLibrary" fn SampleFunction( 17 | arg1: i32, 18 | arg2: *Sample, 19 | ) callconv(WINAPI) BOOL; 20 | 21 | pub inline fn main() anyerror!void { 22 | var sample = Sample{ .a = 1, .b = 2 }; 23 | SampleFunction(1, &sample); 24 | } 25 | ``` 26 | 27 | # [Rust Code]($section.id('rust')) 28 | ```rust 29 | #[derive(Debug, Clone, PartialEq)] 30 | pub struct ComplexData<'a, T: 'a + Display> { 31 | #[serde(rename = "identifier")] 32 | id: &'a str, 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | metadata: Option>, 35 | #[serde(default)] 36 | references: Vec<&'a RefCell>, 37 | timestamp: SystemTime, 38 | data: Box + 'a>, 39 | } 40 | 41 | impl<'a, T: Display> ComplexData<'a, T> { 42 | pub fn new(id: &'a str) -> Result> { 43 | Ok(Self { 44 | id, 45 | metadata: None, 46 | references: Vec::new(), 47 | timestamp: SystemTime::now(), 48 | data: Box::new(std::iter::empty()), 49 | }) 50 | } 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /tests/rendering/simple/layouts/archive-entry.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

10 |
11 | 12 |
13 | Prev: 14 | 15 |
16 |
17 | 18 | 19 | 20 | Prev: 21 | 22 | 23 | 24 | 25 | 26 |
27 | Next: 28 | 29 |
30 |
31 | 32 | 33 | 34 | Next: 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/rendering/simple/layouts/archive.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 |

11 | 12 | 13 |

14 |
15 |

16 | 17 | 18 |

19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /tests/rendering/simple/layouts/index.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 | 11 | -------------------------------------------------------------------------------- /tests/rendering/simple/layouts/sections.shtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 |
10 |

-- TABLE OF CONTENTS --

11 | 12 |
13 |
14 |

-- SECTION BEGIN --

15 |
16 |

-- SECTION END --

17 |
18 | 19 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot.txt: -------------------------------------------------------------------------------- 1 | *-----------------------------------------------* 2 | | WARNING: THIS IS A DEBUG BUILD OF ZINE | 3 | |-----------------------------------------------| 4 | | Debug builds enable expensive sanity checks | 5 | | that reduce performance. | 6 | | | 7 | | To create a release build, run: | 8 | | | 9 | | zig build --release=fast | 10 | | | 11 | | If you're investigating a bug in Zine, then a | 12 | | debug build might turn confusing behavior | 13 | | into a crash. | 14 | | | 15 | | To disable all forms of concurrency, you can | 16 | | add the following flag to your build command: | 17 | | | 18 | | -Dsingle-threaded | 19 | | | 20 | *-----------------------------------------------* 21 | 22 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/alias_absolute.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Homepage

9 |

Your SuperMD content goes here.

H1

Lorem Ipsum 1

H2

Lorem Ipsum 2

H3

Lorem Ipsum 3

10 | 11 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/aliases/path/with/leading/slash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Homepage

9 |

Your SuperMD content goes here.

H1

Lorem Ipsum 1

H2

Lorem Ipsum 2

H3

Lorem Ipsum 3

10 | 11 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/archive/2024/first/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Test Website 7 | 8 | 9 |

First post (2024)

10 |

Lorem ipsum

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Next: 20 | Second post (2025) 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/archive/2024/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

2024

9 |

2024

Lorem ipsum

10 | 11 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/archive/2025/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

2025

9 |

2025

Lorem ipsum

10 | 11 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/archive/2025/second/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Test Website 7 | 8 | 9 |

Second post (2025)

10 |

dolor something something

11 | 12 | 13 | 14 | 15 | Prev: 16 | First post (2024) 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/archive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Archive

9 |
10 |

11 | 2025 12 | 13 |

14 |
15 |

16 | Second post (2025) 17 | 18 |

19 |
20 | 21 |

22 | 2024 23 | 24 |

25 |
26 |

27 | First post (2024) 28 | 29 |

30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Homepage

9 |

Your SuperMD content goes here.

H1

Lorem Ipsum 1

H2

Lorem Ipsum 2

H3

Lorem Ipsum 3

10 | 11 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/nested/aliases/alias_relative.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Homepage

9 |

Your SuperMD content goes here.

H1

Lorem Ipsum 1

H2

Lorem Ipsum 2

H3

Lorem Ipsum 3

10 | 11 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/nested/aliases/aliases/path/without/leading/slash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Homepage

9 |

Your SuperMD content goes here.

H1

Lorem Ipsum 1

H2

Lorem Ipsum 2

H3

Lorem Ipsum 3

10 | 11 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/nested/aliases/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Homepage

9 |

Your SuperMD content goes here.

H1

Lorem Ipsum 1

H2

Lorem Ipsum 2

H3

Lorem Ipsum 3

10 | 11 | -------------------------------------------------------------------------------- /tests/rendering/simple/snapshot/sections/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Website 6 | 7 | 8 |

Sections

9 |
10 |

-- TABLE OF CONTENTS --

11 | 15 |
16 |
17 |

-- SECTION BEGIN --

18 |

H1

Lorem Ipsum 1

19 |

-- SECTION END --

20 | 21 |

-- SECTION BEGIN --

22 |

H2

Lorem Ipsum 2

23 |

-- SECTION END --

24 | 25 |

-- SECTION BEGIN --

26 |

H3

Lorem Ipsum 3

This is a footnote1

27 |

-- SECTION END --

28 |
29 | 30 | -------------------------------------------------------------------------------- /tests/rendering/simple/zine.ziggy: -------------------------------------------------------------------------------- 1 | Site { 2 | .title = "Simple Test Website", 3 | .host_url = "https://example.com", 4 | .content_dir_path = "content", 5 | .layouts_dir_path = "layouts", 6 | .assets_dir_path = "content", 7 | } --------------------------------------------------------------------------------