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
35 changes: 35 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Reject release-tag pushes that don't include a CHANGELOG.md update since the
# previous release tag. Installed into .git/hooks/pre-push by the Nix devshell
# (see flake.nix shellHook). The pre-commit framework only manages the
# pre-commit hook, so pre-push is ours.
#
# Bypass (discouraged): git push --no-verify
set -euo pipefail

tag_re='^refs/tags/v?[0-9]+\.[0-9]+\.[0-9]+$'
status=0

# git feeds one line per ref being pushed: <local_ref> <local_sha> <remote_ref> <remote_sha>
while read -r local_ref local_sha _remote_ref _remote_sha; do
[[ "$local_ref" =~ $tag_re ]] || continue
# Tag deletion (all-zero local sha) — nothing to verify.
[[ "$local_sha" =~ ^0+$ ]] && continue

tag="${local_ref#refs/tags/}"
# Use the tag ref, not $local_sha: for annotated tags the pushed sha is the
# tag object, so resolve through $tag so both lightweight and annotated tags
# peel to the tagged commit.
# Nearest release tag before this commit. None => first release, allow it.
prev="$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)"
[ -n "$prev" ] || continue

if git diff --quiet "$prev" "$tag" -- CHANGELOG.md; then
echo "✗ $tag: CHANGELOG.md has no changes since $prev." >&2
echo " Update the changelog before tagging — e.g. 'nix run .#bump -- <version>'." >&2
echo " (Override with 'git push --no-verify' if you really mean to.)" >&2
status=1
fi
done

exit "$status"
22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ jobs:

steps:
- uses: actions/checkout@v6
with:
# Full history + tags so the release-tag CHANGELOG guard can diff
# against the previous tag.
fetch-depth: 0

Comment thread
tsunaminoai marked this conversation as resolved.
# Authoritative backstop for the local pre-push hook (.githooks/pre-push):
# a release tag must include a CHANGELOG.md change since the previous tag.
# Cannot be bypassed with `git push --no-verify`.
- name: Require CHANGELOG update for release tag
if: ${{ github.ref_type == 'tag' }}
run: |
tag="${{ github.ref_name }}"
prev="$(git describe --tags --abbrev=0 "${tag}^" 2>/dev/null || true)"
if [ -z "$prev" ]; then
echo "No previous tag; skipping CHANGELOG check."
exit 0
fi
if git diff --quiet "$prev" "$tag" -- CHANGELOG.md; then
echo "::error::CHANGELOG.md has no changes between $prev and $tag. Update the changelog (e.g. 'nix run .#bump') before releasing."
exit 1
fi
echo "CHANGELOG.md updated since $prev ✓"

- name: Install Nix
uses: DeterminateSystems/determinate-nix-action@v3
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ Versions track the library API, not Zig itself. The major version will
remain 0.x until Zig reaches 1.0, at which point zigmark will follow the
same stability guarantee.

## [Unreleased]

## [0.7.2] — 2026-07-03

### Fixed

- **Typst mermaid embeds now actually compile.** The Typst renderer emitted
`#image.decode(bytes.fromBase64("…"))`, but `bytes.fromBase64` does not
exist in Typst (and `image.decode` is deprecated) — every document with a
rendered mermaid block failed `typst compile`. Diagrams are now embedded as
`image(bytes("<svg…>"), format: "svg")` with the SVG source in a string
literal (verified against typst 0.14.2). The existing tests only asserted
the emitted string, which is how this slipped through.
- Mermaid diagrams are rendered at their natural size and only scaled down
when wider than the line width (via a `layout`/`measure` wrapper), instead
of being blown up to full text width.

### Added

- `renderTypstWithMermaid` / `TypstRenderer`'s `renderToWriterWithMermaid` —
body-only Typst rendering with the mermaid hook, for callers that supply
their own preamble (previously the hook was only reachable through the
full-document `renderTypstDocWithMermaid`).

## [0.7.0] — 2026-06-04

