From 2240f006e3514e3aa8bf048e3e61e31fe22749c3 Mon Sep 17 00:00:00 2001 From: Loris Cro Date: Sun, 25 Aug 2024 23:39:08 +0200 Subject: [PATCH] wip --- build.zig | 41 +- build.zig.zon | 6 +- build/content.zig | 39 +- build/tools.zig | 30 +- frontmatter.ziggy-schema | 43 +- src/context.zig | 553 ++++----------------- src/context/Asset.zig | 37 +- src/context/Bool.zig | 134 +++++ src/context/Build.zig | 14 +- src/context/DateTime.zig | 46 +- src/context/Float.zig | 7 + src/context/Int.zig | 163 ++++++ src/context/Iterator.zig | 304 +++++++++++ src/context/Map.zig | 214 ++++++++ src/context/Optional.zig | 25 + src/context/Page.zig | 309 +++++++----- src/context/Site.zig | 54 +- src/context/Slice.zig | 17 + src/context/String.zig | 413 +++++++++++++++ src/context/Template.zig | 57 ++- src/context/docgen.zig | 69 --- src/context/doctypes.zig | 146 ++++++ src/context/primitive_builtins/Bool.zig | 113 ----- src/context/primitive_builtins/Dynamic.zig | 235 --------- src/context/primitive_builtins/Float.zig | 0 src/context/primitive_builtins/Int.zig | 158 ------ src/context/primitive_builtins/String.zig | 406 --------------- src/exes/docgen.zig | 287 +++++++---- src/exes/docgen1.zig | 80 --- src/exes/layout.zig | 6 +- src/exes/layout/cache.zig | 67 +-- src/render/html.zig | 129 ++++- 32 files changed, 2286 insertions(+), 1916 deletions(-) create mode 100644 src/context/Bool.zig create mode 100644 src/context/Float.zig create mode 100644 src/context/Int.zig create mode 100644 src/context/Iterator.zig create mode 100644 src/context/Map.zig create mode 100644 src/context/Optional.zig create mode 100644 src/context/Slice.zig create mode 100644 src/context/String.zig delete mode 100644 src/context/docgen.zig create mode 100644 src/context/doctypes.zig delete mode 100644 src/context/primitive_builtins/Bool.zig delete mode 100644 src/context/primitive_builtins/Dynamic.zig delete mode 100644 src/context/primitive_builtins/Float.zig delete mode 100644 src/context/primitive_builtins/Int.zig delete mode 100644 src/context/primitive_builtins/String.zig delete mode 100644 src/exes/docgen1.zig 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(">", .{}); },