Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/rulesets/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "Default Ruleset",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"exclude": [],
"include": ["~DEFAULT_BRANCH"]
}
},
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{ "type": "required_signatures" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": false,
"required_reviewers": [],
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false,
"allowed_merge_methods": ["merge", "squash", "rebase"]
}
},
{
"type": "copilot_code_review",
"parameters": {
"review_on_push": true,
"review_draft_pull_requests": false
}
}
],
"bypass_actors": [
{
"actor_id": null,
"actor_type": "OrganizationAdmin",
"bypass_mode": "always"
},
{
"actor_id": 5,
"actor_type": "RepositoryRole",
"bypass_mode": "always"
}
]
}
28 changes: 28 additions & 0 deletions .github/rulesets/regression-protection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "regression-protection",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"exclude": [],
"include": ["~ALL"]
}
},
"rules": [
{ "type": "deletion" },
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": false,
"do_not_enforce_on_create": true,
"required_status_checks": [
{ "context": "CI / build (x86_64-linux)" },
{ "context": "CI / build (aarch64-linux)" },
{ "context": "CI / wasm" },
{ "context": "CI / bench-regression" }
]
}
}
],
"bypass_actors": []
}
124 changes: 124 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ concurrency:

jobs:
build:
name: build (${{ matrix.system }})
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -59,3 +60,126 @@ jobs:
name: zigmark-${{ matrix.system }}
path: zigmark-release/
retention-days: 7

# ── WASM build ─────────────────────────────────────────────────────────────
# Separate job so "CI / wasm" appears as a distinct required check in branch
# rulesets. The nix check (flake#checks.wasm) also covers this in `build`,
# but that check is bundled under the matrix job name and not independently
# addressable. Running it standalone here gives us a stable check name.
wasm:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
sandbox = false
- name: Setup Nix cache
uses: DeterminateSystems/flakehub-cache-action@v3
with:
use-gha-cache: "enabled"
flakehub-flake-name: "sc2in/ZigMark"
- name: Build WASM module
run: nix build .#checks.x86_64-linux.wasm

# ── Benchmark regression ───────────────────────────────────────────────────
# Downloads the last tagged release binary and compares it against the
# current build on the same machine (cancels runner variance).
# Fails if current_mean > baseline_mean + 2 * baseline_stddev.
# Skipped automatically when no prior release exists (e.g. first PR).
bench-regression:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
sandbox = false
- name: Setup Nix cache
uses: DeterminateSystems/flakehub-cache-action@v3
with:
use-gha-cache: "enabled"
flakehub-flake-name: "sc2in/ZigMark"

- name: Build current binary (ReleaseFast)
run: nix build .#zigmark-fast -o zigmark-current