### Breaking
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ Requires **Zig 0.16.0** or later.
- **`Parser`** — Block-level + inline two-pass parser built on the [mecha](https://github.com/Hejsil/mecha) parser combinator library; accepts a `[]const u8` via `parseMarkdown` or any `*std.Io.Reader` via `parseFromReader`
- **`AST`** — Typed union-based Abstract Syntax Tree (`Document` → `Block` → `Inline`)
- **`HTMLRenderer`** — CommonMark-compliant HTML serialiser; `renderHtmlWithMermaid` accepts an optional `MermaidRendererFn` (`*const fn(Allocator, []const u8) anyerror![]const u8`) to convert `mermaid`/`mermaidjs` fenced blocks to inline SVG `<figure>` elements (falls back to a plain code block on error or when `null`)
- **`TypstRenderer`** — Typst markup renderer; `typst.renderDocument` adds an eisvogel-inspired preamble (title page, TOC, headers/footers, styled code blocks and blockquotes) driven by `DocumentOptions`; `renderTypstDocWithMermaid` accepts the same optional `MermaidRendererFn` (`*const fn(Allocator, []const u8) anyerror![]const u8`) and converts `mermaid`/`mermaidjs` blocks to `#image.decode` calls
- **`TypstRenderer`** — Typst markup renderer; `typst.renderDocument` adds an eisvogel-inspired preamble (title page, TOC, headers/footers, styled code blocks and blockquotes) driven by `DocumentOptions`; `renderTypstDocWithMermaid` (full document) and `renderTypstWithMermaid` (body only) accept the same optional `MermaidRendererFn` (`*const fn(Allocator, []const u8) anyerror![]const u8`) and convert each `mermaid`/`mermaidjs` block to an inline Typst `#{ … }` block that embeds the SVG via `image(bytes("…"), format: "svg")`, rendered at natural size and capped at the line width via a `layout`/`measure` wrapper
- **`ASTRenderer`** — Human-readable tree diagram with box-drawing characters
- **`AIRenderer`** — Token-efficient AST representation for LLM consumption
- **`MarkdownRenderer`** — AST→Markdown normaliser; converts headings to ATX, links to inline, code blocks to fenced
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
.name = .zigmark,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.7.0",
.version = "0.7.2",
// Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
Expand Down
128 changes: 128 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@

shellHook = ''
export ZIG_GLOBAL_CACHE_DIR=.zig-cache

# Install the release-tag CHANGELOG guard. Kept in .githooks/ so
# it is version-controlled; the release workflow enforces the same
# rule as an un-bypassable backstop.
hooks_dir="$(git rev-parse --git-path hooks 2>/dev/null || true)"
if [ -n "$hooks_dir" ] && [ -f .githooks/pre-push ]; then
mkdir -p "$hooks_dir"
cp -f .githooks/pre-push "$hooks_dir/pre-push"
chmod +x "$hooks_dir/pre-push"
fi

echo "To update Zig dependencies, run: update-zon"
echo "To run the fuzzer, run: fuzz [port] (default port: 8080)"
# Auto-generate/update zon2json-lock when entering the dev shell.
Expand Down Expand Up @@ -414,6 +425,123 @@
program = "${bench-app}/bin/zigmark-bench";
meta.description = "Benchmark zigmark against cmark, cmark-gfm, pandoc, discount, and lowdown; updates README.md with results";
};

# `nix run .#bump -- <version|patch|minor|major>` performs the manual
# pre-release steps in one shot: bumps the version in build.zig.zon,
# rolls the CHANGELOG `[Unreleased]` section into a dated release
# section, and commits. Pushing the tag is left to you; the release
# workflow then builds artifacts and publishes.
bump = let
bump-app = pkgs.writeShellApplication {
name = "zigmark-bump";
runtimeInputs = with pkgs; [coreutils gnused gawk git];
meta.description = "Bump version in build.zig.zon and roll the CHANGELOG";
text = ''
dry=0
commit=1
arg=""
for a in "$@"; do
case "$a" in
--dry-run) dry=1 ;;
--no-commit) commit=0 ;;
-h | --help)
echo "usage: nix run .#bump -- <version|patch|minor|major> [--dry-run] [--no-commit]"
exit 0
;;
*) arg="$a" ;;
esac
done

