diff options
author | Martin Ashby <martin@ashbysoft.com> | 2023-08-22 10:49:17 +0100 |
---|---|---|
committer | Martin Ashby <martin@ashbysoft.com> | 2023-08-22 10:49:17 +0100 |
commit | 7ed589a06539fcc91ab779a1e6de0a995f8238b5 (patch) | |
tree | f23bd017d43af4714280aea2d152379370393214 /zig-comments | |
parent | 8f7f272d68b4197f17d1e76c97546017b8ebea90 (diff) | |
download | mfashby.net-7ed589a06539fcc91ab779a1e6de0a995f8238b5.tar.gz mfashby.net-7ed589a06539fcc91ab779a1e6de0a995f8238b5.tar.bz2 mfashby.net-7ed589a06539fcc91ab779a1e6de0a995f8238b5.tar.xz mfashby.net-7ed589a06539fcc91ab779a1e6de0a995f8238b5.zip |
Replace comments with zig version
Diffstat (limited to 'zig-comments')
-rw-r--r-- | zig-comments/build.zig | 81 | ||||
m--------- | zig-comments/lib/mustache-zig | 0 | ||||
m--------- | zig-comments/lib/zigwebserver | 0 | ||||
-rw-r--r-- | zig-comments/src/README.md | 0 | ||||
-rw-r--r-- | zig-comments/src/main.zig | 294 | ||||
-rw-r--r-- | zig-comments/src/migrations/0_init.sql | 2 | ||||
-rw-r--r-- | zig-comments/src/migrations/1_capcha.sql | 6 | ||||
-rw-r--r-- | zig-comments/src/pq.zig | 151 | ||||
-rw-r--r-- | zig-comments/src/templates/badrequest.html | 6 | ||||
-rw-r--r-- | zig-comments/src/templates/capchainvalid.html | 6 | ||||
-rw-r--r-- | zig-comments/src/templates/comments.html | 9 | ||||
-rw-r--r-- | zig-comments/src/templates/form.html | 11 | ||||
-rw-r--r-- | zig-comments/src/templates/notfound.html | 6 | ||||
-rw-r--r-- | zig-comments/src/templates/notification.txt | 5 |
14 files changed, 0 insertions, 577 deletions
diff --git a/zig-comments/build.zig b/zig-comments/build.zig deleted file mode 100644 index b7f3bcc..0000000 --- a/zig-comments/build.zig +++ /dev/null @@ -1,81 +0,0 @@ -const std = @import("std"); - -// Although this function looks imperative, note that its job is to -// declaratively construct a build graph that will be executed by an external -// runner. -pub fn build(b: *std.Build) void { - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. - const target = b.standardTargetOptions(.{}); - - // Standard optimization options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not - // set a preferred release mode, allowing the user to decide how to optimize. - const optimize = b.standardOptimizeOption(.{}); - - const exe = b.addExecutable(.{ - .name = "comments", - // In this case the main source file is merely a path, however, in more - // complicated build scripts, this could be a generated file. - .root_source_file = .{ .path = "src/main.zig" }, - .target = target, - .optimize = optimize, - }); - - const zws = b.addModule("zws", .{ .source_file = .{ .path = "lib/zigwebserver/src/zigwebserver.zig" } }); - exe.addModule("zws", zws); - - exe.linkLibC(); - exe.linkSystemLibrary("libpq"); - exe.addIncludePath(.{ .path = "/usr/include" }); - - const mustache = b.addModule("mustache", .{ .source_file = .{ .path = "lib/mustache-zig/src/mustache.zig" } }); - exe.addModule("mustache", mustache); - - // This declares intent for the executable to be installed into the - // standard location when the user invokes the "install" step (the default - // step when running `zig build`). - b.installArtifact(exe); - - // This *creates* a Run step in the build graph, to be executed when another - // step is evaluated that depends on it. The next line below will establish - // such a dependency. - const run_cmd = b.addRunArtifact(exe); - - // By making the run step depend on the install step, it will be run from the - // installation directory rather than directly from within the cache directory. - // This is not necessary, however, if the application depends on other installed - // files, this ensures they will be present and in the expected location. - run_cmd.step.dependOn(b.getInstallStep()); - - // This allows the user to pass arguments to the application in the build - // command itself, like this: `zig build run -- arg1 arg2 etc` - if (b.args) |args| { - run_cmd.addArgs(args); - } - - // This creates a build step. It will be visible in the `zig build --help` menu, - // and can be selected like this: `zig build run` - // This will evaluate the `run` step rather than the default, which is "install". - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); - - // Creates a step for unit testing. This only builds the test executable - // but does not run it. - const unit_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/main.zig" }, - .target = target, - .optimize = optimize, - }); - unit_tests.addModule("mustache", mustache); - - const run_unit_tests = b.addRunArtifact(unit_tests); - - // Similar to creating the run step earlier, this exposes a `test` step to - // the `zig build --help` menu, providing a way for the user to request - // running the unit tests. - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_unit_tests.step); -} diff --git a/zig-comments/lib/mustache-zig b/zig-comments/lib/mustache-zig deleted file mode 160000 -Subproject a228c9e46d1e06c28e419c0656cfc43f553086a diff --git a/zig-comments/lib/zigwebserver b/zig-comments/lib/zigwebserver deleted file mode 160000 -Subproject 0736c8a26dbd970670117290baf4a68e430aeca diff --git a/zig-comments/src/README.md b/zig-comments/src/README.md deleted file mode 100644 index e69de29..0000000 --- a/zig-comments/src/README.md +++ /dev/null diff --git a/zig-comments/src/main.zig b/zig-comments/src/main.zig deleted file mode 100644 index 898b689..0000000 --- a/zig-comments/src/main.zig +++ /dev/null @@ -1,294 +0,0 @@ -const std = @import("std"); -const zws = @import("zws"); -const pq = @import("pq.zig"); -const mustache = @import("mustache"); - -const Params = zws.Params; - -const Err = error{ - Unexpected, - AccessDenied, - OutOfMemory, - InputOutput, - SystemResources, - IsDir, - OperationAborted, - BrokenPipe, - ConnectionResetByPeer, - ConnectionTimedOut, - NotOpenForReading, - NetNameDeleted, - WouldBlock, - StreamTooLong, - Malformatted, - InvalidLength, - InvalidCharacter, - NoSpaceLeft, - PqError, - ColumnNotFound, - DiskQuota, - FileTooBig, - DeviceBusy, - InvalidArgument, - NotOpenForWriting, - LockViolation, - InvalidRequestMethod, -}; -const Ctx = struct { - db: pq.Db, - pub fn clone(self: @This()) Ctx { - return Ctx{ - .db = self.db, - }; - } - pub fn deinit(self: @This()) void { - _ = self; - } -}; - -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; -const Request = struct { - method: std.http.Method, - target: []const u8, -}; -const ResponseTransfer = union(enum) { - content_length: u64, - chunked: void, - none: void, -}; -const Headers = struct { - const Entry = struct { key: []const u8, val: []const u8 }; - _internal: std.ArrayList(Entry), - - fn init(allocator: std.mem.Allocator) Headers { - return .{ ._internal = std.ArrayList(Entry).init(allocator) }; - } - fn append(self: *@This(), key: []const u8, val: []const u8) !void { - try self._internal.append(.{ .key = key, .val = val }); - } -}; -const Response = struct { - allocator: std.mem.Allocator, - request: Request, - // TODO other fields and writer function to write headers and body to stdout - status: std.http.Status, - transfer_encoding: ResponseTransfer, - headers: Headers, - fn reader(_: @This()) std.fs.File.Reader { - return std.io.getStdIn().reader(); - } - fn do(self: @This()) !void { - for (self.headers._internal.items) |tup| { - try std.io.getStdOut().writeAll(tup.key); - try std.io.getStdOut().writeAll(": "); - try std.io.getStdOut().writeAll(tup.val); - try std.io.getStdOut().writeAll("\r\n"); - } - try std.io.getStdOut().writeAll("\r\n"); - } - fn writer(_: @This()) std.fs.File.Writer { - return std.io.getStdOut().writer(); - } - fn finish(_: @This()) !void { - // TODO Write empty lines? or just do nothing - } -}; - -const Rtr = zws.Router(*Response, Ctx, Err); -const router = Rtr{ - .allocator = gpa.allocator(), - .handlers = &[_]Rtr.Handler{ - .{ - .method = .GET, - .pattern = "/api/comment", - .handle_fn = get_comment, - }, - .{ - .method = .POST, - .pattern = "/api/comment", - .handle_fn = post_comment, - }, - .{ - .method = .GET, - .pattern = "/api/form", - .handle_fn = get_form, - }, - }, - .notfound = notfound, -}; - -/// Run as a CGI program! -pub fn main() !void { - const allocator = gpa.allocator(); - const db_url = std.os.getenv("DATABASE_URL") orelse "postgresql://comments@localhost/comments"; - var db = try pq.Db.init(db_url); - // try db.exec(@embedFile("migrations/0_init.sql")); - // try db.exec(@embedFile("migrations/1_capcha.sql")); - defer db.deinit(); - const req = Request{ - .method = std.meta.stringToEnum(std.http.Method, std.os.getenv("REQUEST_METHOD") orelse "GET") orelse { - return error.InvalidRequestMethod; - }, - .target = std.os.getenv("REQUEST_URI") orelse "/", - }; - var res = Response{ - .allocator = allocator, - .request = req, - .status = .bad_request, - .transfer_encoding = .none, - .headers = Headers.init(allocator), - }; - const ctx = Ctx{ .db = db }; - try router.handle(&res, ctx); -} - -fn notfound(res: *Response, _: Ctx) Err!void { - const rr = @embedFile("templates/notfound.html"); - try constresponse(res, rr, .not_found); -} - -fn badrequest(res: *Response, _: Ctx) Err!void { - const rr = @embedFile("templates/badrequest.html"); - try constresponse(res, rr, .bad_request); -} - -fn constresponse(res: *Response, rr: []const u8, status: std.http.Status) Err!void { - res.status = status; - res.transfer_encoding = .{ .content_length = rr.len }; - try res.headers.append("content-type", "text/html"); - try res.do(); - try res.writer().writeAll(rr); - try res.finish(); -} - -fn get_comment(res: *Response, ctx: Ctx, _: Params) Err!void { - var p = try zws.Path.parse(res.allocator, res.request.target); - defer p.deinit(); - const url: []const u8 = try p.get_query_param("url") orelse { - try badrequest(res, ctx); - return; - }; - - const Comment = struct { - author: []const u8, - comment: []const u8, - ts: []const u8, - }; - var comments = std.ArrayList(Comment).init(res.allocator); - var stmt = try ctx.db.prepare_statement(res.allocator, - \\ select author,comment,ts from comments where url = $1 order by ts - ); - defer stmt.deinit(); - try stmt.bind(0, url); - while (try stmt.step()) { - const cmt = try stmt.read_struct(Comment); - try comments.append(cmt); - } - - const rr = @embedFile("templates/comments.html"); - const tt = comptime mustache.parseComptime(rr, .{}, .{}); - res.transfer_encoding = .chunked; - try res.headers.append("content-type", "text/html"); - try res.do(); - - const data = struct { - comments: []const Comment, - }; - try mustache.render(tt, data{ - .comments = comments.items, - }, res.writer()); - try res.finish(); -} - -fn post_comment(res: *Response, ctx: Ctx, _: Params) Err!void { - var body_aa = std.ArrayList(u8).init(res.allocator); - try res.reader().readAllArrayList(&body_aa, 1_000_000); - var body = try body_aa.toOwnedSlice(); - var form = try zws.Form.parse(res.allocator, body); - const Form = struct { - url: []const u8, - capcha_id: []const u8, - author: []const u8, - comment: []const u8, - capcha_answer: []const u8, - }; - const form_val = form.form_to_struct(Form) catch { - try badrequest(res, ctx); - return; - }; - - // Validate the capcha - { - var stmt = try ctx.db.prepare_statement(res.allocator, "select answer from capchas where id = $1"); - defer stmt.deinit(); - try stmt.bind(0, form_val.capcha_id); - if (!try stmt.step()) { - std.log.err("missing capcha_id {s}", .{form_val.capcha_id}); - try badrequest(res, ctx); - return; - } - const ans = try stmt.read_column(0, []const u8); - if (!std.mem.eql(u8, ans, form_val.capcha_answer)) { - try constresponse(res, @embedFile("templates/capchainvalid.html"), std.http.Status.unauthorized); - return; - } - } - - // Add the comment... - { - var stmt = try ctx.db.prepare_statement(res.allocator, "insert into comments(url,author,comment) values($1, $2, $3)"); - defer stmt.deinit(); - try stmt.bind(0, form_val.url); - try stmt.bind(1, form_val.author); - try stmt.bind(2, form_val.comment); - _ = try stmt.step(); - } - - // And redirect! - res.transfer_encoding = .none; - res.status = .found; - try res.headers.append("location", form_val.url); - try res.do(); - try res.finish(); -} - -fn get_form(res: *Response, ctx: Ctx, _: Params) Err!void { - var p = try zws.Path.parse(res.allocator, res.request.target); - defer p.deinit(); - const url: []const u8 = try p.get_query_param("url") orelse { - try badrequest(res, ctx); - return; - }; - - var stmt = try ctx.db.prepare_statement(res.allocator, "select id, question from capchas order by random() limit 1"); - defer stmt.deinit(); - if (!try stmt.step()) { - std.log.err("no capcha!", .{}); - try badrequest(res, ctx); - return; - } - const Capcha = struct { - id: []const u8, - question: []const u8, - }; - const capcha = try stmt.read_struct(Capcha); - - const rr = @embedFile("templates/form.html"); - const tt = comptime mustache.parseComptime(rr, .{}, .{}); - - res.transfer_encoding = .chunked; - try res.headers.append("content-type", "text/html"); - try res.do(); - // For some reason, mustache.render won't work with anonymous struct - const data = struct { - capcha_id: []const u8, - capcha_question: []const u8, - url: []const u8, - }; - try mustache.render(tt, data{ - .capcha_id = capcha.id, - .capcha_question = capcha.question, - .url = url, - }, res.writer()); - try res.finish(); -} diff --git a/zig-comments/src/migrations/0_init.sql b/zig-comments/src/migrations/0_init.sql deleted file mode 100644 index a799784..0000000 --- a/zig-comments/src/migrations/0_init.sql +++ /dev/null @@ -1,2 +0,0 @@ -create table if not exists comments (url text not null, author text not null, comment text not null, ts timestamptz not null default now()); -create index if not exists idx_comments_url on comments(url);
\ No newline at end of file diff --git a/zig-comments/src/migrations/1_capcha.sql b/zig-comments/src/migrations/1_capcha.sql deleted file mode 100644 index bf43331..0000000 --- a/zig-comments/src/migrations/1_capcha.sql +++ /dev/null @@ -1,6 +0,0 @@ -create table if not exists capchas(question text not null, answer text not null, id uuid not null default gen_random_uuid()); -create unique index if not exists idx_capchas_id on capchas(id); -insert into capchas(question, answer, id) values ('What is 1 + 3?', '4', 'de850c99-906b-4ff8-b8e7-fce3bccd89bc') on conflict do nothing; -insert into capchas(question, answer, id) values ('If I have 3 apples and 4 pears, how many fruit do I have?', '7', 'c83e23a8-ec53-48d4-a468-a530dfa634b7') on conflict do nothing; -insert into capchas(question, answer, id) values ('What is 3 squared?', '9', '798bf299-4e3b-4f98-acac-f0c41201b613') on conflict do nothing; -insert into capchas(question, answer, id) values ('What is the meaning of life, the universe, and everything?', '42', '7025fcbf-ebe8-4679-809f-08d632591b03') on conflict do nothing; diff --git a/zig-comments/src/pq.zig b/zig-comments/src/pq.zig deleted file mode 100644 index a4fc382..0000000 --- a/zig-comments/src/pq.zig +++ /dev/null @@ -1,151 +0,0 @@ -const std = @import("std"); -const pq = @cImport( - @cInclude("libpq-fe.h"), -); - -pub const PqError = error{PqError}; - -// libpq wrapper -// later, this could be a pure-zig client implementation -pub const Db = struct { - c_conn: *pq.PGconn, - pub fn init(connect_url: [:0]const u8) !Db { - if (pq.PQisthreadsafe() == 0) { - std.log.err("PQisthreadsafe returned 0, can't use libpq in this program", .{}); - return PqError.PqError; - } - var maybe_conn: ?*pq.PGconn = pq.PQconnectdb(connect_url); - if (maybe_conn == null) { - std.log.err("PQconnectdb returned null", .{}); - return PqError.PqError; - } - if (pq.PQstatus(maybe_conn) == pq.CONNECTION_BAD) { - std.log.err("PQstatus returned CONNECTION_BAD: {s}", .{pq.PQerrorMessage(maybe_conn)}); - - return PqError.PqError; - } else if (pq.PQstatus(maybe_conn) != pq.CONNECTION_OK) { - std.log.err("PQstatus returned unknown status {}: {s}", .{ pq.PQstatus(maybe_conn), pq.PQerrorMessage(maybe_conn) }); - return PqError.PqError; - } - return Db{ - .c_conn = maybe_conn.?, - }; - } - - pub fn deinit(self: Db) void { - pq.PQfinish(self.c_conn); - } - - pub fn exec(self: Db, query: [:0]const u8) !void { - var res: ?*pq.PGresult = pq.PQexec(self.c_conn, query); - defer pq.PQclear(res); - var est: pq.ExecStatusType = pq.PQresultStatus(res); - if (est != pq.PGRES_COMMAND_OK) { - std.log.err("PQexec error code {} message {s}", .{ est, pq.PQerrorMessage(self.c_conn) }); - return PqError.PqError; - } - } - - pub fn prepare_statement(self: Db, allocator: std.mem.Allocator, query: [:0]const u8) PqError!Stmt { - return Stmt{ - .db = self, - .aa = std.heap.ArenaAllocator.init(allocator), - .query = query, - }; - } -}; - -pub const Stmt = struct { - const MAX_PARAMS = 128; - db: Db, - query: [:0]const u8, - aa: std.heap.ArenaAllocator, - - n_params: usize = 0, - param_values: [MAX_PARAMS][*c]const u8 = undefined, - did_exec: bool = false, - c_res: ?*pq.PGresult = null, - res_index: c_int = -1, - n_tuples: c_int = -1, - n_fields: c_int = -1, - - pub fn deinit(self: *Stmt) void { - self.aa.deinit(); - if (self.c_res != null) { - pq.PQclear(self.c_res); - } - } - - pub fn step(self: *Stmt) !bool { - if (!self.did_exec) { - self.did_exec = true; - self.c_res = pq.PQexecParams(self.db.c_conn, self.query, @as(c_int, @intCast(self.n_params)), null, &self.param_values, null, null, 0); - const rs = pq.PQresultStatus(self.c_res); - if (rs != pq.PGRES_TUPLES_OK and rs != pq.PGRES_SINGLE_TUPLE and rs != pq.PGRES_COMMAND_OK) { - std.log.err("PQresultStatus {} error: {s}", .{ rs, pq.PQerrorMessage(self.db.c_conn) }); - return PqError.PqError; - } - self.n_tuples = pq.PQntuples(self.c_res); - self.n_fields = pq.PQnfields(self.c_res); - } - self.res_index = self.res_index + 1; - return self.res_index < self.n_tuples; - } - - pub fn read_column(self: Stmt, idx: c_int, comptime T: type) !T { - const value_c: [*c]u8 = pq.PQgetvalue(self.c_res, self.res_index, idx); - const value: []const u8 = std.mem.sliceTo(value_c, 0); - return switch (@typeInfo(T)) { - .Int => std.fmt.parseInt(T, value, 10), - .Pointer => |ptrinfo| blk: { - if (ptrinfo.child != u8) { - @compileError("pointer type []const u8 only is supported in read_column"); - } - if (ptrinfo.size != .Slice) { - @compileError("pointer type []const u8 only is supported in read_column"); - } - break :blk value; - }, - else => @compileError("unhandled type " ++ @tagName(@typeInfo(T)) ++ " in read_column"), - }; - } - - pub fn read_columnN(self: Stmt, name: [:0]const u8, comptime T: type) !T { - const idx = pq.PQfnumber(self.c_res, name.ptr); - if (idx == -1) { - std.log.err("read_columnN ColumnNotFound [{s}]", .{name}); - return error.ColumnNotFound; - } - return read_column(self, idx, T); - } - - pub fn bind(self: *Stmt, idx: usize, t: anytype) !void { - const T = @TypeOf(t); - const value: [:0]const u8 = switch (@typeInfo(T)) { - .Pointer => try std.fmt.allocPrintZ(self.aa.allocator(), "{s}", .{t}), - .Int => try std.fmt.allocPrintZ(self.aa.allocator(), "{d}", .{t}), - else => @compileError("unhandled type " ++ @tagName(@typeInfo(T) ++ " in bind")), - }; - self.param_values[idx] = value.ptr; - self.n_params = @max(self.n_params, idx + 1); - } - - pub fn read_struct(self: Stmt, comptime T: type) !T { - const ti = @typeInfo(T); - var t: T = undefined; - inline for (ti.Struct.fields) |field| { - const name: [:0]const u8 = &addZ(field.name.len, field.name[0..].*); - const val = try self.read_columnN(name, field.type); - @field(t, field.name) = val; - } - return t; - } -}; - -// https://github.com/ziglang/zig/issues/16116 -pub fn addZ(comptime length: usize, value: [length]u8) [length:0]u8 { - var terminated_value: [length:0]u8 = undefined; - terminated_value[length] = 0; - @memcpy(&terminated_value, &value); - return terminated_value; -} diff --git a/zig-comments/src/templates/badrequest.html b/zig-comments/src/templates/badrequest.html deleted file mode 100644 index 927e908..0000000 --- a/zig-comments/src/templates/badrequest.html +++ /dev/null @@ -1,6 +0,0 @@ -<doctype HTML> -<html> -<body> - <p>Bad Request!</p> -</body> -</html>
\ No newline at end of file diff --git a/zig-comments/src/templates/capchainvalid.html b/zig-comments/src/templates/capchainvalid.html deleted file mode 100644 index f13899b..0000000 --- a/zig-comments/src/templates/capchainvalid.html +++ /dev/null @@ -1,6 +0,0 @@ -<doctype HTML> -<html> -<body> - <p>Capcha invalid!</p> -</body> -</html>
\ No newline at end of file diff --git a/zig-comments/src/templates/comments.html b/zig-comments/src/templates/comments.html deleted file mode 100644 index e2fc6da..0000000 --- a/zig-comments/src/templates/comments.html +++ /dev/null @@ -1,9 +0,0 @@ -<ul class="comments"> -{{#comments}} - <li class="comment"> - <span class="comment author">{{author}}</span> - <p class="comment content">{{comment}}</p> - <span class="comment timestamp">{{ts}}</span> - </li> -{{/comments}} -</ul> diff --git a/zig-comments/src/templates/form.html b/zig-comments/src/templates/form.html deleted file mode 100644 index e7a836f..0000000 --- a/zig-comments/src/templates/form.html +++ /dev/null @@ -1,11 +0,0 @@ -<form action="/api/comment" method="post"> - <input type="hidden" name="url" value="{{url}}"><br> - <input type="hidden" name="capcha_id" value="{{capcha_id}}"><br> - <label for="author">Name:</label><br> - <input type="text" id="author" name="author"><br> - <label for="comment">Comment:</label><br> - <input type="text" id="comment" name="comment"><br> - <label for="capcha">{{ capcha_question }}</label><br> - <input type="text" id="capcha" name="capcha_answer"><br> - <input type="submit" value="Submit"> -</form> diff --git a/zig-comments/src/templates/notfound.html b/zig-comments/src/templates/notfound.html deleted file mode 100644 index 795d262..0000000 --- a/zig-comments/src/templates/notfound.html +++ /dev/null @@ -1,6 +0,0 @@ -<!doctype HTML> -<html> -<body> - <p>Not found!</p> -</body> -</html>
\ No newline at end of file diff --git a/zig-comments/src/templates/notification.txt b/zig-comments/src/templates/notification.txt deleted file mode 100644 index ba85056..0000000 --- a/zig-comments/src/templates/notification.txt +++ /dev/null @@ -1,5 +0,0 @@ -New comment on {{ url }}: - -Author: {{ author }} - -Comment: {{ comment }}
\ No newline at end of file |