- name: Download last release baseline
id: baseline
env:
GH_TOKEN: ${{ github.token }}
run: |
LAST_TAG=$(gh release list -L 1 --json tagName -q '.[0].tagName' 2>/dev/null || true)
if [ -z "$LAST_TAG" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No prior release found — skipping regression check."
exit 0
fi
mkdir -p baseline
if ! gh release download "$LAST_TAG" \
-p "zigmark-*-x86_64-linux.tar.gz" -D baseline/ 2>/dev/null; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Release asset not found for $LAST_TAG — skipping."
exit 0
fi
tar xzf baseline/*.tar.gz -C baseline/
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "tag=$LAST_TAG" >> "$GITHUB_OUTPUT"

- name: Run hyperfine comparison
if: steps.baseline.outputs.skip == 'false'
run: |
nix profile add nixpkgs#hyperfine
hyperfine \
--warmup 100 -m 300 \
--export-json bench-results.json \
--command-name current \
"./zigmark-current/bin/zigmark README.md > /dev/null" \
--command-name "baseline (${{ steps.baseline.outputs.tag }})" \
"./baseline/bin/zigmark README.md > /dev/null"

- name: Check for regression
if: steps.baseline.outputs.skip == 'false'
run: |
python3 - <<'PYEOF'
import json, sys

with open("bench-results.json") as f:
results = json.load(f)["results"]

current = next(r for r in results if r["command"] == "current")
baseline = next(r for r in results if "baseline" in r["command"])

c_mean = current["mean"]
b_mean = baseline["mean"]
b_std = baseline["stddev"]
threshold = b_mean + 2 * b_std
ratio = c_mean / b_mean if b_mean > 0 else 1.0

print(f"Current: {c_mean*1000:.3f} ms")
print(f"Baseline: {b_mean*1000:.3f} ms (σ = {b_std*1000:.3f} ms)")
print(f"Threshold: {threshold*1000:.3f} ms (baseline + 2σ)")
print(f"Ratio: {ratio:.3f}×")

if c_mean > threshold:
print(f"\n❌ Regression: {c_mean*1000:.3f} ms > {threshold*1000:.3f} ms")
sys.exit(1)

print(f"\n✅ No regression: {c_mean*1000:.3f} ms ≤ {threshold*1000:.3f} ms")
PYEOF

- name: Upload benchmark results
if: steps.baseline.outputs.skip == 'false'
uses: actions/upload-artifact@v4
with:
name: bench-results
path: bench-results.json
retention-days: 30
46 changes: 46 additions & 0 deletions .github/workflows/sync-rulesets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Sync Rulesets

on:
push:
branches: ["main"]
paths:
- ".github/rulesets/**"

jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6

- name: Sync rulesets via API
env:
GH_TOKEN: ${{ secrets.RULESET_SYNC_TOKEN }}
run: |
REPO="${{ github.repository }}"

sync_ruleset() {
local file="$1"
local name
name=$(jq -r '.name' "$file")

existing_id=$(gh api "repos/$REPO/rulesets" \
--jq ".[] | select(.name == \"$name\") | .id" 2>/dev/null || true)

if [ -n "$existing_id" ]; then
echo "Updating ruleset '$name' (id=$existing_id)"
gh api "repos/$REPO/rulesets/$existing_id" \
--method PUT \
--input "$file"
else
echo "Creating ruleset '$name'"
gh api "repos/$REPO/rulesets" \
--method POST \
--input "$file"
fi
}

for f in .github/rulesets/*.json; do
sync_ruleset "$f"
done
117 changes: 117 additions & 0 deletions bench/input-mermaid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Architecture Overview

This document contains representative mermaid diagrams for benchmark purposes.
It measures how parsers handle fenced code blocks with diagram content.

## System Architecture

```mermaid
flowchart TD
Client[Browser / CLI] --> Parser
Parser --> AST[Abstract Syntax Tree]
AST --> HTML[HTML Renderer]
AST --> Typst[Typst Renderer]
AST --> AI[AI Renderer]
HTML --> Mermaid[Mermaid diagrams via pozeiden]
Mermaid --> SVG[Inline SVG]
```

## Request Lifecycle

```mermaid
sequenceDiagram
participant U as User
participant B as Browser
participant W as zigmark.wasm
participant P as pozeiden

U->>B: types Markdown
B->>W: alloc_buf + render_html
W->>W: Parser.parseMarkdown
W->>W: HTMLRenderer
W->>P: render(mermaid_src)
P-->>W: SVG string
W-->>B: HTML with inline SVG
B->>U: live preview
```

## Parsing Pipeline

```mermaid
flowchart LR
Input[Raw Markdown] --> Tokenizer
Tokenizer --> Lexer
Lexer --> BlockParser[Block Parser]
BlockParser --> InlineParser[Inline Parser]
InlineParser --> AST
AST --> HTMLRenderer
AST --> ASTRenderer
AST --> AIRenderer
```

## Renderer Output Formats

```mermaid
pie title Renderer usage distribution
"HTML" : 60
"Typst / PDF" : 20
"AI / LLM" : 15
"AST / Debug" : 5
```

## State Machine: Parser States

```mermaid
stateDiagram-v2
[*] --> Start
Start --> InBlock: blank line
Start --> InParagraph: text
InParagraph --> InBlock: blank line
InParagraph --> InParagraph: more text
InBlock --> InFenced: ``` marker
InFenced --> InBlock: closing ```
InBlock --> [*]: EOF
```

## Memory Management

```mermaid
flowchart TD
A[page_allocator] --> B[ArenaAllocator.init]
B --> C{render call}
C --> D[parse allocations]
C --> E[render allocations]
D --> F[arena.reset retain_capacity]
E --> F
F --> C
```

## GFM Tables (non-diagram content for balance)

| Renderer | Output | Mermaid | Speed |
| ---------- | ----------- | ------- | ------- |
| HTML | `.html` | yes | fast |
| Typst | `.typ` | no | fast |
| AI | plain text | no | fastest |
| AST | tree | no | fast |

## Task list

- [x] CommonMark 652/652 tests
- [x] GFM extensions
- [x] Mermaid via pozeiden
- [x] WASM target
- [ ] SIMD optimisation

## Code (non-diagram fenced block)

```zig
const Parser = zigmark.Parser.init();
const doc = try parser.parseMarkdown(alloc, markdown);
defer doc.deinit(alloc);
const html = try zigmark.HTMLRenderer.render(alloc, doc);
```

---

_Benchmark input — contains 6 mermaid blocks and representative GFM content._
9 changes: 7 additions & 2 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ pub fn build(b: *std.Build) void {
const wasm_optimize = b.option(
std.builtin.OptimizeMode,
"wasm-optimize",
"Optimization for WASM target (default: ReleaseSmall)",
) orelse .ReleaseSmall;
"Optimization for WASM target (default: ReleaseFast)",
) orelse .ReleaseFast;

const wasm_tomlz = b.dependency("tomlz", .{ .target = wasm_target, .optimize = wasm_optimize });
const wasm_yaml = b.dependency("yaml", .{ .target = wasm_target, .optimize = wasm_optimize });
Expand Down Expand Up @@ -337,6 +337,11 @@ pub fn build(b: *std.Build) void {
wasm_lib.entry = .disabled;
wasm_lib.root_module.export_symbol_names = &.{
"render_html",
"render_html_arena",
"render_html_fba",
"render_html_nomermaid",
"flush_mermaid_cache",
"parse_only",
"render_ast",
"render_ai",
"result_len",
Expand Down
Loading
Loading