if [ -z "$arg" ]; then
echo "usage: nix run .#bump -- <version|patch|minor|major> [--dry-run] [--no-commit]" >&2
exit 1
fi

cur=$(sed -nE 's/^[[:space:]]*\.version = "([^"]+)",/\1/p' build.zig.zon | head -n1)
if [ -z "$cur" ]; then
echo "error: could not read current version from build.zig.zon" >&2
exit 1
fi

case "$arg" in
major | minor | patch)
IFS=. read -r ma mi pa <<< "$cur"
case "$arg" in
major)
ma=$((ma + 1))
mi=0
pa=0
;;
minor)
mi=$((mi + 1))
pa=0
;;
patch) pa=$((pa + 1)) ;;
esac
new="$ma.$mi.$pa"
;;
*) new="$arg" ;;
esac

if ! [[ "$new" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "error: invalid version '$new' (expected X.Y.Z or patch|minor|major)" >&2
exit 1
fi

today=$(date +%F)
echo "Release: $cur -> $new ($today)"

if [ "$dry" = 1 ]; then
echo "[dry-run] would update:"
echo " build.zig.zon .version = \"$new\""
echo " CHANGELOG.md roll [Unreleased] into [$new] - $today"
exit 0
fi

if ! grep -q '^## \[Unreleased\]' CHANGELOG.md; then
echo "error: no '## [Unreleased]' section in CHANGELOG.md; add one (with notes) before bumping" >&2
exit 1
fi

sed -i -E "s/^([[:space:]]*\.version = )\"[^\"]+\",/\1\"$new\",/" build.zig.zon
if ! awk -v ver="$new" -v dt="$today" '
!done && /^## \[Unreleased\]/ {
print "## [Unreleased]";
print "";
print "## [" ver "] \xe2\x80\x94 " dt;
done = 1;
next
}
{ print }
END { if (!done) exit 3 }
' CHANGELOG.md > CHANGELOG.md.tmp; then
rm -f CHANGELOG.md.tmp
echo "error: failed to roll '## [Unreleased]' in CHANGELOG.md" >&2
exit 1
fi
mv CHANGELOG.md.tmp CHANGELOG.md

echo "Updated build.zig.zon, CHANGELOG.md"

if [ "$commit" = 1 ]; then
git add build.zig.zon CHANGELOG.md
git commit -m "chore: release $new"
echo ""
echo "Committed 'chore: release $new'. Publish with:"
echo " git tag v$new && git push origin HEAD v$new"
else
echo ""
echo "Files edited (not committed). Then:"
echo " git add build.zig.zon CHANGELOG.md && git commit -m 'chore: release $new'"
echo " git tag v$new && git push origin HEAD v$new"
fi
'';
};
in {
type = "app";
program = "${bump-app}/bin/zigmark-bump";
meta.description = "Bump version in build.zig.zon and roll the CHANGELOG for a release";
};
}
);
};
Expand Down
5 changes: 4 additions & 1 deletion src/markdown/renderers/html.zig
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,10 @@ pub fn renderToWriter(allocator: Allocator, writer: *std.Io.Writer, doc: AST.Doc
}

/// Render `doc` to a writer, converting mermaid fenced blocks to inline SVG
/// using the provided renderer function.
/// using the provided renderer function. The renderer must return memory
/// allocated with `allocator`; it is freed with that allocator after the
/// diagram is emitted, so returning static or externally-owned memory will
/// trigger an invalid free.
pub fn renderToWriterWithMermaid(
allocator: Allocator,
writer: *std.Io.Writer,
Expand Down
Loading
Loading