diff --git a/build.zig b/build.zig
index 8b5dc22..bda6682 100644
--- a/build.zig
+++ b/build.zig
@@ -339,20 +339,45 @@ fn defaultZineOptions(b: *std.Build, debug: bool) ZineOptions {
pub fn scriptyReferenceDocs(
project: *std.Build,
- output_file_path: []const u8,
+ shtml_output_file_path: []const u8,
+ smd_output_file_path: []const u8,
) void {
const zine_dep = project.dependencyFromBuildZig(
zine,
.{ .optimize = .Debug },
);
- const run_docgen = project.addRunArtifact(zine_dep.artifact("docgen"));
- const reference_md = run_docgen.addOutputFileArg("scripty_reference.md");
+ const run_step = project.step(
+ "docgen",
+ "Regenerates Scripty reference docs",
+ );
+
+ {
+ const run_docgen = project.addRunArtifact(
+ zine_dep.artifact("shtml_docgen"),
+ );
+
+ const reference_md = run_docgen.addOutputFileArg(
+ "shtml_scripty_reference.md",
+ );
+
+ const wf = project.addWriteFiles();
+ wf.addCopyFileToSource(reference_md, shtml_output_file_path);
- const wf = project.addWriteFiles();
- wf.addCopyFileToSource(reference_md, output_file_path);
+ run_step.dependOn(&wf.step);
+ }
+ {
+ const run_docgen = project.addRunArtifact(
+ zine_dep.artifact("smd_docgen"),
+ );
+
+ const reference_md = run_docgen.addOutputFileArg(
+ "smd_scripty_reference.md",
+ );
- const desc = project.fmt("Regenerates Scripty reference docs in '{s}'", .{output_file_path});
- const run_step = project.step("docgen", desc);
- run_step.dependOn(&wf.step);
+ const wf = project.addWriteFiles();
+ wf.addCopyFileToSource(reference_md, smd_output_file_path);
+
+ run_step.dependOn(&wf.step);
+ }
}
diff --git a/build.zig.zon b/build.zig.zon
index d86c490..1bb878e 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -6,12 +6,10 @@
.path = "supermd",
},
.scripty = .{
- .url = "git+https://github.com/kristoff-it/scripty#9ed452984a0d38eed3485bf2314553d5bffc17b0",
- .hash = "122000048e34a01f4f57dcb17955df0a7c5bd57411c022916d8ea6f7e8c9c4a94c4d",
+ .path = "../scripty",
},
.superhtml = .{
- .url = "git+https://github.com/kristoff-it/superhtml#5cdf5a1dd81801f052ebc99281f482c450cbf448",
- .hash = "1220d57b3010d361f02335974779d5d89ad9e1eee314bed55bc8276f844b517d4a66",
+ .path = "../superhtml",
},
.ziggy = .{
.url = "git+https://github.com/kristoff-it/ziggy#c66f47bc632c66668d61fa06eda112b41d6e5130",
diff --git a/build/content.zig b/build/content.zig
index aeebd16..1ce3c67 100644
--- a/build/content.zig
+++ b/build/content.zig
@@ -282,17 +282,22 @@ fn writeAssetIndex(
\\
\\
;
- switch (asset.lp) {
- .src_path, .cwd_relative => {
- std.debug.print(msg, .{asset.name});
- std.process.exit(1);
- },
- .generated, .dependency => {
- run.addArg(asset.name);
- run.addFileArg(asset.lp);
- run.addArg(asset.install_path orelse "null");
- },
- }
+
+ _ = msg;
+ // switch (asset.lp) {
+ // .src_path, .cwd_relative => {
+ // std.debug.print(msg, .{asset.name});
+ // std.process.exit(1);
+ // },
+ // .generated, .dependency => {
+ // run.addArg(asset.name);
+ // run.addFileArg(asset.lp);
+ // run.addArg(asset.install_path orelse "null");
+ // },
+ // }
+ run.addArg(asset.name);
+ run.addFileArg(asset.lp);
+ run.addArg(asset.install_path orelse "null");
}
index_step.dependOn(&run.step);
@@ -444,19 +449,19 @@ pub fn scanVariant(
const fm = switch (result) {
.success => |s| s.header,
.empty => {
- std.debug.panic("WARNING: ignoring empty file '{s}{s}'\n", .{
+ std.debug.print("WARNING: ignoring empty file '{s}{s}'\n", .{
permalink, "index.md",
});
break :blk;
},
.framing_error => |line| {
- std.debug.panic("ERROR: bad frontmatter framing in '{s}{s}' (line {})\n", .{
+ std.debug.print("ERROR: bad frontmatter framing in '{s}{s}' (line {})\n", .{
permalink, "index.md", line,
});
std.process.exit(1);
},
.ziggy_error => |diag| {
- std.debug.panic("{s}{}", .{ permalink, diag });
+ std.debug.print("{s}{}", .{ permalink, diag });
std.process.exit(1);
},
};
@@ -524,19 +529,19 @@ pub fn scanVariant(
const fm = switch (result) {
.success => |s| s.header,
.empty => {
- std.debug.panic("WARNING: ignoring empty file '{s}.md'\n", .{
+ std.debug.print("WARNING: ignoring empty file '{s}.md'\n", .{
permalink,
});
continue;
},
.framing_error => |line| {
- std.debug.panic("ERROR: bad frontmatter framing in '{s}.md' (line {})\n", .{
+ std.debug.print("ERROR: bad frontmatter framing in '{s}.md' (line {})\n", .{
permalink, line,
});
std.process.exit(1);
},
.ziggy_error => |diag| {
- std.debug.panic("{}", .{diag});
+ std.debug.print("{}", .{diag});
std.process.exit(1);
},
};
diff --git a/build/tools.zig b/build/tools.zig
index b07b0e1..8529f81 100644
--- a/build/tools.zig
+++ b/build/tools.zig
@@ -52,12 +52,13 @@ pub fn build(b: *std.Build) !void {
// "BDFL version resolution" strategy
const scripty = b.dependency("scripty", .{}).module("scripty");
- const supermd = b.dependency("supermd", mode).module("supermd");
- supermd.addImport("scripty", scripty);
-
const superhtml = b.dependency("superhtml", mode).module("superhtml");
superhtml.addImport("scripty", scripty);
+ const supermd = b.dependency("supermd", mode).module("supermd");
+ supermd.addImport("scripty", scripty);
+ supermd.addImport("superhtml", superhtml);
+
const ziggy = b.dependency("ziggy", mode).module("ziggy");
const zeit = b.dependency("zeit", mode).module("zeit");
const syntax = b.dependency("flow-syntax", mode);
@@ -99,16 +100,27 @@ pub fn build(b: *std.Build) !void {
b.installArtifact(layout);
- const docgen = b.addExecutable(.{
- .name = "docgen",
+ const shtml_docgen = b.addExecutable(.{
+ .name = "shtml_docgen",
.root_source_file = b.path("src/exes/docgen.zig"),
.target = target,
.optimize = .Debug,
});
- docgen.root_module.addImport("zine", zine);
- docgen.root_module.addImport("zeit", zeit);
- docgen.root_module.addImport("ziggy", ziggy);
- b.installArtifact(docgen);
+ shtml_docgen.root_module.addImport("zine", zine);
+ shtml_docgen.root_module.addImport("zeit", zeit);
+ shtml_docgen.root_module.addImport("ziggy", ziggy);
+ b.installArtifact(shtml_docgen);
+
+ const smd_docgen = b.addExecutable(.{
+ .name = "smd_docgen",
+ .root_source_file = b.path("supermd/src/docgen.zig"),
+ .target = target,
+ .optimize = .Debug,
+ });
+ smd_docgen.root_module.addImport("zeit", zeit);
+ smd_docgen.root_module.addImport("ziggy", ziggy);
+ smd_docgen.root_module.addImport("scripty", scripty);
+ b.installArtifact(smd_docgen);
// const md_renderer = b.addExecutable(.{
// .name = "markdown-renderer",
diff --git a/frontmatter.ziggy-schema b/frontmatter.ziggy-schema
index 588186a..bf20558 100644
--- a/frontmatter.ziggy-schema
+++ b/frontmatter.ziggy-schema
@@ -4,38 +4,49 @@ root = Frontmatter
@date = bytes,
struct Frontmatter {
- /// The title of this page.
+ ///The title of this page.
title: ?bytes,
- /// A short description that the section page has access to.
+ ///A short description that the section page has
+ ///access to.
description: ?bytes,
- /// The main author of this page.
+ ///The main author of this page.
author: ?bytes,
date: ?@date,
tags: ?[bytes],
- /// Alternative paths where this content will also be made available.
+ ///Alternative paths where this content will also be
+ ///made available.
aliases: ?[bytes],
- /// When set to true this file will be ignored when bulding the website.
+ ///When set to true this file will be ignored when
+ ///bulding the website.
draft: ?bool,
- /// Path to a layout file inside of the configured layouts directory.
+ ///Path to a layout file inside of the configured
+ ///layouts directory.
layout: bytes,
- /// Alternative versions of this page, created by rendering the content
- /// using a different layout. Useful for creating RSS feeds, for example.
+ ///Alternative versions of this page, created by
+ ///rendering the content using a different layout.
+ ///Useful for creating RSS feeds, for example.
alternatives: ?[Alternative],
- /// Ignore other markdown files in this directory and any sub-directory.
- /// Can only be meaningfully set to true for 'index.md' pages.
+ ///Ignore other markdown files in this directory and
+ ///any sub-directory. Can only be meaningfully set to
+ ///true for 'index.md' pages.
skip_subdirs: ?bool,
- /// User-defined properties that you can then reference in templates.
+ ///User-defined properties that you can then reference
+ ///in templates.
custom: ?map[any],
}
struct Alternative {
- /// Path to a layout file inside of the configured layouts directory.
+ ///Path to a layout file inside of the configured
+ ///layouts directory.
layout: bytes,
- /// Output path, relative to the current directory.
- /// Use an absolute path to refer to the website's root directory.
+ ///Output path, relative to the current directory.
+ ///Use an absolute path to refer to the website's root
+ ///directory.
output: bytes,
- /// Useful when generating `` elements.
+ ///Useful when generating ``
+ ///elements.
title: ?bytes,
- /// Useful when generating `` elements.
+ ///Useful when generating ``
+ ///elements.
type: ?bytes,
}
diff --git a/src/context.zig b/src/context.zig
index 03b8306..12b568f 100644
--- a/src/context.zig
+++ b/src/context.zig
@@ -1,8 +1,10 @@
+const context = @This();
+
const std = @import("std");
const scripty = @import("scripty");
const superhtml = @import("superhtml");
const ziggy = @import("ziggy");
-const docgen = @import("context/docgen.zig");
+const doctypes = @import("context/doctypes.zig");
const Allocator = std.mem.Allocator;
const Ctx = superhtml.utils.Ctx;
@@ -53,8 +55,8 @@ pub var siteGet: *const fn (
pub var allSites: *const fn () []const Site = undefined;
-pub const ScriptyParam = docgen.ScriptyParam;
-pub const Signature = docgen.Signature;
+pub const ScriptyParam = doctypes.ScriptyParam;
+pub const Signature = doctypes.Signature;
pub const md = @import("context/markdown.zig");
@@ -64,27 +66,39 @@ pub const Page = @import("context/Page.zig");
pub const Build = @import("context/Build.zig");
pub const Asset = @import("context/Asset.zig");
pub const DateTime = @import("context/DateTime.zig");
+pub const String = @import("context/String.zig");
+pub const Bool = @import("context/Bool.zig");
+pub const Int = @import("context/Int.zig");
+pub const Float = @import("context/Float.zig");
+pub const Map = @import("context/Map.zig");
+// pub const Slice = @import("context/Slice.zig");
+pub const Optional = @import("context/Optional.zig");
+pub const Iterator = @import("context/Iterator.zig");
pub const Value = union(enum) {
template: *const Template,
site: *const Site,
page: *const Page,
ctx: Ctx(Value),
- alternative: *const Page.Alternative,
+ alternative: Page.Alternative,
build: *const Build,
asset: Asset,
- dynamic: ziggy.dynamic.Value,
- iterator: Iterator,
- iterator_element: IterElement,
- map_kv: MapKV,
- optional: ?Optional,
- string: []const u8,
+ map: Map,
+ // slice: Slice,
+ optional: ?*const context.Optional,
+ string: String,
date: DateTime,
- bool: bool,
- int: i64,
- float: f64,
+ bool: context.Bool,
+ int: Int,
+ float: Float,
+ iterator: *context.Iterator,
+ map_kv: Map.KV,
err: []const u8,
+ pub const Bool = context.Bool;
+ pub const Optional = context.Optional;
+ pub const Iterator = context.Iterator;
+
pub fn errFmt(gpa: Allocator, comptime fmt: []const u8, args: anytype) !Value {
const err_msg = try std.fmt.allocPrint(gpa, fmt, args);
return .{ .err = err_msg };
@@ -108,496 +122,113 @@ pub const Value = union(enum) {
w.print("\n", .{}) catch return error.ErrIO;
}
- pub const call = scripty.defaultCall(Value);
-
- pub const Optional = union(enum) {
- iter_elem: IterElement,
- page: *const Page,
- bool: bool,
- int: i64,
- string: []const u8,
- dynamic: ziggy.dynamic.Value,
- };
-
- pub const Iterator = struct {
- up_idx: u32 = undefined,
- up_tpl: *const anyopaque = undefined,
- impl: union(enum) {
- string_it: SliceIterator([]const u8),
- page_it: PageIterator,
- page_slice_it: SliceIterator(*const Page),
- translation_it: TranslationIterator,
- alt_it: SliceIterator(Page.Alternative),
- map_it: MapIterator,
- dynamic_it: SliceIterator(ziggy.dynamic.Value),
- },
-
- pub fn len(self: Iterator) usize {
- const l: usize = switch (self.impl) {
- inline else => |v| v.len(),
- };
-
- return l;
- }
- pub fn next(iter: *Iterator, gpa: Allocator) !?IterElement {
- switch (iter.impl) {
- inline else => |*v| {
- const n = try v.next(gpa) orelse return null;
- const l = iter.len();
-
- const elem_type = switch (@typeInfo(@TypeOf(n))) {
- .Pointer => |p| p.child,
- else => @TypeOf(n),
- };
- const by_ref = @typeInfo(elem_type) == .Struct and @hasDecl(elem_type, "PassByRef") and elem_type.PassByRef;
- const it = if (by_ref)
- IterElement.IterValue.from(n)
- else
- IterElement.IterValue.from(n.*);
- return .{
- .it = it,
- .idx = v.idx,
- .first = v.idx == 1,
- .last = v.idx == l,
- ._iter = iter,
- };
- },
- }
- }
-
- pub fn dot(self: Iterator, gpa: Allocator, path: []const u8) Value {
- _ = path;
- _ = gpa;
- _ = self;
- return .{ .err = "field access on an iterator value" };
- }
-
- pub const Builtins = struct {};
- };
-
- pub const IterElement = struct {
- it: IterValue,
- idx: usize,
- first: bool,
- last: bool,
- _iter: *const Iterator,
-
- const IterValue = union(enum) {
- string: []const u8,
- page: *const Page,
- alternative: *const Page.Alternative,
- map_kv: MapKV,
- dynamic: ziggy.dynamic.Value,
-
- pub fn from(v: anytype) IterValue {
- return switch (@TypeOf(v)) {
- []const u8 => .{ .string = v },
- *const Page => .{ .page = v },
- *const Page.Alternative => .{ .alternative = v },
- MapKV => .{ .map_kv = v },
- ziggy.dynamic.Value => switch (v) {
- .bytes => |b| .{ .string = b },
- else => .{ .dynamic = v },
- },
- else => @compileError("TODO: implement IterElement.IterValue.from for " ++ @typeName(@TypeOf(v))),
- };
- }
- };
-
- pub const dot = scripty.defaultDot(IterElement, Value, false);
- pub const Builtins = struct {
- pub const up = struct {
- pub const signature: Signature = .{ .ret = .dyn };
- pub const description =
- \\In nested loops, accesses the upper `$loop`
- \\
- ;
- pub const examples =
- \\$loop.up().it
- ;
- pub const call = superhtml.utils.loopUpFunction(
- Value,
- superhtml.VM(Template, Value).Template,
- );
- };
- pub const len = struct {
- pub const signature: Signature = .{ .ret = .int };
- pub const description =
- \\Returns the total number of elements in this loop.
- \\
- ;
- pub const examples =
- \\$loop.len()
- ;
-
- pub fn call(
- ite: IterElement,
- _: Allocator,
- args: []const Value,
- ) !Value {
- const bad_arg = .{ .err = "expected 0 arguments" };
- if (args.len != 0) return bad_arg;
- return .{ .int = @intCast(ite._iter.len()) };
- }
- };
- };
- };
-
pub fn fromStringLiteral(s: []const u8) Value {
- return .{ .string = s };
+ return .{ .string = .{ .value = s } };
}
pub fn fromNumberLiteral(bytes: []const u8) Value {
const num = std.fmt.parseInt(i64, bytes, 10) catch {
return .{ .err = "error parsing numeric literal" };
};
- return .{ .int = num };
+ return .{ .int = .{ .value = num } };
}
pub fn fromBooleanLiteral(b: bool) Value {
- return .{ .bool = b };
+ return .{ .bool = .{ .value = b } };
+ }
+
+ pub fn fromZiggy(gpa: Allocator, value: ziggy.dynamic.Value) !Value {
+ switch (value) {
+ .null => return .{ .optional = null },
+ .bool => |b| return .{ .bool = .{ .value = b } },
+ .integer => |i| return .{ .int = .{ .value = i } },
+ .bytes => |s| return .{ .string = .{ .value = s } },
+ .array => |a| return .{
+ .iterator = try context.Iterator.init(gpa, .{
+ .dynamic_it = .{ .items = a },
+ }),
+ },
+ .tag => |t| {
+ std.debug.assert(std.mem.eql(u8, t.name, "date"));
+ const date = DateTime.init(t.bytes) catch {
+ return .{ .err = "error parsing date" };
+ };
+ return Value.from(gpa, date);
+ },
+ .kv => |kv| return .{ .map = .{ .value = kv } },
+ inline else => |_, t| @panic("TODO: implement" ++ @tagName(t) ++ "support in dynamic data"),
+ }
}
- pub fn from(gpa: Allocator, v: anytype) Value {
- _ = gpa;
+ pub fn from(gpa: Allocator, v: anytype) !Value {
return switch (@TypeOf(v)) {
*Template => .{ .template = v },
*const Template => .{ .template = v },
*const Site => .{ .site = v },
- *const Page => .{ .page = v },
- *const Page.Alternative => .{ .alternative = v },
- []const Page.Alternative => .{
- .iterator = .{
- .impl = .{
- .alt_it = .{ .items = v },
- },
- },
- },
+ *const Page, *Page => .{ .page = v },
+ Page.Alternative => .{ .alternative = v },
*const Build => .{ .build = v },
Ctx(Value) => .{ .ctx = v },
Asset => .{ .asset = v },
- // IterElement => .{ .iteration_element = v },
DateTime => .{ .date = v },
- []const u8 => .{ .string = v },
- ?[]const u8 => .{
- .optional = .{
- .string = v orelse @panic("TODO: null optional reached Value.from"),
- },
- },
- bool => .{ .bool = v },
- i64, usize => .{ .int = @intCast(v) },
- ?Value => if (v) |o| o else .{ .err = "trying to access nil value" },
- *Value => v.*,
- IterElement.IterValue => switch (v) {
- .string => |s| .{ .string = s },
- .page => |p| .{ .page = p },
- .alternative => |p| .{ .alternative = p },
- .map_kv => |kv| .{ .map_kv = kv },
- .dynamic => |d| .{ .dynamic = d },
- },
- Optional => switch (v) {
- .iter_elem => |ie| .{ .iterator_element = ie },
- .page => |p| .{ .page = p },
- .bool => |b| .{ .bool = b },
- .string => |s| .{ .string = s },
- .int => |i| .{ .int = i },
- .dynamic => |d| .{ .dynamic = d },
+ []const u8, []u8 => .{ .string = .{ .value = v } },
+ bool => .{ .bool = .{ .value = v } },
+ i64, usize => .{ .int = .{ .value = @intCast(v) } },
+ ziggy.dynamic.Value => try fromZiggy(gpa, v),
+ Map.ZiggyMap => .{ .map = .{ .value = v } },
+ Map.KV => .{ .map_kv = v },
+ *const context.Optional => .{ .optional = v },
+ ?*const context.Optional => if (v) |opt| .{ .optional = opt } else context.Optional.Null,
+ ?[]const u8 => if (v) |opt|
+ try context.Optional.init(gpa, opt)
+ else
+ context.Optional.Null,
+ ?Value => if (v) |opt|
+ try context.Optional.init(gpa, opt)
+ else
+ context.Optional.Null,
+ Value => v,
+ ?*context.Iterator => if (v) |opt|
+ try context.Optional.init(gpa, opt)
+ else
+ context.Optional.Null,
+ *context.Iterator => .{ .iterator = v },
+ []const []const u8 => .{
+ .iterator = try context.Iterator.init(gpa, .{
+ .string_it = .{ .items = v },
+ }),
},
- ?Optional => .{ .optional = v orelse @panic("TODO: null optional reached Value.from") },
- ziggy.dynamic.Value => .{ .dynamic = v },
- MapKV => .{ .map_kv = v },
- []const []const u8 => .{
- .iterator = .{
- .impl = .{
- .string_it = .{ .items = v },
- },
- },
+ []const Page.Alternative => .{
+ .iterator = try context.Iterator.init(gpa, .{
+ .alt_it = .{ .items = v },
+ }),
},
else => @compileError("TODO: implement Value.from for " ++ @typeName(@TypeOf(v))),
};
}
+
+ pub const call = scripty.defaultCall(Value);
pub fn dot(
self: *Value,
gpa: Allocator,
path: []const u8,
) error{OutOfMemory}!Value {
switch (self.*) {
- .map_kv,
+ // .map_kv,
.string,
.bool,
.int,
.float,
.err,
.date,
+ .optional,
=> return .{ .err = "field access on primitive value" },
- .dynamic => return .{ .err = "field access on dynamic value" },
- .optional => return .{ .err = "field access on optional value" },
+ // .optional => return .{ .err = "field access on optional value" },
.asset => return .{ .err = "field access on asset value" },
// .iteration_element => return
- .iterator_element => |*v| return v.dot(gpa, path),
+ // .iterator_element => |*v| return v.dot(gpa, path),
inline else => |v| return v.dot(gpa, path),
}
}
-
- pub fn builtinsFor(comptime tag: @typeInfo(Value).Union.tag_type.?) type {
- return switch (tag) {
- .string => @import("context/primitive_builtins/String.zig"),
- .int => @import("context/primitive_builtins/Int.zig"),
- .bool => @import("context/primitive_builtins/Bool.zig"),
- .dynamic => @import("context/primitive_builtins/Dynamic.zig"),
- else => {
- const f = std.meta.fieldInfo(Value, tag);
- switch (@typeInfo(f.type)) {
- .Pointer => |ptr| {
- if (@typeInfo(ptr.child) == .Struct) {
- return @field(ptr.child, "Builtins");
- }
- },
- .Struct => {
- return @field(f.type, "Builtins");
- },
- else => {},
- }
-
- return struct {};
- },
- };
- }
-};
-
-pub fn SliceIterator(comptime Element: type) type {
- return struct {
- items: []const Element,
- idx: usize = 0,
-
- pub fn len(self: @This()) usize {
- return self.items.len;
- }
- pub fn index(self: @This()) usize {
- return self.items.idx;
- }
-
- pub fn next(self: *@This(), gpa: Allocator) !?*const Element {
- _ = gpa;
- if (self.idx == self.items.len) return null;
- const result: ?*Element = @constCast(&self.items[self.idx]);
- self.idx += 1;
- return result;
- }
- };
-}
-
-pub const PageIterator = struct {
- idx: usize = 0,
-
- _site: *const Site,
- _parent_section_path: ?[]const u8,
- _list: std.mem.TokenIterator(u8, .scalar),
- _len: usize,
-
- pub fn init(
- site: *const Site,
- parent_section_path: ?[]const u8,
- src: []const u8,
- ) PageIterator {
- return .{
- ._site = site,
- ._parent_section_path = parent_section_path,
- ._list = std.mem.tokenizeScalar(u8, src, '\n'),
- ._len = std.mem.count(u8, src, "\n"),
- };
- }
-
- pub fn len(it: PageIterator) usize {
- return it._len;
- }
- pub fn index(it: PageIterator) usize {
- return it.idx;
- }
-
- pub fn next(it: *PageIterator, gpa: Allocator) !?*const Page {
- _ = gpa;
-
- const next_page = it._list.next() orelse return null;
- defer it.idx += 1;
-
- const page = pageGet(
- it._site,
- next_page,
- it._parent_section_path,
- it.idx,
- false,
- ) catch |err| switch (err) {
- error.OutOfMemory => return error.OutOfMemory,
- error.PageLoad => @panic("TODO: report page load errors"),
- };
-
- return page;
-
- // const value = it._page_loader.call(gpa, .{
- // .index_in_section = it.idx,
- // .parent_section_path = it._parent_section_path,
- // .url_path_prefix = it._url_path_prefix,
- // .md_rel_path = next_page,
- // // TODO: give iterators the ability to error out
- // }) catch @panic("error while fetching next page");
-
- // return value.page;
- }
-};
-
-pub const TranslationIterator = struct {
- idx: usize = 0,
- _page: *const Page,
- _len: usize,
-
- pub fn init(
- page: *const Page,
- ) TranslationIterator {
- return .{
- ._page = page,
-
- ._len = if (page.translation_key == null)
- allSites().len
- else
- page._meta.key_variants.len,
- };
- }
-
- pub fn len(it: TranslationIterator) usize {
- return it._len;
- }
- pub fn index(it: TranslationIterator) usize {
- return it.idx;
- }
-
- pub fn next(it: *TranslationIterator, gpa: Allocator) !?*const Page {
- _ = gpa;
- if (it.idx >= it._len) return null;
-
- defer it.idx += 1;
-
- const t: Page.Translation = if (it._page.translation_key == null) .{
- .site = &allSites()[it.idx],
- .md_rel_path = it._page._meta.md_rel_path,
- } else it._page._meta.key_variants[it.idx];
-
- const page = pageGet(
- t.site,
- t.md_rel_path,
- null,
- null,
- false,
- ) catch |err| switch (err) {
- error.OutOfMemory => return error.OutOfMemory,
- error.PageLoad => @panic("trying to access a non-existent localized variant of a page is an error for now, sorry! give the same translation key to all variants of this page and you won't see this error anymore."),
- };
-
- return page;
- }
-};
-
-pub const MapKV = struct {
- _key: []const u8,
- _value: ziggy.dynamic.Value,
-
- // pub const dot = scripty.defaultDot(MapKV, Value);
- pub const PassByRef = true;
- pub const Builtins = struct {
- pub const key = struct {
- pub const signature: Signature = .{ .ret = .str };
- pub const description =
- \\Returns the key of a key-value pair.
- ;
- pub const examples =
- \\$loop.it.key()
- ;
- pub fn call(
- kv: MapKV,
- _: Allocator,
- args: []const Value,
- ) !Value {
- const bad_arg = .{ .err = "expected 0 arguments" };
- if (args.len != 0) return bad_arg;
- return .{ .string = kv._key };
- }
- };
- pub const value = struct {
- pub const signature: Signature = .{ .ret = .dyn };
- pub const description =
- \\Returns the value of a key-value pair.
- ;
- pub const examples =
- \\$loop.it.value()
- ;
- pub fn call(
- kv: MapKV,
- _: Allocator,
- args: []const Value,
- ) !Value {
- const bad_arg = .{ .err = "expected 0 arguments" };
- if (args.len != 0) return bad_arg;
- return switch (kv._value) {
- .kv => .{ .dynamic = kv._value },
- .bytes => |b| .{ .string = b },
- .tag => |t| .{ .string = t.bytes },
- .integer => |i| .{ .int = i },
- .float => |f| .{ .float = f },
- .bool => |b| .{ .bool = b },
- .array => |a| .{ .iterator = .{ .impl = .{ .dynamic_it = .{ .items = a } } } },
- .null => @panic("TODO: implement support for Ziggy null values in scripty"),
- };
- }
- };
- };
-};
-pub const MapIterator = struct {
- idx: usize = 0,
- _it: std.StringArrayHashMap(ziggy.dynamic.Value).Iterator,
- _len: usize,
- _filter: ?[]const u8 = null,
-
- pub fn init(
- it: std.StringArrayHashMap(ziggy.dynamic.Value).Iterator,
- filter: ?[]const u8,
- ) MapIterator {
- const f = filter orelse return .{ ._it = it, ._len = it.len };
- var filter_it = it;
- var count: usize = 0;
- while (filter_it.next()) |elem| {
- if (std.mem.indexOf(u8, elem.key_ptr.*, f) != null) count += 1;
- }
- return .{ ._it = it, ._len = count, ._filter = f };
- }
-
- pub fn len(it: MapIterator) usize {
- return it._len;
- }
- pub fn index(it: MapIterator) usize {
- return it.idx;
- }
-
- pub fn next(it: *MapIterator, _: Allocator) !?MapKV {
- if (it.idx >= it._len) return null;
-
- while (it._it.next()) |elem| {
- const f = it._filter orelse {
- it.idx += 1;
- return .{
- ._key = elem.key_ptr.*,
- ._value = elem.value_ptr.*,
- };
- };
- if (std.mem.indexOf(u8, elem.key_ptr.*, f) != null) {
- it.idx += 1;
- return .{
- ._key = elem.key_ptr.*,
- ._value = elem.value_ptr.*,
- };
- }
- }
-
- unreachable;
- }
};
diff --git a/src/context/Asset.zig b/src/context/Asset.zig
index 029cb6b..dc5f4b6 100644
--- a/src/context/Asset.zig
+++ b/src/context/Asset.zig
@@ -4,11 +4,12 @@ const std = @import("std");
const _ziggy = @import("ziggy");
const scripty = @import("scripty");
const utils = @import("utils.zig");
-const log = utils.log;
-const Signature = @import("docgen.zig").Signature;
const context = @import("../context.zig");
-const Value = context.Value;
+const log = utils.log;
+const Signature = @import("doctypes.zig").Signature;
const Allocator = std.mem.Allocator;
+const Value = context.Value;
+const Int = context.Int;
_meta: struct {
ref: []const u8,
@@ -30,9 +31,7 @@ pub const description = "Represents an asset.";
pub const dot = scripty.defaultDot(Asset, Value, false);
pub const Builtins = struct {
pub const link = struct {
- pub const signature: Signature = .{
- .ret = .str,
- };
+ pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Returns a link to the asset.
\\
@@ -72,18 +71,16 @@ pub const Builtins = struct {
asset._meta.kind,
);
- return .{ .string = url };
+ return Value.from(gpa, url);
}
};
pub const size = struct {
- pub const signature: Signature = .{
- .ret = .str,
- };
+ pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Returns the size of an asset file in bytes.
;
pub const examples =
- \\
+ \\
;
pub fn call(
self: Asset,
@@ -96,18 +93,16 @@ pub const Builtins = struct {
const stat = std.fs.cwd().statFile(self._meta.path) catch {
return .{ .err = "i/o error while reading asset file" };
};
- return .{ .int = @intCast(stat.size) };
+ return Int.init(@intCast(stat.size));
}
};
pub const bytes = struct {
- pub const signature: Signature = .{
- .ret = .str,
- };
+ pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Returns the raw contents of an asset.
;
pub const examples =
- \\
+ \\
;
pub fn call(
self: Asset,
@@ -119,19 +114,17 @@ pub const Builtins = struct {
const data = std.fs.cwd().readFileAlloc(gpa, self._meta.path, std.math.maxInt(u32)) catch {
return .{ .err = "i/o error while reading asset file" };
};
- return .{ .string = data };
+ return Value.from(gpa, data);
}
};
pub const ziggy = struct {
- pub const signature: Signature = .{
- .ret = .dyn,
- };
+ pub const signature: Signature = .{ .ret = .any };
pub const description =
\\Tries to parse the asset as a Ziggy document.
;
pub const examples =
- \\
+ \\
;
pub fn call(
self: Asset,
@@ -163,7 +156,7 @@ pub const Builtins = struct {
return .{ .err = buf.items };
};
- return .{ .dynamic = parsed };
+ return Value.fromZiggy(gpa, parsed);
}
};
};
diff --git a/src/context/Bool.zig b/src/context/Bool.zig
new file mode 100644
index 0000000..ab4c80b
--- /dev/null
+++ b/src/context/Bool.zig
@@ -0,0 +1,134 @@
+const Bool = @This();
+
+const std = @import("std");
+const utils = @import("utils.zig");
+const context = @import("../context.zig");
+const Signature = @import("doctypes.zig").Signature;
+const Allocator = std.mem.Allocator;
+const Value = context.Value;
+const String = context.String;
+
+value: bool,
+
+pub fn init(b: bool) Value {
+ return .{ .bool = .{ .value = b } };
+}
+
+fn not(b: Bool) Value {
+ return .{ .bool = .{ .value = !b.value } };
+}
+
+pub const True = Bool.init(true);
+pub const False = Bool.init(false);
+
+pub const PassByRef = false;
+pub const description = "A boolean value";
+pub const Builtins = struct {
+ pub const then = struct {
+ pub const signature: Signature = .{
+ .params = &.{ .String, .{ .Opt = .String } },
+ .ret = .String,
+ };
+ pub const description =
+ \\If the boolean is `true`, returns the first argument.
+ \\Otherwise, returns the second argument.
+ \\
+ \\Omitting the second argument defaults to an empty string.
+ \\
+ ;
+ pub const examples =
+ \\$page.draft.then("DRAFT!")
+ ;
+ pub fn call(
+ b: Bool,
+ _: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len < 1 or args.len > 2) return .{
+ .err = "expected 1 or 2 string arguments",
+ };
+
+ if (b.value) {
+ return args[0];
+ } else {
+ if (args.len < 2) return String.init("");
+ return args[1];
+ }
+ }
+ };
+ pub const not = struct {
+ pub const signature: Signature = .{ .ret = .Bool };
+ pub const description =
+ \\Negates a boolean value.
+ \\
+ ;
+ pub const examples =
+ \\$page.draft.not()
+ ;
+ pub fn call(
+ b: Bool,
+ _: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len != 0) return .{ .err = "expected 0 arguments" };
+ return b.not();
+ }
+ };
+ pub const @"and" = struct {
+ pub const signature: Signature = .{
+ .params = &.{ .Bool, .{ .Many = .Bool } },
+ .ret = .Bool,
+ };
+
+ pub const description =
+ \\Computes logical `and` between the receiver value and any other
+ \\value passed as argument.
+ ;
+ pub const examples =
+ \\$page.draft.and($site.tags.len().eq(10))
+ ;
+ pub fn call(
+ b: Bool,
+ _: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len == 0) return .{ .err = "expected 1 or more boolean argument(s)" };
+ for (args) |a| switch (a) {
+ .bool => {},
+ else => return .{ .err = "wrong argument type" },
+ };
+ if (!b.value) return False;
+ for (args) |a| if (!a.bool.value) return False;
+
+ return True;
+ }
+ };
+ pub const @"or" = struct {
+ pub const signature: Signature = .{
+ .params = &.{ .Bool, .{ .Many = .Bool } },
+ .ret = .Bool,
+ };
+ pub const description =
+ \\Computes logical `or` between the receiver value and any other value passed as argument.
+ \\
+ ;
+ pub const examples =
+ \\$page.draft.or($site.tags.len().eq(0))
+ ;
+ pub fn call(
+ b: Bool,
+ _: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len == 0) return .{ .err = "'or' wants at least one argument" };
+ for (args) |a| switch (a) {
+ .bool => {},
+ else => return .{ .err = "wrong argument type" },
+ };
+ if (b.value) return True;
+ for (args) |a| if (a.bool.value) return True;
+
+ return False;
+ }
+ };
+};
diff --git a/src/context/Build.zig b/src/context/Build.zig
index beab43c..718f779 100644
--- a/src/context/Build.zig
+++ b/src/context/Build.zig
@@ -6,26 +6,28 @@ const scripty = @import("scripty");
const utils = @import("utils.zig");
const context = @import("../context.zig");
const Value = context.Value;
-const Signature = @import("docgen.zig").Signature;
+const Signature = @import("doctypes.zig").Signature;
const uninitialized = utils.uninitialized;
+pub const dot = scripty.defaultDot(Build, Value, false);
+pub const PassByRef = true;
+
pub const description =
\\Gives you access to build-time assets and other build related info.
\\When inside of a git repository it also gives git-related metadata.
;
-pub const dot = scripty.defaultDot(Build, Value, false);
-pub const PassByRef = true;
+pub const Fields = struct {};
pub const Builtins = struct {
pub const asset = struct {
pub const signature: Signature = .{
- .params = &.{.str},
+ .params = &.{.String},
.ret = .Asset,
};
pub const description =
\\Retuns a build-time asset (i.e. an asset generated through your 'build.zig' file) by name.
;
pub const examples =
- \\
+ \\
;
pub fn call(
_: *const Build,
@@ -38,7 +40,7 @@ pub const Builtins = struct {
if (args.len != 1) return bad_arg;
const ref = switch (args[0]) {
- .string => |s| s,
+ .string => |s| s.value,
else => return bad_arg,
};
diff --git a/src/context/DateTime.zig b/src/context/DateTime.zig
index 9bedb03..8cdf840 100644
--- a/src/context/DateTime.zig
+++ b/src/context/DateTime.zig
@@ -5,8 +5,11 @@ const Allocator = std.mem.Allocator;
const zeit = @import("zeit");
const ziggy = @import("ziggy");
const utils = @import("utils.zig");
-const Signature = @import("docgen.zig").Signature;
-const Value = @import("../context.zig").Value;
+const context = @import("../context.zig");
+const Signature = @import("doctypes.zig").Signature;
+const Value = context.Value;
+const String = context.String;
+const Bool = context.Bool;
_dt: zeit.Time,
// Use inst() to access this field
@@ -22,9 +25,15 @@ pub fn init(iso8601: []const u8) !DateTime {
};
}
+pub const description =
+ \\A datetime.
+;
pub const Builtins = struct {
pub const gt = struct {
- pub const signature: Signature = .{ .params = &.{.date}, .ret = .bool };
+ pub const signature: Signature = .{
+ .params = &.{.Date},
+ .ret = .Bool,
+ };
pub const description =
\\Return true if lhs is later than rhs (the argument).
\\
@@ -46,11 +55,14 @@ pub const Builtins = struct {
else => return argument_error,
};
- return .{ .bool = dt._inst.timestamp > rhs._inst.timestamp };
+ return Bool.init(dt._inst.timestamp > rhs._inst.timestamp);
}
};
pub const lt = struct {
- pub const signature: Signature = .{ .params = &.{.date}, .ret = .bool };
+ pub const signature: Signature = .{
+ .params = &.{.Date},
+ .ret = .Bool,
+ };
pub const description =
\\Return true if lhs is earlier than rhs (the argument).
\\
@@ -72,11 +84,14 @@ pub const Builtins = struct {
else => return argument_error,
};
- return .{ .bool = dt._inst.timestamp < rhs._inst.timestamp };
+ return Bool.init(dt._inst.timestamp < rhs._inst.timestamp);
}
};
pub const eq = struct {
- pub const signature: Signature = .{ .params = &.{.date}, .ret = .bool };
+ pub const signature: Signature = .{
+ .params = &.{.Date},
+ .ret = .Bool,
+ };
pub const description =
\\Return true if lhs is the same instant as the rhs (the argument).
\\
@@ -98,11 +113,14 @@ pub const Builtins = struct {
else => return argument_error,
};
- return .{ .bool = dt._inst.timestamp == rhs._inst.timestamp };
+ return Bool.init(dt._inst.timestamp == rhs._inst.timestamp);
}
};
pub const format = struct {
- pub const signature: Signature = .{ .params = &.{.str}, .ret = .str };
+ pub const signature: Signature = .{
+ .params = &.{.String},
+ .ret = .String,
+ };
pub const description =
\\Formats a datetime according to the specified format string.
\\
@@ -120,12 +138,14 @@ pub const Builtins = struct {
const argument_error = .{ .err = "'format' wants one (string) argument" };
if (args.len != 1) return argument_error;
const string = switch (args[0]) {
- .string => |s| s,
+ .string => |s| s.value,
else => return argument_error,
};
inline for (@typeInfo(DateFormats).Struct.decls) |decl| {
if (std.mem.eql(u8, decl.name, string)) {
- return .{ .string = try @call(.auto, @field(DateFormats, decl.name), .{ dt, gpa }) };
+ return String.init(
+ try @call(.auto, @field(DateFormats, decl.name), .{ dt, gpa }),
+ );
}
} else {
return .{ .err = "unsupported date format" };
@@ -134,7 +154,7 @@ pub const Builtins = struct {
};
pub const formatHTTP = struct {
- pub const signature: Signature = .{ .ret = .str };
+ pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Formats a datetime according to the HTTP spec.
\\
@@ -165,7 +185,7 @@ pub const Builtins = struct {
},
);
- return .{ .string = formatted_date };
+ return String.init(formatted_date);
}
};
};
diff --git a/src/context/Float.zig b/src/context/Float.zig
new file mode 100644
index 0000000..d5ca026
--- /dev/null
+++ b/src/context/Float.zig
@@ -0,0 +1,7 @@
+const Float = @This();
+
+f: f64,
+
+pub const PassByRef = false;
+pub const description = "A 64bit float value.";
+pub const Builtins = struct {};
diff --git a/src/context/Int.zig b/src/context/Int.zig
new file mode 100644
index 0000000..fca11e5
--- /dev/null
+++ b/src/context/Int.zig
@@ -0,0 +1,163 @@
+const Int = @This();
+
+const std = @import("std");
+const utils = @import("utils.zig");
+const context = @import("../context.zig");
+const Signature = @import("doctypes.zig").Signature;
+const Allocator = std.mem.Allocator;
+const Value = context.Value;
+const Bool = context.Bool;
+const String = context.String;
+
+value: i64,
+
+pub fn init(i: i64) Value {
+ return .{ .int = .{ .value = i } };
+}
+
+pub const PassByRef = false;
+pub const description = "A signed 64-bit integer.";
+pub const Builtins = struct {
+ pub const eq = struct {
+ pub const signature: Signature = .{
+ .params = &.{.Int},
+ .ret = .Bool,
+ };
+ pub const description =
+ \\Tests if two integers have the same value.
+ \\
+ ;
+ pub const examples =
+ \\$page.wordCount().eq(200)
+ ;
+ pub fn call(
+ int: Int,
+ _: Allocator,
+ args: []const Value,
+ ) !Value {
+ const argument_error = .{ .err = "'plus' wants one int argument" };
+ if (args.len != 1) return argument_error;
+
+ switch (args[0]) {
+ .int => |rhs| return Bool.init(int.value == rhs.value),
+ else => return argument_error,
+ }
+ }
+ };
+ pub const gt = struct {
+ pub const signature: Signature = .{
+ .params = &.{.Int},
+ .ret = .Bool,
+ };
+ pub const description =
+ \\Returns true if lhs is greater than rhs (the argument).
+ \\
+ ;
+ pub const examples =
+ \\$page.wordCount().gt(200)
+ ;
+ pub fn call(
+ int: Int,
+ _: Allocator,
+ args: []const Value,
+ ) !Value {
+ const argument_error = .{ .err = "'gt' wants one int argument" };
+ if (args.len != 1) return argument_error;
+
+ switch (args[0]) {
+ .int => |rhs| return Bool.init(int.value > rhs.value),
+ else => return argument_error,
+ }
+ }
+ };
+
+ pub const plus = struct {
+ pub const signature: Signature = .{
+ .params = &.{.Int},
+ .ret = .Int,
+ };
+ pub const description =
+ \\Sums two integers.
+ \\
+ ;
+ pub const examples =
+ \\$page.wordCount().plus(10)
+ ;
+ pub fn call(
+ int: Int,
+ _: Allocator,
+ args: []const Value,
+ ) !Value {
+ const argument_error = .{ .err = "expected 1 int argument" };
+ if (args.len != 1) return argument_error;
+
+ switch (args[0]) {
+ .int => |add| return Int.init(int.value +| add.value),
+ .float => @panic("TODO: int with float argument"),
+ else => return argument_error,
+ }
+ }
+ };
+ pub const div = struct {
+ pub const signature: Signature = .{
+ .params = &.{.Int},
+ .ret = .Int,
+ };
+ pub const description =
+ \\Divides the receiver by the argument.
+ \\
+ ;
+ pub const examples =
+ \\$page.wordCount().div(10)
+ ;
+ pub fn call(
+ int: Int,
+ _: Allocator,
+ args: []const Value,
+ ) !Value {
+ const argument_error = .{ .err = "'div' wants one (int|float) argument" };
+ if (args.len != 1) return argument_error;
+
+ switch (args[0]) {
+ .int => |den| {
+ const res = std.math.divTrunc(i64, int.value, den.value) catch |err| {
+ return .{ .err = @errorName(err) };
+ };
+
+ return Int.init(res);
+ },
+ .float => @panic("TODO: div with float argument"),
+ else => return argument_error,
+ }
+ }
+ };
+
+ pub const byteSize = struct {
+ pub const signature: Signature = .{ .ret = .String };
+ pub const description =
+ \\Turns a raw number of bytes into a human readable string that
+ \\appropriately uses Kilo, Mega, Giga, etc.
+ \\
+ ;
+ pub const examples =
+ \\$page.asset('photo.jpg').size().byteSize()
+ ;
+ pub fn call(
+ int: Int,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len != 0) return .{ .err = "expected 0 arguments" };
+
+ const size: usize = if (int.value > 0) @intCast(int.value) else return Value.errFmt(
+ gpa,
+ "cannot represent {} (a negative value) as a size",
+ .{int.value},
+ );
+
+ return String.init(try std.fmt.allocPrint(gpa, "{:.0}", .{
+ std.fmt.fmtIntSizeBin(size),
+ }));
+ }
+ };
+};
diff --git a/src/context/Iterator.zig b/src/context/Iterator.zig
new file mode 100644
index 0000000..4e35973
--- /dev/null
+++ b/src/context/Iterator.zig
@@ -0,0 +1,304 @@
+const Iterator = @This();
+
+const std = @import("std");
+const ziggy = @import("ziggy");
+const superhtml = @import("superhtml");
+const scripty = @import("scripty");
+const context = @import("../context.zig");
+const doctypes = @import("doctypes.zig");
+const Signature = doctypes.Signature;
+const Allocator = std.mem.Allocator;
+const Value = context.Value;
+const Template = context.Template;
+const Site = context.Site;
+const Page = context.Page;
+const Map = context.Map;
+
+it: Value = undefined,
+idx: usize = 0,
+first: bool = undefined,
+last: bool = undefined,
+len: usize,
+
+_superhtml_context: superhtml.utils.IteratorContext(Value, Template) = .{},
+_impl: Impl,
+
+pub const Impl = union(enum) {
+ string_it: SliceIterator([]const u8),
+ page_it: PageIterator,
+ page_slice_it: SliceIterator(*const Page),
+ translation_it: TranslationIterator,
+ alt_it: SliceIterator(Page.Alternative),
+ map_it: MapIterator,
+ dynamic_it: SliceIterator(ziggy.dynamic.Value),
+
+ pub fn len(impl: Impl) usize {
+ switch (impl) {
+ inline else => |v| return v.len(),
+ }
+ }
+};
+
+pub fn init(gpa: Allocator, impl: Impl) !*Iterator {
+ const res = try gpa.create(Iterator);
+ res.* = .{ ._impl = impl, .len = impl.len() };
+ return res;
+}
+
+pub fn deinit(iter: *const Iterator, gpa: Allocator) void {
+ gpa.destroy(iter);
+}
+
+pub fn next(iter: *Iterator, gpa: Allocator) !bool {
+ switch (iter._impl) {
+ inline else => |*v| {
+ const item = try v.next(gpa);
+ iter.it = try Value.from(gpa, item orelse return false);
+ iter.idx += 1;
+ iter.first = iter.idx == 1;
+ iter.last = iter.idx == iter.len - 1;
+ return true;
+ },
+ }
+}
+
+pub const dot = scripty.defaultDot(Iterator, Value, false);
+pub const description = "An iterator.";
+pub const Fields = struct {
+ pub const it =
+ \\The current iteration variable.
+ ;
+ pub const idx =
+ \\The current iteration index.
+ ;
+ pub const len =
+ \\The length of the sequence being iterated.
+ ;
+ pub const first =
+ \\True on the first iteration loop.
+ ;
+ pub const last =
+ \\True on the last iteration loop.
+ ;
+};
+pub const Builtins = struct {
+ pub const up = struct {
+ pub const signature: Signature = .{ .ret = .Iterator };
+ pub const description =
+ \\In nested loops, accesses the upper `$loop`
+ \\
+ ;
+ pub const examples =
+ \\$loop.up().it
+ ;
+ pub fn call(
+ it: *Iterator,
+ _: Allocator,
+ args: []const Value,
+ ) !Value {
+ const bad_arg = .{ .err = "expected 0 arguments" };
+ if (args.len != 0) return bad_arg;
+ return it._superhtml_context.up();
+ }
+ };
+ // pub const len = struct {
+ // pub const signature: Signature = .{ .ret = .int };
+ // pub const description =
+ // \\Returns the total number of elements in this loop.
+ // ;
+ // pub const examples =
+ // \\$loop.len()
+ // ;
+
+ // pub fn call(
+ // it: Iterator,
+ // _: Allocator,
+ // args: []const Value,
+ // ) !Value {
+ // const bad_arg = .{ .err = "expected 0 arguments" };
+ // if (args.len != 0) return bad_arg;
+ // const l = it._len orelse return .{
+ // .err = "this iterator doesn't know its total length",
+ // };
+ // return Value.from(l);
+ // }
+ // };
+ // pub const @"len?" = struct {
+ // pub const signature: Signature = .{ .ret = .{ .opt = .int } };
+ // pub const description =
+ // \\Returns the total number of elements in this loop.
+ // \\
+ // ;
+ // pub const examples =
+ // \\$loop.len?()
+ // ;
+
+ // pub fn call(
+ // it: Iterator,
+ // _: Allocator,
+ // args: []const Value,
+ // ) !Value {
+ // const bad_arg = .{ .err = "expected 0 arguments" };
+ // if (args.len != 0) return bad_arg;
+ // return Value.from(it._len);
+ // }
+ // };
+};
+
+fn SliceIterator(comptime Element: type) type {
+ return struct {
+ idx: usize = 0,
+ items: []const Element,
+
+ pub fn len(self: @This()) usize {
+ return self.items.len;
+ }
+
+ pub fn next(self: *@This(), gpa: Allocator) !?Element {
+ _ = gpa;
+ if (self.idx == self.items.len) return null;
+ self.idx += 1;
+ return self.items[self.idx];
+ }
+ };
+}
+
+pub const PageIterator = struct {
+ idx: usize = 0,
+
+ site: *const Site,
+ parent_section_path: ?[]const u8,
+ list: std.mem.TokenIterator(u8, .scalar),
+ _len: usize,
+
+ pub fn init(
+ site: *const Site,
+ parent_section_path: ?[]const u8,
+ src: []const u8,
+ ) PageIterator {
+ return .{
+ .site = site,
+ .parent_section_path = parent_section_path,
+ .list = std.mem.tokenizeScalar(u8, src, '\n'),
+ ._len = std.mem.count(u8, src, "\n"),
+ };
+ }
+
+ pub fn len(it: PageIterator) usize {
+ return it._len;
+ }
+
+ pub fn next(it: *PageIterator, gpa: Allocator) !?*const Page {
+ _ = gpa;
+
+ const next_page = it.list.next() orelse return null;
+ defer it.idx += 1;
+
+ const page = context.pageGet(
+ it.site,
+ next_page,
+ it.parent_section_path,
+ it.idx,
+ false,
+ ) catch |err| switch (err) {
+ error.OutOfMemory => return error.OutOfMemory,
+ error.PageLoad => @panic("TODO: report page load errors"),
+ };
+
+ return page;
+ }
+};
+
+pub const TranslationIterator = struct {
+ idx: usize = 0,
+ page: *const Page,
+ _len: usize,
+
+ pub fn init(
+ page: *const Page,
+ ) TranslationIterator {
+ return .{
+ .page = page,
+ ._len = if (page.translation_key == null)
+ context.allSites().len
+ else
+ page._meta.key_variants.len,
+ };
+ }
+
+ pub fn len(it: TranslationIterator) usize {
+ return it._len;
+ }
+
+ pub fn next(it: *TranslationIterator, gpa: Allocator) !?*const Page {
+ _ = gpa;
+ if (it.idx >= it._len) return null;
+
+ defer it.idx += 1;
+
+ const t: Page.Translation = if (it.page.translation_key == null) .{
+ .site = &context.allSites()[it.idx],
+ .md_rel_path = it.page._meta.md_rel_path,
+ } else it.page._meta.key_variants[it.idx];
+
+ const page = context.pageGet(
+ t.site,
+ t.md_rel_path,
+ null,
+ null,
+ false,
+ ) catch |err| switch (err) {
+ error.OutOfMemory => return error.OutOfMemory,
+ error.PageLoad => @panic("trying to access a non-existent localized variant of a page is an error for now, sorry! give the same translation key to all variants of this page and you won't see this error anymore."),
+ };
+
+ return page;
+ }
+};
+
+pub const MapIterator = struct {
+ idx: usize = 0,
+ it: std.StringArrayHashMap(ziggy.dynamic.Value).Iterator,
+ _len: usize,
+ filter: ?[]const u8 = null,
+
+ pub fn init(
+ it: std.StringArrayHashMap(ziggy.dynamic.Value).Iterator,
+ filter: ?[]const u8,
+ ) MapIterator {
+ const f = filter orelse return .{ .it = it, ._len = it.len };
+ var filter_it = it;
+ var count: usize = 0;
+ while (filter_it.next()) |elem| {
+ if (std.mem.indexOf(u8, elem.key_ptr.*, f) != null) count += 1;
+ }
+ return .{ .it = it, ._len = count, .filter = f };
+ }
+
+ pub fn len(it: MapIterator) usize {
+ return it._len;
+ }
+
+ pub fn next(it: *MapIterator, _: Allocator) !?Map.KV {
+ if (it.idx >= it._len) return null;
+
+ while (it.it.next()) |elem| {
+ const f = it.filter orelse {
+ it.idx += 1;
+ return .{
+ .key = elem.key_ptr.*,
+ .value = elem.value_ptr.*,
+ };
+ };
+ if (std.mem.indexOf(u8, elem.key_ptr.*, f) != null) {
+ it.idx += 1;
+ return .{
+ .key = elem.key_ptr.*,
+ .value = elem.value_ptr.*,
+ };
+ }
+ }
+
+ unreachable;
+ }
+};
diff --git a/src/context/Map.zig b/src/context/Map.zig
new file mode 100644
index 0000000..e102597
--- /dev/null
+++ b/src/context/Map.zig
@@ -0,0 +1,214 @@
+const Map = @This();
+
+const std = @import("std");
+const ziggy = @import("ziggy");
+const scripty = @import("scripty");
+const context = @import("../context.zig");
+const DateTime = @import("DateTime.zig");
+const Signature = @import("doctypes.zig").Signature;
+const Allocator = std.mem.Allocator;
+const Value = context.Value;
+const Optional = context.Optional;
+const Bool = context.Bool;
+
+value: ZiggyMap,
+
+pub const ZiggyMap = ziggy.dynamic.Map(ziggy.dynamic.Value);
+
+pub fn dot(map: Map, gpa: Allocator, path: []const u8) Value {
+ _ = map;
+ _ = gpa;
+ _ = path;
+ return .{ .err = "Map has no fields" };
+}
+pub const description =
+ \\A map that can hold any value, used to represent the `custom` field
+ \\in Page frontmatters or Ziggy / JSON data loaded from assets.
+;
+pub const Builtins = struct {
+ pub const getOr = struct {
+ pub const signature: Signature = .{
+ .params = &.{ .String, .String },
+ .ret = .String,
+ };
+ pub const description =
+ \\Tries to get a value from a map, returns the second value on failure.
+ \\
+ ;
+ pub const examples =
+ \\$page.custom.getOr('coauthor', 'Loris Cro')
+ ;
+ pub fn call(
+ map: Map,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ const bad_arg = .{ .err = "expected 2 string arguments" };
+ if (args.len != 2) return bad_arg;
+
+ const path = switch (args[0]) {
+ .string => |s| s.value,
+ else => return bad_arg,
+ };
+
+ const default = args[1];
+
+ if (map.value.fields.get(path)) |value| {
+ if (value == .null) return default;
+ return Value.fromZiggy(gpa, value);
+ }
+
+ return default;
+ }
+ };
+
+ pub const get = struct {
+ pub const signature: Signature = .{
+ .params = &.{.String},
+ .ret = .any,
+ };
+ pub const description =
+ \\Tries to get a value from a map, errors out if the value is not present.
+ \\
+ ;
+ pub const examples =
+ \\$page.custom.get('coauthor')
+ ;
+ pub fn call(
+ map: Map,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ const bad_arg = .{ .err = "expected 1 string argument" };
+ if (args.len != 1) return bad_arg;
+
+ const path = switch (args[0]) {
+ .string => |s| s.value,
+ else => return bad_arg,
+ };
+
+ const missing = try Value.errFmt(gpa, "missing value '{s}'", .{path});
+
+ if (map.value.fields.get(path)) |value| {
+ if (value == .null) return missing;
+ return Value.fromZiggy(gpa, value);
+ }
+
+ return missing;
+ }
+ };
+
+ pub const @"get?" = struct {
+ pub const signature: Signature = .{
+ .params = &.{.String},
+ .ret = .{ .Opt = .any },
+ };
+ pub const description =
+ \\Tries to get a dynamic value, to be used in conjuction with an `if` attribute.
+ \\
+ ;
+ pub const examples =
+ \\
+ \\
+ \\
+ ;
+ pub fn call(
+ map: Map,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ const bad_arg = .{ .err = "'get?' wants 1 string argument" };
+ if (args.len != 1) return bad_arg;
+
+ const path = switch (args[0]) {
+ .string => |s| s.value,
+ else => return bad_arg,
+ };
+
+ if (map.value.fields.get(path)) |value| {
+ return Value.fromZiggy(gpa, value);
+ }
+
+ return Optional.Null;
+ }
+ };
+ pub const has = struct {
+ pub const signature: Signature = .{
+ .params = &.{.String},
+ .ret = .Bool,
+ };
+ pub const description =
+ \\Returns true if the map contains the provided key.
+ \\
+ ;
+ pub const examples =
+ \\Yep!
+ ;
+ pub fn call(
+ map: Map,
+ gpa: Allocator,
+ args: []const Value,
+ ) Value {
+ _ = gpa;
+ const bad_arg = .{ .err = "'get?' wants 1 string argument" };
+ if (args.len != 1) return bad_arg;
+
+ const path = switch (args[0]) {
+ .string => |s| s.value,
+ else => return bad_arg,
+ };
+
+ return Bool.init(map.value.fields.get(path) != null);
+ }
+ };
+
+ pub const iterate = struct {
+ pub const signature: Signature = .{
+ .params = &.{.{ .Opt = .String }},
+ .ret = .{ .Many = .KV },
+ };
+ pub const description =
+ \\Iterates over key-value pairs of a Ziggy map.
+ \\
+ \\You can optionally pass a string that will be used to filter key names.
+ ;
+ pub const examples =
+ \\$page.custom.iterate()
+ ;
+ pub fn call(
+ map: Map,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ const bad_arg = .{ .err = "expected 0 or 1 string argument" };
+ if (args.len > 1) return bad_arg;
+
+ const filter: ?[]const u8 = if (args.len == 0) null else switch (args[0]) {
+ .string => |s| s.value,
+ else => return bad_arg,
+ };
+
+ return .{
+ .iterator = try context.Iterator.init(gpa, .{
+ .map_it = context.Iterator.MapIterator.init(
+ map.value.fields.iterator(),
+ filter,
+ ),
+ }),
+ };
+ }
+ };
+};
+
+pub const KV = struct {
+ key: []const u8,
+ value: ziggy.dynamic.Value,
+
+ pub const dot = scripty.defaultDot(KV, Value, false);
+ pub const description = "A key-value pair.";
+ pub const Fields = struct {
+ pub const key = "The key string.";
+ pub const value = "The corresponding value.";
+ };
+ pub const Builtins = struct {};
+};
diff --git a/src/context/Optional.zig b/src/context/Optional.zig
new file mode 100644
index 0000000..d2cd0be
--- /dev/null
+++ b/src/context/Optional.zig
@@ -0,0 +1,25 @@
+const Optional = @This();
+
+const std = @import("std");
+const context = @import("../context.zig");
+const Allocator = std.mem.Allocator;
+const Value = context.Value;
+
+value: Value,
+
+pub const Null: Value = .{ .optional = null };
+pub fn init(gpa: Allocator, v: anytype) !Value {
+ const box = try gpa.create(Optional);
+ box.value = try Value.from(gpa, v);
+ return .{ .optional = box };
+}
+
+// pub fn dot(opt: Optional, gpa: Allocator, path: []const u8) !Value {
+// _ = opt;
+// _ = gpa;
+// _ = path;
+// return .{ .err = "todo" };
+// }
+pub const PassByRef = false;
+pub const description = "An optional value, to be used in conjunction with `if` attributes.";
+pub const Builtins = struct {};
diff --git a/src/context/Page.zig b/src/context/Page.zig
index 05bf10e..251eae9 100644
--- a/src/context/Page.zig
+++ b/src/context/Page.zig
@@ -6,11 +6,14 @@ const scripty = @import("scripty");
const supermd = @import("supermd");
const utils = @import("utils.zig");
const render = @import("../render.zig");
-const Signature = @import("docgen.zig").Signature;
+const Signature = @import("doctypes.zig").Signature;
const DateTime = @import("DateTime.zig");
const context = @import("../context.zig");
-const Value = context.Value;
const Allocator = std.mem.Allocator;
+const Value = context.Value;
+const Optional = context.Optional;
+const Bool = context.Bool;
+const String = context.String;
var asset_undef: context.AssetExtern = .{};
var page_undef: context.PageExtern = .{};
@@ -79,31 +82,125 @@ pub const Translation = struct {
};
pub const Alternative = struct {
+ name: []const u8 = "",
layout: []const u8,
output: []const u8,
- title: []const u8 = "",
type: []const u8 = "",
pub const dot = scripty.defaultDot(Alternative, Value, false);
- pub const PassByRef = true;
+ // pub const PassByRef = true;
+
pub const Builtins = struct {};
pub const description =
\\An alternative version of the current page. Title and type
\\can be used when generating `` elements.
;
+ pub const Fields = struct {
+ pub const layout =
+ \\The SuperHTML layout to use to generate this alternative version of the page.
+ ;
+ pub const output =
+ \\Output path where to to put the generated alternative.
+ ;
+ pub const name =
+ \\A name that can be used to fetch this alternative version
+ \\of the page.
+ ;
+ pub const @"type" =
+ \\A metadata field that can be used to set the content-type of this alternative version of the Page.
+ \\
+ \\Useful for example to generate RSS links:
+ \\
+ \\```superhtml
+ \\
+ \\
+ \\```
+ ;
+ };
};
-pub const description =
- \\The current page.
-;
pub const dot = scripty.defaultDot(Page, Value, false);
pub const PassByRef = true;
+
+pub const description =
+ \\The page currently being rendered.
+;
+pub const Fields = struct {
+ pub const title =
+ \\Title of the page,
+ \\as set in the SuperMD frontmatter.
+ ;
+ pub const description =
+ \\Description of the page,
+ \\as set in the SuperMD frontmatter.
+ ;
+ pub const author =
+ \\Author of the page,
+ \\as set in the SuperMD frontmatter.
+ ;
+ pub const date =
+ \\Publication date of the page,
+ \\as set in the SuperMD frontmatter.
+ \\
+ \\Used to provide default ordering of pages.
+ ;
+ pub const layout =
+ \\SuperHTML layout used to render the page,
+ \\as set in the SuperMD frontmatter.
+ ;
+ pub const draft =
+ \\When set to true the page will not be rendered in release mode,
+ \\as set in the SuperMD frontmatter.
+ ;
+ pub const tags =
+ \\Tags associated with the page,
+ \\as set in the SuperMD frontmatter.
+ ;
+ pub const aliases =
+ \\Aliases of the current page,
+ \\as set in the SuperMD frontmatter.
+ \\
+ \\Aliases can be used to make the same page available
+ \\from different locations.
+ \\
+ \\Every entry in the list is an output location where the
+ \\rendered page will be copied to.
+ ;
+ pub const alternatives =
+ \\Alternative versions of the page,
+ \\as set in the SuperMD frontmatter.
+ \\
+ \\Alternatives are a good way of implementing RSS feeds, for example.
+ ;
+ pub const skip_subdirs =
+ \\Skips any other potential content present in the subdir of the page,
+ \\as set in the SuperMD frontmatter.
+ \\
+ \\Can only be set to true on section pages (i.e. `index.md` pages).
+ ;
+ pub const translation_key =
+ \\Translation key used to map this page with corresponding localized variants,
+ \\as set in the SuperMD frontmatter.
+ \\
+ \\See the docs on i18n for more info.
+ ;
+ pub const custom =
+ \\A Ziggy map where you can define custom properties for the page,
+ \\as set in the SuperMD frontmatter.
+ ;
+};
pub const Builtins = struct {
pub const isCurrent = struct {
- pub const signature: Signature = .{ .ret = .bool };
+ pub const signature: Signature = .{ .ret = .Bool };
pub const description =
- \\Returns true if the page is the current page. To be used in
- \\conjunction with the various functions that give you references
- \\to other pages, like `$site.page()`, for example.
+ \\Returns true if the target page is the one currently being
+ \\rendered.
+ \\
+ \\To be used in conjunction with the various functions that give
+ \\you references to other pages, like `$site.page()`, for example.
;
pub const examples =
\\
@@ -115,13 +212,13 @@ pub const Builtins = struct {
) !Value {
_ = gpa;
if (args.len != 0) return .{ .err = "expected 0 arguments" };
- return .{ .bool = p._meta.is_root };
+ return Bool.init(p._meta.is_root);
}
};
pub const asset = struct {
pub const signature: Signature = .{
- .params = &.{.str},
+ .params = &.{.String},
.ret = .Asset,
};
pub const description =
@@ -155,7 +252,7 @@ pub const Builtins = struct {
if (args.len != 1) return bad_arg;
const ref = switch (args[0]) {
- .string => |s| s,
+ .string => |s| s.value,
else => return bad_arg,
};
@@ -168,7 +265,7 @@ pub const Builtins = struct {
\\Returns the Site that the page belongs to.
;
pub const examples =
- \\
+ \\
;
pub fn call(
p: *const Page,
@@ -180,43 +277,18 @@ pub const Builtins = struct {
return .{ .site = p._meta.site };
}
};
- pub const locales = struct {
- pub const signature: Signature = .{ .ret = .{ .many = .Page } };
- pub const description =
- \\Returns a list of localized variants of the current page.
- ;
- pub const examples =
- \\
- ;
- pub fn call(
- p: *const Page,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- _ = gpa;
- if (args.len != 0) return .{ .err = "expected 0 arguments" };
- return .{
- .iterator = .{
- .impl = .{
- .translation_it = context.TranslationIterator.init(p),
- },
- },
- };
- }
- };
- pub const @"locale?" = struct {
+
+ pub const locale = struct {
pub const signature: Signature = .{
- .params = &.{.str},
- .ret = .{ .opt = .Page },
+ .params = &.{.String},
+ .ret = .{ .Opt = .Page },
};
pub const description =
- \\Returns a reference to a localized variant of the target page, if
- \\present. Returns null otherwise.
+ \\Returns a reference to a localized variant of the target page.
\\
- \\To be used in conjunction with an `if` attribute.
;
pub const examples =
- \\
+ \\
;
pub fn call(
p: *const Page,
@@ -231,7 +303,7 @@ pub const Builtins = struct {
if (args.len != 1) return bad_arg;
const code = switch (args[0]) {
- .string => |s| s,
+ .string => |s| s.value,
else => return bad_arg,
};
@@ -242,10 +314,10 @@ pub const Builtins = struct {
for (p._meta.key_variants) |*v| {
if (std.mem.eql(u8, v.site._meta.kind.multi.code, code)) {
const other = context.pageGet(other_site, tk, null, null, false) catch @panic("TODO: report that a localized variant failed to load");
- return .{ .optional = .{ .page = other } };
+ return .{ .page = other };
}
}
- return .{ .optional = null };
+ return .{ .err = "locale not found" };
} else {
const other = context.pageGet(
other_site,
@@ -253,38 +325,38 @@ pub const Builtins = struct {
null,
null,
false,
- ) catch @panic("trying to access a non-existent localized variant of a page is an error for now, sorry! give the same translation key to all variants of this page and you won't see this error anymore.");
- return .{ .optional = .{ .page = other } };
+ ) catch @panic("Trying to access a non-existent localized variant of a page is an error for now, sorry! As a temporary workaround you can set a translation key for this page (and its localized variants). This limitation will be lifted in the future.");
+ return .{ .page = other };
}
}
};
- pub const @"locale!" = struct {
+ pub const @"locale?" = struct {
pub const signature: Signature = .{
- .params = &.{.str},
- .ret = .{ .opt = .Page },
+ .params = &.{.String},
+ .ret = .{ .Opt = .Page },
};
pub const description =
- \\Returns a reference to a localized variant of the target page.
+ \\Returns a reference to a localized variant of the target page, if
+ \\present. Returns null otherwise.
\\
+ \\To be used in conjunction with an `if` attribute.
;
pub const examples =
- \\
+ \\
;
pub fn call(
p: *const Page,
gpa: Allocator,
args: []const Value,
) !Value {
- _ = gpa;
-
const bad_arg = .{
.err = "expected 1 string argument",
};
if (args.len != 1) return bad_arg;
const code = switch (args[0]) {
- .string => |s| s,
+ .string => |s| s.value,
else => return bad_arg,
};
@@ -295,10 +367,10 @@ pub const Builtins = struct {
for (p._meta.key_variants) |*v| {
if (std.mem.eql(u8, v.site._meta.kind.multi.code, code)) {
const other = context.pageGet(other_site, tk, null, null, false) catch @panic("TODO: report that a localized variant failed to load");
- return .{ .page = other };
+ return Optional.init(gpa, other);
}
}
- return .{ .err = "locale not found" };
+ return .{ .optional = null };
} else {
const other = context.pageGet(
other_site,
@@ -307,12 +379,35 @@ pub const Builtins = struct {
null,
false,
) catch @panic("trying to access a non-existent localized variant of a page is an error for now, sorry! give the same translation key to all variants of this page and you won't see this error anymore.");
- return .{ .page = other };
+ return Optional.init(gpa, other);
}
}
};
+
+ pub const locales = struct {
+ pub const signature: Signature = .{ .ret = .{ .Many = .Page } };
+ pub const description =
+ \\Returns the list of localized variants of the current page.
+ ;
+ pub const examples =
+ \\
+ ;
+ pub fn call(
+ p: *const Page,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len != 0) return .{ .err = "expected 0 arguments" };
+ return .{
+ .iterator = try context.Iterator.init(gpa, .{
+ .translation_it = context.Iterator.TranslationIterator.init(p),
+ }),
+ };
+ }
+ };
+
pub const wordCount = struct {
- pub const signature: Signature = .{ .ret = .int };
+ pub const signature: Signature = .{ .ret = .Int };
pub const description =
\\Returns the word count of the page.
\\
@@ -329,12 +424,12 @@ pub const Builtins = struct {
) !Value {
_ = gpa;
if (args.len != 0) return .{ .err = "expected 0 arguments" };
- return .{ .int = @intCast(self._meta.word_count) };
+ return .{ .int = .{ .value = @intCast(self._meta.word_count) } };
}
};
pub const isSection = struct {
- pub const signature: Signature = .{ .ret = .bool };
+ pub const signature: Signature = .{ .ret = .Bool };
pub const description =
\\Returns true if the current page defines a section (i.e. if
\\the current page is an 'index.md' page).
@@ -350,12 +445,12 @@ pub const Builtins = struct {
) !Value {
_ = gpa;
if (args.len != 0) return .{ .err = "expected 0 arguments" };
- return .{ .bool = self._meta.is_section };
+ return Bool.init(self._meta.is_section);
}
};
pub const subpages = struct {
- pub const signature: Signature = .{ .ret = .{ .many = .Page } };
+ pub const signature: Signature = .{ .ret = .{ .Many = .Page } };
pub const description =
\\Returns a list of all the pages in this section. If the page is
\\not a section, returns an empty list.
@@ -364,7 +459,9 @@ pub const Builtins = struct {
\\structure section in the official docs for more info.
;
pub const examples =
- \\
+ \\
+ \\
+ \\
;
pub fn call(
p: *const Page,
@@ -383,7 +480,7 @@ pub const Builtins = struct {
};
pub const nextPage = struct {
- pub const signature: Signature = .{ .ret = .{ .opt = .Page } };
+ pub const signature: Signature = .{ .ret = .{ .Opt = .Page } };
pub const description =
\\Returns the next page in the same section, sorted by date.
\\
@@ -393,7 +490,7 @@ pub const Builtins = struct {
;
pub const examples =
\\
- \\
+ \\
\\
;
@@ -412,7 +509,7 @@ pub const Builtins = struct {
}
};
pub const prevPage = struct {
- pub const signature: Signature = .{ .ret = .{ .opt = .Page } };
+ pub const signature: Signature = .{ .ret = .{ .Opt = .Page } };
pub const description =
\\Tries to return the page before the target one (sorted by date), to be used with an `if` attribute.
;
@@ -438,7 +535,7 @@ pub const Builtins = struct {
};
pub const hasNext = struct {
- pub const signature: Signature = .{ .ret = .bool };
+ pub const signature: Signature = .{ .ret = .Bool };
pub const description =
\\Returns true of the target page has another page after (sorted by date)
;
@@ -448,9 +545,10 @@ pub const Builtins = struct {
pub fn call(
p: *const Page,
- _: Allocator,
+ gpa: Allocator,
args: []const Value,
) !Value {
+ _ = gpa;
if (args.len != 0) return .{ .err = "expected 0 arguments" };
if (p._meta.index_in_section == null) return .{
@@ -458,11 +556,11 @@ pub const Builtins = struct {
};
const other = try context.pageFind(.{ .next = p });
- return if (other.optional == null) .{ .bool = false } else .{ .bool = true };
+ return Bool.init(other.optional != null);
}
};
pub const hasPrev = struct {
- pub const signature: Signature = .{ .ret = .bool };
+ pub const signature: Signature = .{ .ret = .Bool };
pub const description =
\\Returns true of the target page has another page before (sorted by date)
;
@@ -471,24 +569,25 @@ pub const Builtins = struct {
;
pub fn call(
p: *const Page,
- _: Allocator,
+ gpa: Allocator,
args: []const Value,
) !Value {
+ _ = gpa;
if (args.len != 0) return .{ .err = "expected 0 arguments" };
const idx = p._meta.index_in_section orelse return .{
.err = "unable to do prev on a page loaded by scripty, for now",
};
- if (idx == 0) return .{ .bool = false };
+ if (idx == 0) return Bool.False;
const other = try context.pageFind(.{ .prev = p });
- return if (other.optional == null) .{ .bool = false } else .{ .bool = true };
+ return Bool.init(other.optional != null);
}
};
pub const link = struct {
- pub const signature: Signature = .{ .ret = .str };
+ pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Returns the URL of the target page.
;
@@ -516,13 +615,13 @@ pub const Builtins = struct {
"/",
});
- return .{ .string = result };
+ return String.init(result);
}
};
// TODO: delete this
pub const permalink = struct {
- pub const signature: Signature = .{ .ret = .str };
+ pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Deprecated, use `link()`
;
@@ -537,7 +636,7 @@ pub const Builtins = struct {
};
pub const content = struct {
- pub const signature: Signature = .{ .ret = .str };
+ pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Renders the full Markdown page to HTML
;
@@ -553,29 +652,24 @@ pub const Builtins = struct {
const ast = p._meta.ast orelse return .{
.err = "only the main page can be rendered for now",
};
- try render.html(gpa, ast, ast.md.root, true, "", buf.writer());
- return .{ .string = try buf.toOwnedSlice() };
+ try render.html(gpa, ast, ast.md.root, "", buf.writer());
+ return String.init(try buf.toOwnedSlice());
}
};
pub const block = struct {
pub const signature: Signature = .{
- .params = &.{ .str, .{ .opt = .bool } },
- .ret = .str,
+ .params = &.{.String},
+ .ret = .String,
};
pub const description =
- \\Renders only the specified content block of a page.
- \\A content blcok is a Markdown heading defined to be a `block`
- \\with an id attribute set.
- \\
- \\A second optional boolean parameter defines if the heading itself
- \\should be rendered or not (defaults to `true`).
+ \\Renders the specified content block of a page.
\\
\\Example:
\\ `# [Title]($block.id('section-id'))`
;
pub const examples =
- \\
- \\
+ \\
+ \\
;
pub fn call(
p: *const Page,
@@ -583,17 +677,12 @@ pub const Builtins = struct {
args: []const Value,
) !Value {
const bad_arg = .{
- .err = "expected 1 string argument and an optional bool argument",
+ .err = "expected 1 string argument argument",
};
- if (args.len < 1 or args.len > 2) return bad_arg;
+ if (args.len != 1) return bad_arg;
const block_id = switch (args[0]) {
- .string => |s| s,
- else => return bad_arg,
- };
-
- const heading = if (args.len == 1) true else switch (args[1]) {
- .bool => |s| s,
+ .string => |s| s.value,
else => return bad_arg,
};
@@ -602,28 +691,26 @@ pub const Builtins = struct {
};
var buf = std.ArrayList(u8).init(gpa);
- const node = ast.sections.get(block_id) orelse {
+ const node = ast.blocks.get(block_id) orelse {
return Value.errFmt(
gpa,
"content section '{s}' doesn't exist, available sections are: {s}",
- .{ block_id, ast.sections.keys() },
+ .{ block_id, ast.blocks.keys() },
);
};
- try render.html(gpa, ast, node, heading, "", buf.writer());
- return .{ .string = try buf.toOwnedSlice() };
+ try render.html(gpa, ast, node, "", buf.writer());
+ return String.init(try buf.toOwnedSlice());
}
};
pub const toc = struct {
- pub const signature: Signature = .{
- .ret = .str,
- };
+ pub const signature: Signature = .{ .ret = .String };
pub const description =
\\Renders the table of content.
;
pub const examples =
- \\
+ \\
;
pub fn call(
p: *const Page,
@@ -641,7 +728,7 @@ pub const Builtins = struct {
var buf = std.ArrayList(u8).init(gpa);
try render.htmlToc(ast, buf.writer());
- return .{ .string = try buf.toOwnedSlice() };
+ return String.init(try buf.toOwnedSlice());
}
};
};
diff --git a/src/context/Site.zig b/src/context/Site.zig
index 0e11633..6e21a36 100644
--- a/src/context/Site.zig
+++ b/src/context/Site.zig
@@ -5,8 +5,10 @@ const Allocator = std.mem.Allocator;
const scripty = @import("scripty");
const utils = @import("utils.zig");
const context = @import("../context.zig");
+const Signature = @import("doctypes.zig").Signature;
const Value = context.Value;
-const Signature = @import("docgen.zig").Signature;
+const Bool = context.Bool;
+const String = context.String;
host_url: []const u8,
title: []const u8,
@@ -33,10 +35,18 @@ pub const description =
pub const dot = scripty.defaultDot(Site, Value, false);
pub const PassByRef = true;
+pub const Fields = struct {
+ pub const host_url =
+ \\The host URL, as defined in your `build.zig`.
+ ;
+ pub const title =
+ \\The website title, as defined in your `build.zig`.
+ ;
+};
pub const Builtins = struct {
pub const localeCode = struct {
pub const signature: Signature = .{
- .ret = .str,
+ .ret = .String,
};
pub const description =
\\In a multilingual website, returns the locale of the current
@@ -51,14 +61,13 @@ pub const Builtins = struct {
args: []const Value,
) !Value {
_ = gpa;
-
const bad_arg = .{
.err = "expected 0 arguments",
};
if (args.len != 0) return bad_arg;
return switch (p._meta.kind) {
- .multi => |l| .{ .string = l.code },
+ .multi => |l| String.init(l.code),
.simple => .{
.err = "only available in a multilingual website",
},
@@ -67,14 +76,14 @@ pub const Builtins = struct {
};
pub const localeName = struct {
pub const signature: Signature = .{
- .ret = .str,
+ .ret = .String,
};
pub const description =
\\In a multilingual website, returns the locale name of the current
\\variant as defined in your `build.zig` file.
;
pub const examples =
- \\
+ \\
;
pub fn call(
p: *const Site,
@@ -82,14 +91,13 @@ pub const Builtins = struct {
args: []const Value,
) !Value {
_ = gpa;
-
const bad_arg = .{
.err = "expected 0 arguments",
};
if (args.len != 0) return bad_arg;
return switch (p._meta.kind) {
- .multi => |l| .{ .string = l.name },
+ .multi => |l| String.init(l.name),
.simple => .{
.err = "only available in a multilingual website",
},
@@ -99,7 +107,7 @@ pub const Builtins = struct {
pub const link = struct {
pub const signature: Signature = .{
- .ret = .str,
+ .ret = .String,
};
pub const description =
\\Returns a link to the homepage of the website.
@@ -108,7 +116,7 @@ pub const Builtins = struct {
\\multilingual website.
;
pub const examples =
- \\
+ \\
;
pub fn call(
p: *const Site,
@@ -126,13 +134,13 @@ pub const Builtins = struct {
"/",
}) catch @panic("oom");
- return .{ .string = url };
+ return String.init(url);
}
};
pub const asset = struct {
pub const signature: Signature = .{
- .params = &.{.str},
+ .params = &.{.String},
.ret = .Asset,
};
pub const description =
@@ -152,7 +160,7 @@ pub const Builtins = struct {
if (args.len != 1) return bad_arg;
const ref = switch (args[0]) {
- .string => |s| s,
+ .string => |s| s.value,
else => return bad_arg,
};
@@ -161,7 +169,7 @@ pub const Builtins = struct {
};
pub const page = struct {
pub const signature: Signature = .{
- .parameters = &.{.str},
+ .params = &.{.String},
.ret = .Page,
};
pub const description =
@@ -192,7 +200,7 @@ pub const Builtins = struct {
if (args.len != 1) return bad_arg;
const ref = switch (args[0]) {
- .string => |s| s,
+ .string => |s| s.value,
else => return bad_arg,
};
@@ -206,7 +214,7 @@ pub const Builtins = struct {
};
pub const pages = struct {
pub const signature: Signature = .{
- .parameters = &.{ .many = .str },
+ .params = &.{.{ .Many = .String }},
.ret = .Page,
};
pub const description =
@@ -231,7 +239,7 @@ pub const Builtins = struct {
errdefer gpa.free(page_list);
for (args, page_list) |a, *p| {
const ref = switch (a) {
- .string => |s| s,
+ .string => |s| s.value,
else => {
gpa.free(page_list);
return .{ .err = "argument is not a string" };
@@ -254,17 +262,15 @@ pub const Builtins = struct {
}
}
return .{
- .iterator = .{
- .impl = .{
- .page_slice_it = .{ .items = page_list },
- },
- },
+ .iterator = try context.Iterator.init(gpa, .{
+ .page_slice_it = .{ .items = page_list },
+ }),
};
}
};
pub const locale = struct {
pub const signature: Signature = .{
- .parameters = &.{.str},
+ .params = &.{.String},
.ret = .Site,
};
pub const description =
@@ -286,7 +292,7 @@ pub const Builtins = struct {
if (args.len != 1) return bad_arg;
const code = switch (args[0]) {
- .string => |s| s,
+ .string => |s| s.value,
else => return bad_arg,
};
diff --git a/src/context/Slice.zig b/src/context/Slice.zig
new file mode 100644
index 0000000..3dc7563
--- /dev/null
+++ b/src/context/Slice.zig
@@ -0,0 +1,17 @@
+const Slice = @This();
+
+const std = @import("std");
+const context = @import("../context.zig");
+const Allocator = std.mem.Allocator;
+const Value = context.Value;
+
+value: []const Value,
+
+pub fn dot(s: Slice, gpa: Allocator, path: []const u8) !Value {
+ _ = s;
+ _ = gpa;
+ _ = path;
+ return .{ .err = "todo" };
+}
+pub const description = "TODO";
+pub const Builtins = struct {};
diff --git a/src/context/String.zig b/src/context/String.zig
new file mode 100644
index 0000000..f82a37c
--- /dev/null
+++ b/src/context/String.zig
@@ -0,0 +1,413 @@
+const String = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const hl = @import("../highlight.zig");
+const utils = @import("utils.zig");
+const log = utils.log;
+const Signature = @import("doctypes.zig").Signature;
+const Value = @import("../context.zig").Value;
+
+value: []const u8,
+
+pub fn init(str: []const u8) Value {
+ return .{ .string = .{ .value = str } };
+}
+
+pub const description = "A string.";
+pub const PassByRef = false;
+pub const Builtins = struct {
+ pub const len = struct {
+ pub const signature: Signature = .{ .ret = .Int };
+ pub const description =
+ \\Returns the length of a string.
+ \\
+ ;
+ pub const examples =
+ \\$page.title.len()
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len != 0) return .{ .err = "expected 0 arguments" };
+ return Value.from(gpa, str.value.len);
+ }
+ };
+
+ pub const contains = struct {
+ pub const signature: Signature = .{
+ .params = &.{.String},
+ .ret = .Bool,
+ };
+ pub const description =
+ \\Returns true if the receiver contains the provided string.
+ \\
+ ;
+ pub const examples =
+ \\$page.permalink().contains("/blog/")
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ const bad_arg = .{
+ .err = "expected 1 string argument",
+ };
+ if (args.len != 1) return bad_arg;
+
+ const needle = switch (args[0]) {
+ .string => |s| s.value,
+ else => return bad_arg,
+ };
+
+ return Value.from(gpa, std.mem.indexOf(u8, str.value, needle) != null);
+ }
+ };
+
+ pub const endsWith = struct {
+ pub const signature: Signature = .{
+ .params = &.{.String},
+ .ret = .Bool,
+ };
+ pub const description =
+ \\Returns true if the receiver ends with the provided string.
+ \\
+ ;
+ pub const examples =
+ \\$page.permalink().endsWith("/blog/")
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ const bad_arg = .{
+ .err = "expected 1 string argument",
+ };
+ if (args.len != 1) return bad_arg;
+
+ const needle = switch (args[0]) {
+ .string => |s| s.value,
+ else => return bad_arg,
+ };
+
+ const result = std.mem.endsWith(u8, str.value, needle);
+ log.debug("endsWith('{s}', '{s}') = {}", .{ str.value, needle, result });
+
+ return Value.from(gpa, result);
+ }
+ };
+ pub const eql = struct {
+ pub const signature: Signature = .{
+ .params = &.{.String},
+ .ret = .Bool,
+ };
+ pub const description =
+ \\Returns true if the receiver equals the provided string.
+ \\
+ ;
+ pub const examples =
+ \\$page.author.eql("Loris Cro")
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ const bad_arg = .{
+ .err = "expected 1 string argument",
+ };
+ if (args.len != 1) return bad_arg;
+ const needle = switch (args[0]) {
+ .string => |s| s.value,
+ else => return bad_arg,
+ };
+
+ return Value.from(gpa, std.mem.eql(u8, str.value, needle));
+ }
+ };
+
+ pub const basename = struct {
+ pub const signature: Signature = .{ .ret = .String };
+ pub const description =
+ \\Returns the last component of a path.
+ ;
+ pub const examples =
+ \\TODO
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len != 0) return .{ .err = "expected 0 arguments" };
+
+ return Value.from(gpa, std.fs.path.basename(str.value));
+ }
+ };
+ pub const suffix = struct {
+ pub const signature: Signature = .{
+ .params = &.{ .String, .{ .Many = .String } },
+ .ret = .String,
+ };
+ pub const description =
+ \\Concatenates strings together (left-to-right).
+ \\
+ ;
+ pub const examples =
+ \\$page.title.suffix("Foo","Bar", "Baz")
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len == 0) return .{ .err = "'suffix' wants at least one argument" };
+ var out = std.ArrayList(u8).init(gpa);
+ errdefer out.deinit();
+
+ try out.appendSlice(str.value);
+ for (args) |a| {
+ const fx = switch (a) {
+ .string => |s| s.value,
+ else => return .{ .err = "'suffix' arguments must be strings" },
+ };
+
+ try out.appendSlice(fx);
+ }
+
+ return Value.from(gpa, try out.toOwnedSlice());
+ }
+ };
+ pub const fmt = struct {
+ pub const signature: Signature = .{
+ .params = &.{ .String, .{ .Many = .String } },
+ .ret = .String,
+ };
+ pub const description =
+ \\Looks for '{}' placeholders in the receiver string and
+ \\replaces them with the provided arguments.
+ \\
+ ;
+ pub const examples =
+ \\$i18n.get!("welcome-message").fmt($page.custom.get!("name"))
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len == 0) return .{ .err = "expected 1 or more argument(s)" };
+ var out = std.ArrayList(u8).init(gpa);
+ errdefer out.deinit();
+
+ var it = std.mem.splitSequence(u8, str.value, "{}");
+ for (args) |a| {
+ const str_arg = switch (a) {
+ .string => |s| s.value,
+ else => return .{ .err = "'path' arguments must be strings" },
+ };
+ const before = it.next() orelse {
+ return .{ .err = "fmt: more args than placeholders" };
+ };
+
+ try out.appendSlice(before);
+ try out.appendSlice(str_arg);
+ }
+
+ const last = it.next() orelse {
+ return .{ .err = "fmt: more args than placeholders" };
+ };
+
+ try out.appendSlice(last);
+
+ if (it.next() != null) {
+ return .{ .err = "fmt: more placeholders than args" };
+ }
+
+ return Value.from(gpa, try out.toOwnedSlice());
+ }
+ };
+
+ pub const addPath = struct {
+ pub const signature: Signature = .{
+ .params = &.{ .String, .{ .Many = .String } },
+ .ret = .String,
+ };
+ pub const description =
+ \\Joins URL path segments automatically adding `/` as needed.
+ ;
+ pub const examples =
+ \\$site.host_url.addPath("rss.xml")
+ \\$site.host_url.addPath("foo/bar", "/baz")
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len == 0) return .{ .err = "'path' wants at least one argument" };
+ var out = std.ArrayList(u8).init(gpa);
+ errdefer out.deinit();
+
+ try out.appendSlice(str.value);
+ if (!std.mem.endsWith(u8, str.value, "/")) {
+ try out.append('/');
+ }
+
+ for (args) |a| {
+ const fx = switch (a) {
+ .string => |s| s.value,
+ else => return .{ .err = "'path' arguments must be strings" },
+ };
+
+ if (fx.len == 0) continue;
+ if (fx[0] == '/') {
+ try out.appendSlice(fx[1..]);
+ } else {
+ try out.appendSlice(fx);
+ }
+ }
+
+ return Value.from(gpa, try out.toOwnedSlice());
+ }
+ };
+ pub const syntaxHighlight = struct {
+ pub const signature: Signature = .{
+ .params = &.{.String},
+ .ret = .String,
+ };
+ pub const description =
+ \\Applies syntax highlighting to a string.
+ \\The argument specifies the language name.
+ \\
+ ;
+ pub const examples =
+ \\
+ \\
+ \\
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len != 1) return .{ .err = "'syntaxHighlight' wants one argument" };
+ var out = std.ArrayList(u8).init(gpa);
+ errdefer out.deinit();
+
+ const lang = switch (args[0]) {
+ .string => |s| s.value,
+ else => return .{
+ .err = "the argument to 'syntaxHighlight' must be of type string",
+ },
+ };
+
+ // _ = lang;
+ // _ = str;
+ hl.highlightCode(gpa, lang, str.value, out.writer()) catch |err| switch (err) {
+ error.NoLanguage => return .{ .err = "unable to find a parser for the provided language" },
+ error.OutOfMemory => return error.OutOfMemory,
+ else => return .{ .err = "error while syntax highlighting" },
+ };
+
+ return Value.from(gpa, try out.toOwnedSlice());
+ }
+ };
+
+ pub const parseInt = struct {
+ pub const signature: Signature = .{ .ret = .Int };
+ pub const description =
+ \\Parses an integer out of a string
+ \\
+ ;
+ pub const examples =
+ \\$page.custom.get!('not-a-num-for-some-reason').parseInt()
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len != 0) return .{ .err = "expected 0 arguments" };
+
+ const parsed = std.fmt.parseInt(i64, str.value, 10) catch |err| {
+ return Value.errFmt(gpa, "error parsing int from '{s}': {s}", .{
+ str.value, @errorName(err),
+ });
+ };
+
+ return Value.from(gpa, parsed);
+ }
+ };
+
+ pub const splitN = struct {
+ pub const signature: Signature = .{
+ .params = &.{ .String, .Int },
+ .ret = .String,
+ };
+ pub const description =
+ \\Splits the string using the first string argument as delimiter and then
+ \\returns the Nth substring (where N is the second argument).
+ \\
+ \\Indices start from 0.
+ \\
+ ;
+ pub const examples =
+ \\$page.author.splitN(" ", 1)
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len != 2) return .{ .err = "expected 2 (string, int) arguments" };
+
+ const split = switch (args[0]) {
+ .string => |s| s.value,
+ else => return .{ .err = "the first argument must be a string" },
+ };
+
+ const n: usize = switch (args[1]) {
+ .int => |i| if (i.value >= 0) @intCast(i.value) else return .{
+ .err = "the second argument must be non-negative",
+ },
+ else => return .{ .err = "the second argument must be an integer" },
+ };
+
+ var it = std.mem.splitSequence(u8, str.value, split);
+ const too_short: Value = .{ .err = "sequence ended too early" };
+ for (0..n) |_| _ = it.next() orelse return too_short;
+
+ const result = it.next() orelse return too_short;
+ return Value.from(gpa, result);
+ }
+ };
+ pub const lower = struct {
+ pub const signature: Signature = .{ .ret = .String };
+ pub const description =
+ \\Returns a lowercase version of the target string.
+ \\
+ ;
+ pub const examples =
+ \\$page.title.lower()
+ ;
+ pub fn call(
+ str: String,
+ gpa: Allocator,
+ args: []const Value,
+ ) !Value {
+ if (args.len != 0) return .{ .err = "expected 0 arguments" };
+
+ const l = try gpa.dupe(u8, str.value);
+ for (l) |*ch| ch.* = std.ascii.toLower(ch.*);
+
+ return Value.from(gpa, l);
+ }
+ };
+};
diff --git a/src/context/Template.zig b/src/context/Template.zig
index 4c2401a..32f957d 100644
--- a/src/context/Template.zig
+++ b/src/context/Template.zig
@@ -3,21 +3,64 @@ const Template = @This();
const superhtml = @import("superhtml");
const scripty = @import("scripty");
const ziggy = @import("ziggy");
-const Value = @import("../context.zig").Value;
-const Site = @import("Site.zig");
-const Build = @import("Build.zig");
-const Page = @import("Page.zig");
+const context = @import("../context.zig");
+const Value = context.Value;
+const Site = context.Site;
+const Page = context.Page;
+const Build = context.Build;
+const Map = context.Map;
+const Iterator = context.Iterator;
+const Optional = context.Optional;
const Ctx = superhtml.utils.Ctx;
site: *const Site,
page: *const Page,
-i18n: ziggy.dynamic.Value,
build: Build = .{},
+i18n: Map.ZiggyMap,
// Globals specific to SuperHTML
-loop: ?Value = null,
-@"if": ?Value = null,
ctx: Ctx(Value) = .{},
+loop: ?*Iterator = null,
+@"if": ?*const Optional = null,
pub const dot = scripty.defaultDot(Template, Value, false);
+pub const description = "";
+pub const Fields = struct {
+ pub const site =
+ \\The current website. In a multilingual website,
+ \\each locale will have its own separate instance of $site
+ ;
+
+ pub const page =
+ \\The page being currently rendered.
+ ;
+
+ pub const i18n =
+ \\In a multilingual website it contains the translations
+ \\defined in the corresponding i18n file.
+ \\
+ \\See the i18n docs for more info.
+ ;
+
+ pub const build =
+ \\Gives you access to build-time assets (i.e. assets built
+ \\ via the Zig build system) alongside other information
+ \\relative to the current build.
+ ;
+
+ pub const ctx =
+ \\A key-value mapping that contains data defined in ``
+ \\nodes.
+ ;
+
+ pub const loop =
+ \\The current iterator, only available within elements
+ \\that have a `loop` attribute.
+ ;
+
+ pub const @"if" =
+ \\The current branching variable, only available within elements
+ \\that have an `if` attribute used to unwrap an optional value.
+ ;
+};
pub const Builtins = struct {};
diff --git a/src/context/docgen.zig b/src/context/docgen.zig
deleted file mode 100644
index f6099f9..0000000
--- a/src/context/docgen.zig
+++ /dev/null
@@ -1,69 +0,0 @@
-const std = @import("std");
-const ziggy = @import("ziggy");
-const context = @import("../context.zig");
-const Value = context.Value;
-
-pub const Signature = struct {
- params: []const ScriptyParam = &.{},
- ret: ScriptyParam,
-};
-
-pub const ScriptyParam = union(enum) {
- Site,
- Page,
- Build,
- Assets,
- Asset,
- Alternative,
- str,
- int,
- bool,
- date,
- dyn,
- opt: Base,
- many: Base,
-
- pub const Base = enum {
- Site,
- Page,
- Alternative,
- str,
- int,
- bool,
- date,
- dyn,
- };
-
- pub fn fromType(t: type) ScriptyParam {
- return switch (t) {
- context.Page, *context.Page => .Page,
- []const u8 => .str,
- []const []const u8 => .{ .many = .str },
- context.DateTime => .date,
- usize => .int,
- bool => .bool,
- ziggy.dynamic.Value => .dyn,
- context.Page.Alternative => .Alternative,
- []const context.Page.Alternative => .{ .many = .Alternative },
- context.Page.Translation => .Translation,
- []const context.Page.Translation => .{ .many = .Translation },
-
- else => @compileError("TODO: add support for " ++ @typeName(t)),
- };
- }
-
- pub fn name(p: ScriptyParam, comptime is_fn_param: bool) []const u8 {
- switch (p) {
- .many => |m| switch (m) {
- inline else => |mm| {
- const dots = if (is_fn_param) "..." else "";
- return "[" ++ dots ++ @tagName(mm) ++ "]";
- },
- },
- .opt => |o| switch (o) {
- inline else => |oo| return "?" ++ @tagName(oo),
- },
- inline else => return @tagName(p),
- }
- }
-};
diff --git a/src/context/doctypes.zig b/src/context/doctypes.zig
new file mode 100644
index 0000000..010ead5
--- /dev/null
+++ b/src/context/doctypes.zig
@@ -0,0 +1,146 @@
+const std = @import("std");
+const ziggy = @import("ziggy");
+const superhtml = @import("superhtml");
+const context = @import("../context.zig");
+const Value = context.Value;
+
+pub const Signature = struct {
+ params: []const ScriptyParam = &.{},
+ ret: ScriptyParam,
+
+ pub fn format(
+ s: Signature,
+ comptime fmt: []const u8,
+ options: std.fmt.FormatOptions,
+ out_stream: anytype,
+ ) !void {
+ _ = fmt;
+ _ = options;
+ try out_stream.writeAll("(");
+ for (s.params, 0..) |p, idx| {
+ try out_stream.writeAll(p.link(true));
+ if (idx < s.params.len - 1) {
+ try out_stream.writeAll(", ");
+ }
+ }
+ try out_stream.writeAll(") -> ");
+ try out_stream.writeAll(s.ret.link(false));
+ }
+};
+
+pub const ScriptyParam = union(enum) {
+ Site,
+ Page,
+ Build,
+ Asset,
+ Alternative,
+ Iterator,
+ String,
+ Int,
+ Float,
+ Bool,
+ Date,
+ Ctx,
+ KV,
+ any,
+ err,
+ Map: Base,
+ Opt: Base,
+ Many: Base,
+
+ pub const Base = enum {
+ Site,
+ Page,
+ Alternative,
+ Iterator,
+ String,
+ Int,
+ Bool,
+ Date,
+ KV,
+ any,
+ };
+
+ pub fn fromType(t: type) ScriptyParam {
+ return switch (t) {
+ context.Template => .any,
+ ?context.Value => .any,
+ context.Page, *const context.Page => .Page,
+ context.Site, *const context.Site => .Site,
+ context.Build => .Build,
+ superhtml.utils.Ctx(context.Value) => .Ctx,
+ context.Page.Alternative => .Alternative,
+ context.Asset => .Asset,
+ // context.Slice => .any,
+ context.Optional, ?*const context.Optional => .{ .Opt = .any },
+ context.String => .String,
+ context.Bool => .Bool,
+ context.Int => .Int,
+ context.Float => .Float,
+ context.DateTime => .Date,
+ context.Map, context.Map.ZiggyMap => .{ .Map = .any },
+ context.Map.KV => .KV,
+ context.Iterator => .Iterator,
+ ?*context.Iterator => .{ .Opt = .Iterator },
+ []const context.Page.Alternative => .{ .Many = .Alternative },
+ []const u8 => .String,
+ ?[]const u8 => .{ .Opt = .String },
+ []const []const u8 => .{ .Many = .String },
+ bool => .Bool,
+ usize => .Int,
+ ziggy.dynamic.Value => .any,
+ context.Value => .any,
+ else => @compileError("TODO: add support for " ++ @typeName(t)),
+ };
+ }
+
+ pub fn string(
+ p: ScriptyParam,
+ comptime is_fn_param: bool,
+ ) []const u8 {
+ switch (p) {
+ .Many => |m| switch (m) {
+ inline else => |mm| {
+ const dots = if (is_fn_param) "..." else "";
+ return "[" ++ @tagName(mm) ++ dots ++ "]";
+ },
+ },
+ .Opt => |o| switch (o) {
+ inline else => |oo| return "?" ++ @tagName(oo),
+ },
+ inline else => return @tagName(p),
+ }
+ }
+ pub fn link(
+ p: ScriptyParam,
+ comptime is_fn_param: bool,
+ ) []const u8 {
+ switch (p) {
+ .Many => |m| switch (m) {
+ inline else => |mm| {
+ const dots = if (is_fn_param) "..." else "";
+ return std.fmt.comptimePrint(
+ \\[[{0s}]($link.ref("{0s}")){1s}]
+ , .{ @tagName(mm), dots });
+ },
+ },
+ .Opt => |o| switch (o) {
+ inline else => |oo| return comptime std.fmt.comptimePrint(
+ \\?[{0s}]($link.ref("{0s}"))
+ , .{@tagName(oo)}),
+ },
+ inline else => |_, t| return comptime std.fmt.comptimePrint(
+ \\[{0s}]($link.ref("{0s}"))
+ , .{@tagName(t)}),
+ }
+ }
+
+ pub fn id(p: ScriptyParam) []const u8 {
+ switch (p) {
+ .Opt, .Many => |o| switch (o) {
+ inline else => |oo| return @tagName(oo),
+ },
+ inline else => return @tagName(p),
+ }
+ }
+};
diff --git a/src/context/primitive_builtins/Bool.zig b/src/context/primitive_builtins/Bool.zig
deleted file mode 100644
index f310a42..0000000
--- a/src/context/primitive_builtins/Bool.zig
+++ /dev/null
@@ -1,113 +0,0 @@
-const std = @import("std");
-const utils = @import("../utils.zig");
-const Allocator = std.mem.Allocator;
-const Signature = @import("../docgen.zig").Signature;
-const Value = @import("../../context.zig").Value;
-
-pub const then = struct {
- pub const signature: Signature = .{
- .params = &.{ .str, .{ .opt = .str } },
- .ret = .str,
- };
- pub const description =
- \\If the boolean is `true`, returns the first argument.
- \\Otherwise, returns the second argument.
- \\
- \\Omitting the second argument defaults to an empty string.
- \\
- ;
- pub const examples =
- \\$page.draft.then("DRAFT!")
- ;
- pub fn call(
- b: bool,
- _: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len < 1 or args.len > 2) return .{
- .err = "expected 1 or 2 string arguments",
- };
-
- if (b) {
- return args[0];
- } else {
- if (args.len < 2) return .{ .string = "" };
- return args[1];
- }
- }
-};
-pub const not = struct {
- pub const signature: Signature = .{ .ret = .bool };
- pub const description =
- \\Negates a boolean value.
- \\
- ;
- pub const examples =
- \\$page.draft.not()
- ;
- pub fn call(
- b: bool,
- _: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len != 0) return .{ .err = "'not' wants no arguments" };
- return .{ .bool = !b };
- }
-};
-pub const @"and" = struct {
- pub const signature: Signature = .{
- .params = &.{ .bool, .{ .many = .bool } },
- .ret = .bool,
- };
-
- pub const description =
- \\Computes logical `and` between the receiver value and any other value passed as argument.
- \\
- ;
- pub const examples =
- \\$page.draft.and($site.tags.len().eq(10))
- ;
- pub fn call(
- b: bool,
- _: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len == 0) return .{ .err = "'and' wants at least one argument" };
- for (args) |a| switch (a) {
- .bool => {},
- else => return .{ .err = "wrong argument type" },
- };
- if (!b) return .{ .bool = false };
- for (args) |a| if (!a.bool) return .{ .bool = false };
-
- return .{ .bool = true };
- }
-};
-pub const @"or" = struct {
- pub const signature: Signature = .{
- .params = &.{ .bool, .{ .many = .bool } },
- .ret = .bool,
- };
- pub const description =
- \\Computes logical `or` between the receiver value and any other value passed as argument.
- \\
- ;
- pub const examples =
- \\$page.draft.or($site.tags.len().eq(0))
- ;
- pub fn call(
- b: bool,
- _: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len == 0) return .{ .err = "'or' wants at least one argument" };
- for (args) |a| switch (a) {
- .bool => {},
- else => return .{ .err = "wrong argument type" },
- };
- if (b) return .{ .bool = true };
- for (args) |a| if (a.bool) return .{ .bool = true };
-
- return .{ .bool = false };
- }
-};
diff --git a/src/context/primitive_builtins/Dynamic.zig b/src/context/primitive_builtins/Dynamic.zig
deleted file mode 100644
index eba52a0..0000000
--- a/src/context/primitive_builtins/Dynamic.zig
+++ /dev/null
@@ -1,235 +0,0 @@
-const std = @import("std");
-const assert = std.debug.assert;
-const ziggy = @import("ziggy");
-const utils = @import("../utils.zig");
-const context = @import("../../context.zig");
-const DateTime = @import("../DateTime.zig");
-const Signature = @import("../docgen.zig").Signature;
-const Value = context.Value;
-const Allocator = std.mem.Allocator;
-
-pub const get = struct {
- pub const signature: Signature = .{ .params = &.{ .str, .dyn }, .ret = .dyn };
- pub const description =
- \\Tries to get a dynamic value, returns the second value on failure.
- \\
- ;
- pub const examples =
- \\$page.custom.get('coauthor', 'Loris Cro')
- ;
- pub fn call(
- dyn: ziggy.dynamic.Value,
- gpa: Allocator,
- args: []const Value,
- ) Value {
- _ = gpa;
- const bad_arg = .{ .err = "'get' wants two (string) arguments" };
- if (args.len != 2) return bad_arg;
-
- const path = switch (args[0]) {
- .string => |s| s,
- else => return bad_arg,
- };
-
- const default = args[1];
-
- if (dyn == .null) return default;
- if (dyn != .kv) return .{ .err = "get on a non-map dynamic value" };
-
- if (dyn.kv.fields.get(path)) |value| {
- switch (value) {
- .null => return default,
- .bool => |b| return .{ .bool = b },
- .integer => |i| return .{ .int = i },
- .bytes => |s| return .{ .string = s },
- .array => |a| return .{
- .iterator = .{
- .impl = .{
- .dynamic_it = .{ .items = a },
- },
- },
- },
- .tag => |t| {
- assert(std.mem.eql(u8, t.name, "date"));
- const date = DateTime.init(t.bytes) catch {
- return .{ .err = "error parsing date" };
- };
- return .{ .date = date };
- },
- inline else => |_, t| @panic("TODO: implement" ++ @tagName(t) ++ "support in dynamic data"),
- }
- }
-
- return default;
- }
-};
-
-pub const @"get!" = struct {
- pub const signature: Signature = .{ .params = &.{.str}, .ret = .dyn };
- pub const description =
- \\Tries to get a dynamic value, errors out if the value is not present.
- \\
- ;
- pub const examples =
- \\$page.custom.get!('coauthor')
- ;
- pub fn call(
- dyn: ziggy.dynamic.Value,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- const bad_arg = .{ .err = "'get' wants one (string) argument" };
- if (args.len != 1) return bad_arg;
-
- const path = switch (args[0]) {
- .string => |s| s,
- else => return bad_arg,
- };
-
- if (dyn != .kv) return .{ .err = "get on a non-map dynamic value" };
-
- const missing = try Value.errFmt(gpa, "missing value '{s}'", .{path});
-
- if (dyn.kv.fields.get(path)) |value| {
- switch (value) {
- .null => return missing,
- .bool,
- => |b| return .{ .bool = b },
- .integer => |i| return .{ .int = i },
- .bytes => |s| return .{ .string = s },
- .array => |a| return .{
- .iterator = .{
- .impl = .{
- .dynamic_it = .{ .items = a },
- },
- },
- },
- .tag => |t| {
- assert(std.mem.eql(u8, t.name, "date"));
- const date = DateTime.init(t.bytes) catch {
- return .{ .err = "error parsing date" };
- };
- return .{ .date = date };
- },
- .kv => return .{ .dynamic = value },
- inline else => |_, t| @panic("TODO: implement" ++ @tagName(t) ++ "support in dynamic data"),
- }
- }
-
- return missing;
- }
-};
-
-pub const @"get?" = struct {
- pub const signature: Signature = .{ .params = &.{.str}, .ret = .{ .opt = .dyn } };
- pub const description =
- \\Tries to get a dynamic value, to be used in conjuction with an `if` attribute.
- \\
- ;
- pub const examples =
- \\
- ;
- pub fn call(
- dyn: ziggy.dynamic.Value,
- gpa: Allocator,
- args: []const Value,
- ) Value {
- _ = gpa;
- const bad_arg = .{ .err = "'get?' wants 1 string argument" };
- if (args.len != 1) return bad_arg;
-
- const path = switch (args[0]) {
- .string => |s| s,
- else => return bad_arg,
- };
-
- if (dyn == .null) return .{ .optional = null };
- if (dyn != .kv) return .{ .err = "get? on a non-map dynamic value" };
-
- if (dyn.kv.fields.get(path)) |value| {
- switch (value) {
- .null => return .{ .optional = null },
- .bool => |b| return .{ .optional = .{ .bool = b } },
- .integer => |i| return .{ .optional = .{ .int = i } },
- .bytes => |s| return .{ .optional = .{ .string = s } },
- .kv => return .{ .optional = .{ .dynamic = value } },
-
- inline else => |_, t| @panic("TODO: implement" ++ @tagName(t) ++ "support in dynamic data"),
- }
- }
-
- return .{ .optional = null };
- }
-};
-pub const has = struct {
- pub const signature: Signature = .{ .params = &.{.str}, .ret = .bool };
- pub const description =
- \\Returns true if the map contains the provided key.
- \\
- ;
- pub const examples =
- \\Yep!
- ;
- pub fn call(
- dyn: ziggy.dynamic.Value,
- gpa: Allocator,
- args: []const Value,
- ) Value {
- _ = gpa;
- const bad_arg = .{ .err = "'get?' wants 1 string argument" };
- if (args.len != 1) return bad_arg;
-
- const path = switch (args[0]) {
- .string => |s| s,
- else => return bad_arg,
- };
-
- if (dyn == .null) return .{ .optional = null };
- if (dyn != .kv) return .{ .err = "has called on a non-map dynamic value" };
-
- return .{
- .bool = dyn.kv.fields.get(path) != null,
- };
- }
-};
-
-pub const iterate = struct {
- pub const signature: Signature = .{
- .parameters = &.{ .opt = .{.str} },
- .ret = .dyn,
- };
- pub const description =
- \\Iterates over key-value pairs of a Ziggy map.
- \\
- \\You can optionally pass a string that will be used to filter key names.
- ;
- pub const examples =
- \\$page.custom.iterate()
- ;
- pub fn call(
- dyn: ziggy.dynamic.Value,
- gpa: Allocator,
- args: []const Value,
- ) Value {
- _ = gpa;
- const bad_arg = .{ .err = "expected 0 or 1 string arguments" };
- if (args.len > 1) return bad_arg;
-
- if (dyn != .kv) return .{
- .err = "trying to iterate a non-map dynamic value",
- };
-
- const filter: ?[]const u8 = if (args.len == 0) null else switch (args[0]) {
- .string => |s| s,
- else => return bad_arg,
- };
-
- return .{
- .iterator = .{
- .impl = .{
- .map_it = context.MapIterator.init(dyn.kv.fields.iterator(), filter),
- },
- },
- };
- }
-};
diff --git a/src/context/primitive_builtins/Float.zig b/src/context/primitive_builtins/Float.zig
deleted file mode 100644
index e69de29..0000000
diff --git a/src/context/primitive_builtins/Int.zig b/src/context/primitive_builtins/Int.zig
deleted file mode 100644
index 3f55338..0000000
--- a/src/context/primitive_builtins/Int.zig
+++ /dev/null
@@ -1,158 +0,0 @@
-const std = @import("std");
-const utils = @import("../utils.zig");
-const Signature = @import("../docgen.zig").Signature;
-const Value = @import("../../context.zig").Value;
-const Allocator = std.mem.Allocator;
-
-pub const eq = struct {
- pub const signature: Signature = .{
- .params = &.{.int},
- .ret = .bool,
- };
- pub const description =
- \\Tests if two integers have the same value.
- \\
- ;
- pub const examples =
- \\$page.wordCount().eq(200)
- ;
- pub fn call(
- num: i64,
- _: Allocator,
- args: []const Value,
- ) !Value {
- const argument_error = .{ .err = "'plus' wants one int argument" };
- if (args.len != 1) return argument_error;
-
- switch (args[0]) {
- .int => |rhs| {
- return .{ .bool = num == rhs };
- },
- else => return argument_error,
- }
- }
-};
-pub const gt = struct {
- pub const signature: Signature = .{
- .params = &.{.int},
- .ret = .bool,
- };
- pub const description =
- \\Returns true if lhs is greater than rhs (the argument).
- \\
- ;
- pub const examples =
- \\$page.wordCount().gt(200)
- ;
- pub fn call(
- num: i64,
- _: Allocator,
- args: []const Value,
- ) !Value {
- const argument_error = .{ .err = "'gt' wants one int argument" };
- if (args.len != 1) return argument_error;
-
- switch (args[0]) {
- .int => |rhs| {
- return .{ .bool = num > rhs };
- },
- else => return argument_error,
- }
- }
-};
-
-pub const plus = struct {
- pub const signature: Signature = .{
- .params = &.{.int},
- .ret = .int,
- };
- pub const description =
- \\Sums two integers.
- \\
- ;
- pub const examples =
- \\$page.wordCount().plus(10)
- ;
- pub fn call(
- num: i64,
- _: Allocator,
- args: []const Value,
- ) !Value {
- const argument_error = .{ .err = "'plus' wants one (int|float) argument" };
- if (args.len != 1) return argument_error;
-
- switch (args[0]) {
- .int => |add| {
- return .{ .int = num +| add };
- },
- .float => @panic("TODO: int with float argument"),
- else => return argument_error,
- }
- }
-};
-pub const div = struct {
- pub const signature: Signature = .{
- .params = &.{.int},
- .ret = .int,
- };
- pub const description =
- \\Divides the receiver by the argument.
- \\
- ;
- pub const examples =
- \\$page.wordCount().div(10)
- ;
- pub fn call(
- num: i64,
- _: Allocator,
- args: []const Value,
- ) !Value {
- const argument_error = .{ .err = "'div' wants one (int|float) argument" };
- if (args.len != 1) return argument_error;
-
- switch (args[0]) {
- .int => |den| {
- const res = std.math.divTrunc(i64, num, den) catch |err| {
- return .{ .err = @errorName(err) };
- };
-
- return .{ .int = res };
- },
- .float => @panic("TODO: div with float argument"),
- else => return argument_error,
- }
- }
-};
-
-pub const byteSize = struct {
- pub const signature: Signature = .{
- .ret = .string,
- };
- pub const description =
- \\Turns a raw number of bytes into a human readable string that
- \\appropriately uses Kilo, Mega, Giga, etc.
- \\
- ;
- pub const examples =
- \\$page.asset('photo.jpg').size().byteSize()
- ;
- pub fn call(
- num: i64,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len != 0) return .{ .err = "expected 0 arguments" };
-
- const size: usize = if (num > 0) @intCast(num) else return Value.errFmt(
- gpa,
- "cannot represent {} (a negative value) as a size",
- .{num},
- );
-
- return .{
- .string = try std.fmt.allocPrint(gpa, "{:.0}", .{
- std.fmt.fmtIntSizeBin(size),
- }),
- };
- }
-};
diff --git a/src/context/primitive_builtins/String.zig b/src/context/primitive_builtins/String.zig
deleted file mode 100644
index 67f0242..0000000
--- a/src/context/primitive_builtins/String.zig
+++ /dev/null
@@ -1,406 +0,0 @@
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-const hl = @import("../../highlight.zig");
-const utils = @import("../utils.zig");
-const log = utils.log;
-const Signature = @import("../docgen.zig").Signature;
-const Value = @import("../../context.zig").Value;
-
-pub const len = struct {
- pub const signature: Signature = .{ .ret = .int };
- pub const description =
- \\Returns the length of a string.
- \\
- ;
- pub const examples =
- \\$page.title.len()
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len != 0) return .{ .err = "expected 0 arguments" };
- return Value.from(gpa, str.len);
- }
-};
-
-pub const contains = struct {
- pub const signature: Signature = .{
- .params = &.{.str},
- .ret = .bool,
- };
- pub const description =
- \\Returns true if the receiver contains the provided string.
- \\
- ;
- pub const examples =
- \\$page.permalink().contains("/blog/")
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- _ = gpa;
- const bad_arg = .{
- .err = "expected 1 string argument",
- };
- if (args.len != 1) return bad_arg;
-
- const needle = switch (args[0]) {
- .string => |s| s,
- else => return bad_arg,
- };
-
- return .{ .bool = std.mem.indexOf(u8, str, needle) != null };
- }
-};
-
-pub const endsWith = struct {
- pub const signature: Signature = .{
- .params = &.{.str},
- .ret = .bool,
- };
- pub const description =
- \\Returns true if the receiver ends with the provided string.
- \\
- ;
- pub const examples =
- \\$page.permalink().endsWith("/blog/")
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- _ = gpa;
- const bad_arg = .{
- .err = "expected 1 string argument",
- };
- if (args.len != 1) return bad_arg;
-
- const needle = switch (args[0]) {
- .string => |s| s,
- else => return bad_arg,
- };
-
- const result = std.mem.endsWith(u8, str, needle);
- log.debug("endsWith('{s}', '{s}') = {}", .{ str, needle, result });
-
- return .{ .bool = result };
- }
-};
-pub const eql = struct {
- pub const signature: Signature = .{
- .params = &.{.str},
- .ret = .bool,
- };
- pub const description =
- \\Returns true if the receiver equals the provided string.
- \\
- ;
- pub const examples =
- \\$page.author.eql("Loris Cro")
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- _ = gpa;
- const bad_arg = .{
- .err = "expected 1 string argument",
- };
- if (args.len != 1) return bad_arg;
- const needle = switch (args[0]) {
- .string => |s| s,
- else => return bad_arg,
- };
-
- return .{ .bool = std.mem.eql(u8, str, needle) };
- }
-};
-
-pub const basename = struct {
- pub const signature: Signature = .{
- .ret = .str,
- };
- pub const description =
- \\Returns the last component of a path.
- ;
- pub const examples =
- \\$page.permalink().contains("/blog/")
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- _ = gpa;
- if (args.len != 0) return .{ .err = "expected 0 arguments" };
-
- return .{ .string = std.fs.path.basename(str) };
- }
-};
-pub const suffix = struct {
- pub const signature: Signature = .{
- .params = &.{ .str, .{ .many = .str } },
- .ret = .str,
- };
- pub const description =
- \\Concatenates strings together (left-to-right).
- \\
- ;
- pub const examples =
- \\$page.title.suffix("Foo","Bar", "Baz")
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len == 0) return .{ .err = "'suffix' wants at least one argument" };
- var out = std.ArrayList(u8).init(gpa);
- errdefer out.deinit();
-
- try out.appendSlice(str);
- for (args) |a| {
- const fx = switch (a) {
- .string => |s| s,
- else => return .{ .err = "'suffix' arguments must be strings" },
- };
-
- try out.appendSlice(fx);
- }
-
- return .{ .string = try out.toOwnedSlice() };
- }
-};
-pub const fmt = struct {
- pub const signature: Signature = .{
- .params = &.{ .str, .{ .many = .str } },
- .ret = .str,
- };
- pub const description =
- \\Looks for '{}' placeholders in the receiver string and
- \\replaces them with the provided arguments.
- \\
- ;
- pub const examples =
- \\$i18n.get!("welcome-message").fmt($page.custom.get!("name"))
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len == 0) return .{ .err = "'fmt' wants at least one argument" };
- var out = std.ArrayList(u8).init(gpa);
- errdefer out.deinit();
-
- var it = std.mem.splitSequence(u8, str, "{}");
- for (args) |a| {
- const str_arg = switch (a) {
- .string => |s| s,
- else => return .{ .err = "'path' arguments must be strings" },
- };
- const before = it.next() orelse {
- return .{ .err = "fmt: more args than placeholders" };
- };
-
- try out.appendSlice(before);
- try out.appendSlice(str_arg);
- }
-
- const last = it.next() orelse {
- return .{ .err = "fmt: more args than placeholders" };
- };
-
- try out.appendSlice(last);
-
- if (it.next() != null) {
- return .{ .err = "fmt: more placeholders than args" };
- }
-
- return .{ .string = try out.toOwnedSlice() };
- }
-};
-
-pub const addPath = struct {
- pub const signature: Signature = .{
- .params = &.{ .str, .{ .many = .str } },
- .ret = .str,
- };
- pub const description =
- \\Joins URL path segments automatically adding `/` as needed.
- ;
- pub const examples =
- \\$site.host_url.addPath("rss.xml")
- \\$site.host_url.addPath("foo/bar", "/baz")
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len == 0) return .{ .err = "'path' wants at least one argument" };
- var out = std.ArrayList(u8).init(gpa);
- errdefer out.deinit();
-
- try out.appendSlice(str);
- if (!std.mem.endsWith(u8, str, "/")) {
- try out.append('/');
- }
-
- for (args) |a| {
- const fx = switch (a) {
- .string => |s| s,
- else => return .{ .err = "'path' arguments must be strings" },
- };
-
- if (fx.len == 0) continue;
- if (fx[0] == '/') {
- try out.appendSlice(fx[1..]);
- } else {
- try out.appendSlice(fx);
- }
- }
-
- return .{ .string = try out.toOwnedSlice() };
- }
-};
-pub const syntaxHighlight = struct {
- pub const signature: Signature = .{
- .params = &.{.str},
- .ret = .str,
- };
- pub const description =
- \\Applies syntax highlighting to a string.
- \\The argument specifies the language name.
- \\
- ;
- pub const examples =
- \\
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len != 1) return .{ .err = "'syntaxHighlight' wants one argument" };
- var out = std.ArrayList(u8).init(gpa);
- errdefer out.deinit();
-
- const lang = switch (args[0]) {
- .string => |s| s,
- else => return .{ .err = "the argument to 'syntaxHighlight' must be of type string" },
- };
-
- // _ = lang;
- // _ = str;
- hl.highlightCode(gpa, lang, str, out.writer()) catch |err| switch (err) {
- error.NoLanguage => return .{ .err = "unable to find a parser for the provided language" },
- error.OutOfMemory => return error.OutOfMemory,
- else => return .{ .err = "error while syntax highlighting" },
- };
-
- return .{ .string = try out.toOwnedSlice() };
- }
-};
-
-pub const parseInt = struct {
- pub const signature: Signature = .{
- .ret = .int,
- };
- pub const description =
- \\Parses an integer out of a string
- \\
- ;
- pub const examples =
- \\$page.custom.get!('not-a-num-for-some-reason').parseInt()
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len != 0) return .{ .err = "expected 0 arguments" };
-
- const parsed = std.fmt.parseInt(i64, str, 10) catch |err| {
- return Value.errFmt(gpa, "error parsing int from '{s}': {s}", .{
- str, @errorName(err),
- });
- };
-
- return .{ .int = parsed };
- }
-};
-
-pub const splitN = struct {
- pub const signature: Signature = .{
- .parameters = &.{ .str, .int },
- .ret = .str,
- };
- pub const description =
- \\Splits the string using the first string argument as delimiter and then
- \\returns the Nth substring (where N is the second argument).
- \\
- \\Indices start from 0.
- \\
- ;
- pub const examples =
- \\$page.author.splitN(" ", 1)
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- _ = gpa;
- if (args.len != 2) return .{ .err = "expected 2 (string, int) arguments" };
-
- const split = switch (args[0]) {
- .string => |s| s,
- else => return .{ .err = "the first argument must be a string" },
- };
-
- const n: usize = switch (args[1]) {
- .int => |i| if (i >= 0) @intCast(i) else return .{
- .err = "the second argument must be non-negative",
- },
- else => return .{ .err = "the second argument must be an integer" },
- };
-
- var it = std.mem.splitSequence(u8, str, split);
- const too_short: Value = .{ .err = "sequence ended too early" };
- for (0..n) |_| _ = it.next() orelse return too_short;
-
- const result = it.next() orelse return too_short;
- return .{ .string = result };
- }
-};
-pub const lower = struct {
- pub const signature: Signature = .{
- .ret = .str,
- };
- pub const description =
- \\Returns a lowercase version of the target string.
- \\
- ;
- pub const examples =
- \\$page.title.lower()
- ;
- pub fn call(
- str: []const u8,
- gpa: Allocator,
- args: []const Value,
- ) !Value {
- if (args.len != 0) return .{ .err = "expected 0 arguments" };
-
- const l = try gpa.dupe(u8, str);
- for (l) |*ch| ch.* = std.ascii.toLower(ch.*);
-
- return .{ .string = l };
- }
-};
diff --git a/src/exes/docgen.zig b/src/exes/docgen.zig
index 9fe0f9f..5fab2dc 100644
--- a/src/exes/docgen.zig
+++ b/src/exes/docgen.zig
@@ -2,6 +2,13 @@ const std = @import("std");
const zine = @import("zine");
const context = zine.context;
const Value = context.Value;
+const Template = context.Template;
+const Param = context.ScriptyParam;
+
+const ref: Reference = .{
+ .global = analyzeFields(Template),
+ .values = analyzeValues(),
+};
pub fn main() !void {
var gpa_impl: std.heap.GeneralPurposeAllocator(.{}) = .{};
@@ -26,133 +33,211 @@ pub fn main() !void {
try w.writeAll(
\\---
- \\{
- \\ .title = "Scripty Reference",
- \\ .description = "",
- \\ .author = "Loris Cro",
- \\ .layout = "scripty-reference.shtml",
- \\ .date = @date("2023-06-16T00:00:00"),
- \\ .draft = false,
- \\}
+ \\.title = "SuperHTML Scripty Reference",
+ \\.description = "",
+ \\.author = "Loris Cro",
+ \\.layout = "scripty-reference.shtml",
+ \\.date = @date("2023-06-16T00:00:00"),
+ \\.draft = false,
\\---
\\
);
+ try w.print("{}", .{ref});
+ try buf_writer.flush();
+}
- // Globals
- {
- try w.writeAll(
- \\# Globals
- \\
- );
-
- const globals = .{
- .{ .name = "$site", .type_name = "Site", .desc = context.Site.description },
- .{ .name = "$page", .type_name = "Page", .desc = context.Page.description },
- .{
- .name = "$loop",
- .type_name = "?Loop",
- .desc =
- \\The iteration element in a loop, only available inside of elements with a `loop` attribute.
- ,
- },
- .{
- .name = "$if",
- .type_name = "?V",
- .desc =
- \\The payload of an optional value, only available inside of elemens with an `if` attribute.
- ,
- },
- };
+fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
+ std.debug.print(fmt, args);
+ std.process.exit(1);
+}
+
+fn oom() noreturn {
+ fatal("out of memory", .{});
+}
- inline for (globals) |g| {
- try w.print(
- \\## {s} : {s}
+pub const Reference = struct {
+ global: []const Field,
+ values: []const Type,
+
+ pub const Field = struct {
+ name: []const u8,
+ type_name: Param,
+ description: []const u8,
+ };
+
+ pub const Type = struct {
+ name: Param,
+ description: []const u8,
+ fields: []const Field,
+ builtins: []const Builtin,
+ };
+
+ pub const Builtin = struct {
+ name: []const u8,
+ signature: context.Signature,
+ description: []const u8,
+ examples: []const u8,
+ };
+
+ pub fn format(
+ r: Reference,
+ comptime fmt: []const u8,
+ options: std.fmt.FormatOptions,
+ out_stream: anytype,
+ ) !void {
+ _ = fmt;
+ _ = options;
+
+ try out_stream.print("# [Global Scope]($block.id('global'))\n\n", .{});
+ for (r.global) |f| {
+ try out_stream.print(
+ \\## `${s}` : {s}
\\
\\{s}
\\
- , .{ g.name, g.type_name, g.desc });
+ \\
+ , .{
+ f.name,
+ f.type_name.link(false),
+ f.description,
+ });
}
- }
-
- // Types
- {
- try w.writeAll(
- \\# Types
- \\
- );
- const types = .{
- .{ .name = "Site", .t = context.Site, .builtins = Value.builtinsFor(.site) },
- .{ .name = "Page", .t = context.Page, .builtins = Value.builtinsFor(.page) },
- .{ .name = "Alternative", .t = context.Page.Alternative, .builtins = Value.builtinsFor(.alternative) },
- .{ .name = "Translation", .t = context.Page.Translation, .builtins = Value.builtinsFor(.translation) },
- .{ .name = "str", .builtins = Value.builtinsFor(.string) },
- .{ .name = "date", .builtins = Value.builtinsFor(.date) },
- .{ .name = "int", .builtins = Value.builtinsFor(.int) },
- .{ .name = "bool", .builtins = Value.builtinsFor(.bool) },
- .{ .name = "dyn", .builtins = Value.builtinsFor(.dynamic) },
- };
- inline for (types) |t| {
- try w.print(
- \\## {s}
+ for (r.values[1..]) |v| {
+ try out_stream.print(
+ \\# [{s}]($block.id('{s}'))
+ \\
+ \\{s}
+ \\
\\
- , .{t.name});
- if (@hasField(@TypeOf(t), "t")) {
- inline for (@typeInfo(t.t).Struct.fields) |f| {
- if (f.name[0] != '_') {
- try w.print("### {s} : {s}", .{ f.name, context.ScriptyParam.fromType(f.type).name(false) });
-
- if (f.default_value) |d| {
- const v: *const f.type = @alignCast(@ptrCast(d));
- switch (f.type) {
- []const u8 => try w.print(" = \"{s}\"", .{v.*}),
- []const []const u8 => try w.print(" = []", .{}),
- std.json.Value => try w.print(" = null", .{}),
- else => try w.print(" = {any}", .{v.*}),
- }
- }
- try w.writeAll(",\n ");
- }
- }
+ , .{ v.name.string(false), v.name.id(), v.description });
+
+ if (v.fields.len > 0)
+ try out_stream.print("## Fields\n\n", .{});
+
+ for (v.fields) |f| {
+ try out_stream.print(
+ \\### `{s}` : {s}
+ \\
+ \\{s}
+ \\
+ \\
+ , .{ f.name, f.type_name.link(false), f.description });
}
- inline for (@typeInfo(t.builtins).Struct.decls) |d| {
- try w.print("### {s}", .{d.name});
- const decl = @field(t.builtins, d.name);
- try printSignature(w, decl.signature);
- try w.print(
+ if (v.builtins.len > 0)
+ try out_stream.print("## Functions\n\n", .{});
+
+ for (v.builtins) |b| {
+ try out_stream.print(
+ \\### []($heading.id("{s}.{s}")) [`fn`]($link.ref("{s}.{s}")) {s} {s}
\\
\\{s}
\\
- \\Examples:
- \\```
+ \\#### Examples
+ \\
+ \\```superhtml
\\{s}
- \\```
+ \\```
\\
- , .{ decl.description, decl.examples });
+ , .{
+ // Type.Function
+ v.name.string(false),
+ b.name,
+
+ // Type.Function
+ v.name.string(false),
+ b.name,
+
+ b.name,
+ b.signature,
+ b.description,
+ b.examples,
+ });
}
}
}
- try buf_writer.flush();
+};
+
+pub fn analyzeValues() []const Reference.Type {
+ const info = @typeInfo(context.Value).Union;
+ var values: [info.fields.len]Reference.Type = undefined;
+ inline for (info.fields, &values) |f, *v| {
+ const t = getStructType(f.type) orelse {
+ std.debug.assert(f.type == []const u8);
+ v.* = .{
+ .name = .err,
+ .fields = &.{},
+ .builtins = &.{},
+ .description =
+ \\A Scripty error.
+ \\
+ \\In Scripty all errors are unrecoverable.
+ \\When available, you can use `?` variants
+ \\of functions (e.g. `get?`) to obtain a null
+ \\value instead of an error.
+ ,
+ };
+ continue;
+ };
+ v.* = analyzeType(t);
+ }
+ const out = values;
+ return &out;
+}
+pub fn analyzeType(T: type) Reference.Type {
+ const builtins = analyzeBuiltins(T);
+ const fields = analyzeFields(T);
+ return .{
+ .name = Param.fromType(T),
+ .description = T.description,
+ .fields = fields,
+ .builtins = builtins,
+ };
}
-fn printSignature(w: anytype, s: context.Signature) !void {
- try w.writeAll("(");
- for (s.params, 0..) |p, idx| {
- try w.writeAll(p.name(true));
- if (idx < s.params.len - 1) {
- try w.writeAll(", ");
- }
+fn getStructType(T: type) ?type {
+ switch (@typeInfo(T)) {
+ .Struct => return T,
+ .Pointer => |p| switch (p.size) {
+ .One => return getStructType(p.child),
+ else => return null,
+ },
+ .Optional => |opt| return getStructType(opt.child),
+ else => return null,
}
- try w.writeAll(") -> ");
- try w.writeAll(s.ret.name(false));
}
-fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
- std.debug.print(fmt, args);
- std.process.exit(1);
+fn analyzeBuiltins(T: type) []const Reference.Builtin {
+ const info = @typeInfo(T.Builtins).Struct;
+ var decls: [info.decls.len]Reference.Builtin = undefined;
+ inline for (info.decls, &decls) |decl, *b| {
+ const t = @field(T.Builtins, decl.name);
+ b.* = .{
+ .name = decl.name,
+ .signature = t.signature,
+ .description = t.description,
+ .examples = t.examples,
+ };
+ }
+ const out = decls;
+ return &out;
}
-fn oom() noreturn {
- fatal("out of memory", .{});
+fn analyzeFields(T: type) []const Reference.Field {
+ const info = @typeInfo(T).Struct;
+ var reference_fields: [info.fields.len]Reference.Field = undefined;
+ var idx: usize = 0;
+ for (info.fields) |tf| {
+ if (!@hasDecl(T, "Fields")) continue;
+ if (tf.name[0] == '_') continue;
+ reference_fields[idx] = .{
+ .name = tf.name,
+ .description = @field(T.Fields, tf.name),
+ .type_name = Param.fromType(tf.type),
+ };
+ idx += 1;
+ }
+ const out = reference_fields[0..idx].*;
+ return &out;
}
diff --git a/src/exes/docgen1.zig b/src/exes/docgen1.zig
deleted file mode 100644
index 3ced97c..0000000
--- a/src/exes/docgen1.zig
+++ /dev/null
@@ -1,80 +0,0 @@
-const std = @import("std");
-const zine = @import("zine");
-const context = zine.context;
-const Value = context.Value;
-
-pub const Reference = struct {
- globals: []const Field,
- primitives: []const Primitive,
-
- pub const Field = struct {
- name: []const u8,
- type: Type,
- };
-
- pub const Type = union(enum) {
- primitive: Primitive,
- @"struct": Struct,
- };
-
- pub const Primitive = struct {
- name: []const u8,
- builtins: []const Builtin = &.{},
- };
-
- pub const Struct = struct {
- name: []const u8,
- description: []const u8,
- fields: []const Field = &.{},
- builtins: []const Builtin = &.{},
- optional: bool = false,
- };
-
- pub const Builtin = struct {
- name: []const u8,
- signature: context.Signature,
- doc: []const u8,
- examples: []const u8,
- };
-};
-
-pub fn main() !void {
- var gpa_impl: std.heap.GeneralPurposeAllocator(.{}) = .{};
- var arena_impl = std.heap.ArenaAllocator.init(gpa_impl.allocator());
- defer arena_impl.deinit();
-
- const arena = arena_impl.allocator();
-
- const args = std.process.argsAlloc(arena) catch oom();
- const out_path = args[1];
-
- const out_file = std.fs.cwd().createFile(out_path, .{}) catch |err| {
- fatal("error while creating output file: {s}\n{s}\n", .{
- out_path,
- @errorName(err),
- });
- };
- defer out_file.close();
-
- var buf_writer = std.io.bufferedWriter(out_file.writer());
- const w = buf_writer.writer();
-
- var ref: Reference = .{
- .globals = &.{
- .{
- .name = "$site",
- .type =
- },
- },
- .primitives = &.{},
- };
-}
-
-fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
- std.debug.print(fmt, args);
- std.process.exit(1);
-}
-
-fn oom() noreturn {
- fatal("out of memory", .{});
-}
diff --git a/src/exes/layout.zig b/src/exes/layout.zig
index 7486d57..7d19fcf 100644
--- a/src/exes/layout.zig
+++ b/src/exes/layout.zig
@@ -109,8 +109,8 @@ pub fn main() !void {
};
var locale_code: ?[]const u8 = null;
- const i18n: ziggy.dynamic.Value = blk: {
- if (std.mem.eql(u8, i18n_path, "null")) break :blk .null;
+ const i18n: ziggy.dynamic.Map(ziggy.dynamic.Value) = blk: {
+ if (std.mem.eql(u8, i18n_path, "null")) break :blk .{};
locale_code = std.fs.path.stem(i18n_path);
const bytes = readFile(build_root, i18n_path, arena) catch |err| {
@@ -124,7 +124,7 @@ pub fn main() !void {
.path = i18n_path,
};
- break :blk ziggy.parseLeaky(ziggy.dynamic.Value, arena, bytes, .{
+ break :blk ziggy.parseLeaky(ziggy.dynamic.Map(ziggy.dynamic.Value), arena, bytes, .{
.diagnostic = &diag,
}) catch {
std.debug.print("unable to load i18n file:\n{s}\n\n", .{
diff --git a/src/exes/layout/cache.zig b/src/exes/layout/cache.zig
index 3565589..d544bd1 100644
--- a/src/exes/layout/cache.zig
+++ b/src/exes/layout/cache.zig
@@ -307,7 +307,7 @@ const page_finder = struct {
},
};
- return .{ .optional = .{ .page = val } };
+ return context.Optional.init(gpa, val);
},
.subpages => |page| {
const path = page._meta.md_rel_path;
@@ -344,27 +344,23 @@ const page_finder = struct {
};
return .{
- .iterator = .{
- .impl = .{
- .page_it = context.PageIterator.init(
- page._meta.site,
- page._meta.md_asset_dir_rel_path,
- ps,
- ),
- },
- },
+ .iterator = try context.Iterator.init(gpa, .{
+ .page_it = context.Iterator.PageIterator.init(
+ page._meta.site,
+ page._meta.md_asset_dir_rel_path,
+ ps,
+ ),
+ }),
};
}
return .{
- .iterator = .{
- .impl = .{
- .page_it = context.PageIterator.init(
- page._meta.site,
- null,
- "",
- ),
- },
- },
+ .iterator = try context.Iterator.init(gpa, .{
+ .page_it = context.Iterator.PageIterator.init(
+ page._meta.site,
+ null,
+ "",
+ ),
+ }),
};
},
}
@@ -543,7 +539,7 @@ const asset_collector = struct {
&.{},
);
break :blk std.fs.path.join(gpa, &.{
- page_link.string,
+ page_link.string.value,
ref,
});
},
@@ -842,19 +838,24 @@ fn loadPage(
.{lines},
);
+ const tag_name = switch (err.kind) {
+ .html => |h| switch (h.tag) {
+ inline else => |t| @tagName(t),
+ },
+ else => @tagName(err.kind),
+ };
std.debug.print(
\\
\\[{s}] {s}
- \\({s}) {s}:{}:{}: {s}
+ \\{s}:{}:{}: {s}
\\ {s}
\\ {s}
\\
, .{
- @tagName(err.kind), msg,
- md_rel_path, md_path,
- fm_offset + range.start.row, range.start.col,
- lines_fmt, line_trim,
- highlight,
+ tag_name, msg,
+ md_path, fm_offset + range.start.row,
+ range.start.col, lines_fmt,
+ line_trim, highlight,
});
}
std.process.exit(1);
@@ -865,7 +866,7 @@ fn loadPage(
const directive = n.getDirective() orelse continue;
switch (directive.kind) {
- .block => {},
+ .block, .heading, .box => {},
.code => |code| {
const value = switch (code.src.?) {
else => unreachable,
@@ -893,6 +894,15 @@ fn loadPage(
std.math.maxInt(u32),
) catch @panic("i/o");
+ log.debug("dep: '{s}'", .{value.asset._meta.path});
+
+ dep_writer.print("{s} ", .{value.asset._meta.path}) catch {
+ std.debug.panic(
+ "error while writing to dep file file: '{s}'",
+ .{value.asset._meta.path},
+ );
+ };
+
if (code.language) |lang| {
var buf = std.ArrayList(u8).init(gpa);
@@ -923,6 +933,7 @@ fn loadPage(
inline else => |val, tag| {
const res = switch (val.src.?) {
.url => continue,
+ .self_page => context.String.init(""),
.page => |p| blk: {
const page_site = if (p.locale) |lc|
sites.get(lc) orelse @panic("TODO: report that a locale could not be found in a markdown link directive")
@@ -974,7 +985,7 @@ fn loadPage(
switch (res) {
else => unreachable,
.string => |s| {
- @field(directive.kind, @tagName(tag)).src = .{ .url = s };
+ @field(directive.kind, @tagName(tag)).src = .{ .url = s.value };
},
.asset => |a| {
const url = try asset_collector.collect(
diff --git a/src/render/html.zig b/src/render/html.zig
index fea74c5..921a0d6 100644
--- a/src/render/html.zig
+++ b/src/render/html.zig
@@ -12,33 +12,53 @@ const log = std.log.scoped(.layout);
pub fn html(
gpa: std.mem.Allocator,
ast: Ast,
- start_node: supermd.Node,
- // render the heading element when 'start' is a section
- heading: bool,
+ start: supermd.Node,
// path to the file, used in error messages
path: []const u8,
w: anytype,
) !void {
var it = Iter.init(ast.md.root);
- const start = if (heading)
- start_node
- else
- start_node.nextSibling() orelse start_node;
it.reset(start, .enter);
+
+ const full_page = start.n == ast.md.root.n;
+
+ var open_div = false;
var event: ?Iter.Event = .{ .node = start, .dir = .enter };
while (event) |ev| : (event = it.next()) {
const node = ev.node;
- const node_lvl = node.headingLevel();
const node_is_block = if (node.getDirective()) |d|
d.kind == .block
else
false;
- if (node_lvl > 0 and node_is_block and node.n != start_node.n) break;
+
+ if (!full_page and node_is_block and node.n != start.n) break;
switch (node.nodeType()) {
.DOCUMENT => {},
.BLOCK_QUOTE => switch (ev.dir) {
- .enter => try w.print("", .{}),
- .exit => try w.print("
", .{}),
+ .enter => {
+ const d = node.getDirective() orelse {
+ try w.print("", .{});
+ continue;
+ };
+
+ try w.print("", .{});
+ },
+ .exit => {
+ if (node.getDirective() == null) {
+ try w.print("", .{});
+ continue;
+ } else {
+ try w.print("
", .{});
+ }
+ },
},
.LIST => switch (ev.dir) {
.enter => try w.print("<{s}>", .{
@@ -69,22 +89,74 @@ pub fn html(
if (gp.listIsTight()) continue;
switch (ev.dir) {
- .enter => try w.print("", .{}),
- .exit => try w.print("
", .{}),
+ .enter => {
+ if (node.getDirective()) |d| {
+ if (open_div) {
+ try w.print("", .{});
+ }
+ open_div = true;
+ try w.print("", .{});
+ event = it.next();
+ event = it.next();
+ if (node.firstChild().?.nextSibling() == null) {
+ continue;
+ }
+ }
+
+ try w.print("
", .{});
+ },
+ .exit => {
+ if (node.getDirective() != null) {
+ if (node.firstChild().?.nextSibling() == null) {
+ continue;
+ }
+ }
+ try w.print("
", .{});
+ },
}
},
.HEADING => switch (ev.dir) {
.enter => {
- try w.print("
", .{});
+ if (node.getDirective()) |d| switch (d.kind) {
+ else => {},
+ .heading => {
+ try w.print("", .{});
+ continue;
+ },
+ .block => {
+ if (open_div) {
+ try w.print(" ", .{});
+ }
+ open_div = true;
+ try w.print("", .{});
+ },
+ };
+
+ try w.print("", .{node.headingLevel()});
},
.exit => try w.print("", .{node.headingLevel()}),
},
@@ -233,6 +305,9 @@ pub fn html(
},
}
}
+ if (open_div) {
+ try w.writeAll("
");
+ }
}
fn renderDirective(
@@ -246,7 +321,7 @@ fn renderDirective(
const node = ev.node;
const directive = node.getDirective() orelse return renderLink(ev, w);
switch (directive.kind) {
- .block => {},
+ .block, .heading, .box => {},
.image => |img| switch (ev.dir) {
.enter => {
if (img.caption != null) try w.print("", .{});
@@ -297,7 +372,11 @@ fn renderDirective(
for (attrs) |attr| try w.print("{s} ", .{attr});
try w.print("\"", .{});
}
- try w.print(" href=\"{s}\"", .{lnk.src.?.url});
+
+ try w.print(" href=\"{s}", .{lnk.src.?.url});
+ if (lnk.ref) |r| try w.print("#{s}", .{r});
+ try w.print("\"", .{});
+
if (lnk.target) |t| try w.print(" target=\"{s}\"", .{t});
try w.print(">", .{});
},