mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
merge main, resolve comment conflicts in Input.zig
This commit is contained in:
@@ -182,9 +182,14 @@ pub const Serve = struct {
|
||||
common: Common = .{},
|
||||
};
|
||||
|
||||
pub const DumpFormat = enum {
|
||||
html,
|
||||
markdown,
|
||||
};
|
||||
|
||||
pub const Fetch = struct {
|
||||
url: [:0]const u8,
|
||||
dump: bool = false,
|
||||
dump_mode: ?DumpFormat = null,
|
||||
common: Common = .{},
|
||||
withbase: bool = false,
|
||||
strip: dump.Opts.Strip = .{},
|
||||
@@ -321,11 +326,12 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\
|
||||
\\fetch command
|
||||
\\Fetches the specified URL
|
||||
\\Example: {s} fetch --dump https://lightpanda.io/
|
||||
\\Example: {s} fetch --dump html https://lightpanda.io/
|
||||
\\
|
||||
\\Options:
|
||||
\\--dump Dumps document to stdout.
|
||||
\\ Defaults to false.
|
||||
\\ Argument must be 'html' or 'markdown'.
|
||||
\\ Defaults to no dump.
|
||||
\\
|
||||
\\--strip_mode Comma separated list of tag groups to remove from dump
|
||||
\\ the dump. e.g. --strip_mode js,css
|
||||
@@ -532,7 +538,7 @@ fn parseFetchArgs(
|
||||
allocator: Allocator,
|
||||
args: *std.process.ArgIterator,
|
||||
) !Fetch {
|
||||
var fetch_dump: bool = false;
|
||||
var dump_mode: ?DumpFormat = null;
|
||||
var withbase: bool = false;
|
||||
var url: ?[:0]const u8 = null;
|
||||
var common: Common = .{};
|
||||
@@ -540,7 +546,17 @@ fn parseFetchArgs(
|
||||
|
||||
while (args.next()) |opt| {
|
||||
if (std.mem.eql(u8, "--dump", opt)) {
|
||||
fetch_dump = true;
|
||||
var peek_args = args.*;
|
||||
if (peek_args.next()) |next_arg| {
|
||||
if (std.meta.stringToEnum(DumpFormat, next_arg)) |mode| {
|
||||
dump_mode = mode;
|
||||
_ = args.next();
|
||||
} else {
|
||||
dump_mode = .html;
|
||||
}
|
||||
} else {
|
||||
dump_mode = .html;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -607,7 +623,7 @@ fn parseFetchArgs(
|
||||
|
||||
return .{
|
||||
.url = url.?,
|
||||
.dump = fetch_dump,
|
||||
.dump_mode = dump_mode,
|
||||
.strip = strip,
|
||||
.common = common,
|
||||
.withbase = withbase,
|
||||
|
||||
@@ -262,6 +262,8 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
// and default actions execute (unless prevented)
|
||||
defer {
|
||||
event._event_phase = .none;
|
||||
event._stop_propagation = false;
|
||||
event._stop_immediate_propagation = false;
|
||||
// Handle checkbox/radio activation rollback or commit
|
||||
if (activation_state) |state| {
|
||||
state.restore(event, page);
|
||||
@@ -322,19 +324,18 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
var i: usize = path_len;
|
||||
while (i > 1) {
|
||||
i -= 1;
|
||||
if (event._stop_propagation) return;
|
||||
const current_target = path[i];
|
||||
if (self.lookup.get(.{
|
||||
.event_target = @intFromPtr(current_target),
|
||||
.type_string = event._type_string,
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, was_handled, true);
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: At target
|
||||
if (event._stop_propagation) return;
|
||||
event._event_phase = .at_target;
|
||||
const target_et = target.asEventTarget();
|
||||
|
||||
@@ -375,14 +376,12 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
if (event._bubbles) {
|
||||
event._event_phase = .bubbling_phase;
|
||||
for (path[1..]) |current_target| {
|
||||
if (event._stop_propagation) break;
|
||||
if (self.lookup.get(.{
|
||||
.type_string = event._type_string,
|
||||
.event_target = @intFromPtr(current_target),
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, was_handled, false);
|
||||
if (event._stop_propagation) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
506
src/browser/markdown.zig
Normal file
506
src/browser/markdown.zig
Normal file
@@ -0,0 +1,506 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("Page.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
|
||||
pub const Opts = struct {
|
||||
// Options for future customization (e.g., dialect)
|
||||
};
|
||||
|
||||
const State = struct {
|
||||
const ListType = enum { ordered, unordered };
|
||||
const ListState = struct {
|
||||
type: ListType,
|
||||
index: usize,
|
||||
};
|
||||
|
||||
list_depth: usize = 0,
|
||||
list_stack: [32]ListState = undefined,
|
||||
in_pre: bool = false,
|
||||
pre_node: ?*Node = null,
|
||||
in_code: bool = false,
|
||||
in_table: bool = false,
|
||||
table_row_index: usize = 0,
|
||||
table_col_count: usize = 0,
|
||||
last_char_was_newline: bool = true,
|
||||
};
|
||||
|
||||
fn isBlock(tag: Element.Tag) bool {
|
||||
return switch (tag) {
|
||||
.p, .div, .section, .article, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .pre, .table, .hr => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn shouldAddSpacing(tag: Element.Tag) bool {
|
||||
return switch (tag) {
|
||||
.p, .h1, .h2, .h3, .h4, .h5, .h6, .blockquote, .pre, .table => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn ensureNewline(state: *State, writer: *std.Io.Writer) !void {
|
||||
if (!state.last_char_was_newline) {
|
||||
try writer.writeByte('\n');
|
||||
state.last_char_was_newline = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||
_ = opts;
|
||||
var state = State{};
|
||||
try render(node, &state, writer, page);
|
||||
if (!state.last_char_was_newline) {
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
||||
switch (node._type) {
|
||||
.document, .document_fragment => {
|
||||
try renderChildren(node, state, writer, page);
|
||||
},
|
||||
.element => |el| {
|
||||
try renderElement(el, state, writer, page);
|
||||
},
|
||||
.cdata => |cd| {
|
||||
if (node.is(Node.CData.Text)) |_| {
|
||||
var text = cd.getData();
|
||||
if (state.in_pre) {
|
||||
if (state.pre_node) |pre| {
|
||||
if (node.parentNode() == pre and node.nextSibling() == null) {
|
||||
text = std.mem.trimRight(u8, text, " \t\r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
try renderText(text, state, writer);
|
||||
}
|
||||
},
|
||||
else => {}, // Ignore other node types
|
||||
}
|
||||
}
|
||||
|
||||
fn renderChildren(parent: *Node, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
||||
var it = parent.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
try render(child, state, writer, page);
|
||||
}
|
||||
}
|
||||
|
||||
fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
||||
const tag = el.getTag();
|
||||
|
||||
// Skip hidden/metadata elements
|
||||
switch (tag) {
|
||||
.script, .style, .noscript, .template, .head, .meta, .link, .title, .svg => return,
|
||||
else => {},
|
||||
}
|
||||
|
||||
// --- Opening Tag Logic ---
|
||||
|
||||
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
||||
if (isBlock(tag)) {
|
||||
if (!state.in_table) {
|
||||
try ensureNewline(state, writer);
|
||||
if (shouldAddSpacing(tag)) {
|
||||
// Add an extra newline for spacing between blocks
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
}
|
||||
} else if (tag == .li or tag == .tr) {
|
||||
try ensureNewline(state, writer);
|
||||
}
|
||||
|
||||
// Prefixes
|
||||
switch (tag) {
|
||||
.h1 => try writer.writeAll("# "),
|
||||
.h2 => try writer.writeAll("## "),
|
||||
.h3 => try writer.writeAll("### "),
|
||||
.h4 => try writer.writeAll("#### "),
|
||||
.h5 => try writer.writeAll("##### "),
|
||||
.h6 => try writer.writeAll("###### "),
|
||||
.ul => {
|
||||
if (state.list_depth < state.list_stack.len) {
|
||||
state.list_stack[state.list_depth] = .{ .type = .unordered, .index = 0 };
|
||||
state.list_depth += 1;
|
||||
}
|
||||
},
|
||||
.ol => {
|
||||
if (state.list_depth < state.list_stack.len) {
|
||||
state.list_stack[state.list_depth] = .{ .type = .ordered, .index = 1 };
|
||||
state.list_depth += 1;
|
||||
}
|
||||
},
|
||||
.li => {
|
||||
const indent = if (state.list_depth > 0) state.list_depth - 1 else 0;
|
||||
for (0..indent) |_| try writer.writeAll(" ");
|
||||
|
||||
if (state.list_depth > 0) {
|
||||
const current_list = &state.list_stack[state.list_depth - 1];
|
||||
if (current_list.type == .ordered) {
|
||||
try writer.print("{d}. ", .{current_list.index});
|
||||
current_list.index += 1;
|
||||
} else {
|
||||
try writer.writeAll("- ");
|
||||
}
|
||||
} else {
|
||||
try writer.writeAll("- ");
|
||||
}
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.table => {
|
||||
state.in_table = true;
|
||||
state.table_row_index = 0;
|
||||
state.table_col_count = 0;
|
||||
},
|
||||
.tr => {
|
||||
state.table_col_count = 0;
|
||||
try writer.writeByte('|');
|
||||
},
|
||||
.td, .th => {
|
||||
// Note: leading pipe handled by previous cell closing or tr opening
|
||||
state.last_char_was_newline = false;
|
||||
try writer.writeByte(' ');
|
||||
},
|
||||
.blockquote => {
|
||||
try writer.writeAll("> ");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.pre => {
|
||||
try writer.writeAll("```\n");
|
||||
state.in_pre = true;
|
||||
state.pre_node = el.asNode();
|
||||
state.last_char_was_newline = true;
|
||||
},
|
||||
.code => {
|
||||
if (!state.in_pre) {
|
||||
try writer.writeByte('`');
|
||||
state.in_code = true;
|
||||
state.last_char_was_newline = false;
|
||||
}
|
||||
},
|
||||
.b, .strong => {
|
||||
try writer.writeAll("**");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.i, .em => {
|
||||
try writer.writeAll("*");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.s, .del => {
|
||||
try writer.writeAll("~~");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.hr => {
|
||||
try writer.writeAll("---\n");
|
||||
state.last_char_was_newline = true;
|
||||
return; // Void element
|
||||
},
|
||||
.br => {
|
||||
if (state.in_table) {
|
||||
try writer.writeByte(' ');
|
||||
} else {
|
||||
try writer.writeByte('\n');
|
||||
state.last_char_was_newline = true;
|
||||
}
|
||||
return; // Void element
|
||||
},
|
||||
.img => {
|
||||
try writer.writeAll(";
|
||||
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||
try writer.writeAll(src);
|
||||
}
|
||||
try writer.writeAll(")");
|
||||
state.last_char_was_newline = false;
|
||||
return; // Treat as void
|
||||
},
|
||||
.anchor => {
|
||||
try writer.writeByte('[');
|
||||
try renderChildren(el.asNode(), state, writer, page);
|
||||
try writer.writeAll("](");
|
||||
if (el.getAttributeSafe(comptime .wrap("href"))) |href| {
|
||||
try writer.writeAll(href);
|
||||
}
|
||||
try writer.writeByte(')');
|
||||
state.last_char_was_newline = false;
|
||||
return;
|
||||
},
|
||||
.input => {
|
||||
if (el.getAttributeSafe(comptime .wrap("type"))) |type_attr| {
|
||||
if (std.ascii.eqlIgnoreCase(type_attr, "checkbox")) {
|
||||
if (el.getAttributeSafe(comptime .wrap("checked"))) |_| {
|
||||
try writer.writeAll("[x] ");
|
||||
} else {
|
||||
try writer.writeAll("[ ] ");
|
||||
}
|
||||
state.last_char_was_newline = false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// --- Render Children ---
|
||||
try renderChildren(el.asNode(), state, writer, page);
|
||||
|
||||
// --- Closing Tag Logic ---
|
||||
|
||||
// Suffixes
|
||||
switch (tag) {
|
||||
.pre => {
|
||||
if (!state.last_char_was_newline) {
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
try writer.writeAll("```\n");
|
||||
state.in_pre = false;
|
||||
state.pre_node = null;
|
||||
state.last_char_was_newline = true;
|
||||
},
|
||||
.code => {
|
||||
if (!state.in_pre) {
|
||||
try writer.writeByte('`');
|
||||
state.in_code = false;
|
||||
state.last_char_was_newline = false;
|
||||
}
|
||||
},
|
||||
.b, .strong => {
|
||||
try writer.writeAll("**");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.i, .em => {
|
||||
try writer.writeAll("*");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.s, .del => {
|
||||
try writer.writeAll("~~");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.blockquote => {},
|
||||
.ul, .ol => {
|
||||
if (state.list_depth > 0) state.list_depth -= 1;
|
||||
},
|
||||
.table => {
|
||||
state.in_table = false;
|
||||
},
|
||||
.tr => {
|
||||
try writer.writeByte('\n');
|
||||
if (state.table_row_index == 0) {
|
||||
try writer.writeByte('|');
|
||||
var i: usize = 0;
|
||||
while (i < state.table_col_count) : (i += 1) {
|
||||
try writer.writeAll("---|");
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
state.table_row_index += 1;
|
||||
state.last_char_was_newline = true;
|
||||
},
|
||||
.td, .th => {
|
||||
try writer.writeAll(" |");
|
||||
state.table_col_count += 1;
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Post-block newlines
|
||||
if (isBlock(tag)) {
|
||||
if (!state.in_table) {
|
||||
try ensureNewline(state, writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn renderText(text: []const u8, state: *State, writer: *std.Io.Writer) !void {
|
||||
if (text.len == 0) return;
|
||||
|
||||
if (state.in_pre) {
|
||||
try writer.writeAll(text);
|
||||
if (text.len > 0 and text[text.len - 1] == '\n') {
|
||||
state.last_char_was_newline = true;
|
||||
} else {
|
||||
state.last_char_was_newline = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for pure whitespace
|
||||
const is_all_whitespace = for (text) |c| {
|
||||
if (!std.ascii.isWhitespace(c)) break false;
|
||||
} else true;
|
||||
|
||||
if (is_all_whitespace) {
|
||||
if (!state.last_char_was_newline) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Collapse whitespace
|
||||
var it = std.mem.tokenizeAny(u8, text, " \t\n\r");
|
||||
var first = true;
|
||||
while (it.next()) |word| {
|
||||
if (first) {
|
||||
if (!state.last_char_was_newline) {
|
||||
if (text.len > 0 and std.ascii.isWhitespace(text[0])) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
|
||||
try escapeMarkdown(writer, word);
|
||||
state.last_char_was_newline = false;
|
||||
first = false;
|
||||
}
|
||||
|
||||
// Handle trailing whitespace from the original text
|
||||
if (!first and !state.last_char_was_newline) {
|
||||
if (text.len > 0 and std.ascii.isWhitespace(text[text.len - 1])) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escapeMarkdown(writer: *std.Io.Writer, text: []const u8) !void {
|
||||
for (text) |c| {
|
||||
switch (c) {
|
||||
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {
|
||||
try writer.writeByte('\\');
|
||||
try writer.writeByte(c);
|
||||
},
|
||||
else => try writer.writeByte(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
|
||||
const testing = @import("../testing.zig");
|
||||
const page = try testing.test_session.createPage();
|
||||
defer testing.test_session.removePage();
|
||||
const doc = page.window._document;
|
||||
|
||||
const div = try doc.createElement("div", null, page);
|
||||
try page.parseHtmlAsChildren(div.asNode(), html);
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
try dump(div.asNode(), .{}, &aw.writer, page);
|
||||
|
||||
try testing.expectString(expected, aw.written());
|
||||
}
|
||||
|
||||
test "markdown: basic" {
|
||||
try testMarkdownHTML("Hello world", "Hello world\n");
|
||||
}
|
||||
|
||||
test "markdown: whitespace" {
|
||||
try testMarkdownHTML("<span>A</span> <span>B</span>", "A B\n");
|
||||
}
|
||||
|
||||
test "markdown: escaping" {
|
||||
try testMarkdownHTML("<p># Not a header</p>", "\n\\# Not a header\n");
|
||||
}
|
||||
|
||||
test "markdown: strikethrough" {
|
||||
try testMarkdownHTML("<s>deleted</s>", "~~deleted~~\n");
|
||||
}
|
||||
|
||||
test "markdown: task list" {
|
||||
try testMarkdownHTML(
|
||||
\\<input type="checkbox" checked><input type="checkbox">
|
||||
, "[x] [ ] \n");
|
||||
}
|
||||
|
||||
test "markdown: ordered list" {
|
||||
try testMarkdownHTML(
|
||||
\\<ol><li>First</li><li>Second</li></ol>
|
||||
, "1. First\n2. Second\n");
|
||||
}
|
||||
|
||||
test "markdown: table" {
|
||||
try testMarkdownHTML(
|
||||
\\<table><thead><tr><th>Head 1</th><th>Head 2</th></tr></thead>
|
||||
\\<tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody></table>
|
||||
,
|
||||
\\
|
||||
\\| Head 1 | Head 2 |
|
||||
\\|---|---|
|
||||
\\| Cell 1 | Cell 2 |
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
test "markdown: nested lists" {
|
||||
try testMarkdownHTML(
|
||||
\\<ul><li>Parent<ul><li>Child</li></ul></li></ul>
|
||||
,
|
||||
\\- Parent
|
||||
\\ - Child
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
test "markdown: blockquote" {
|
||||
try testMarkdownHTML("<blockquote>Hello world</blockquote>", "\n> Hello world\n");
|
||||
}
|
||||
|
||||
test "markdown: links" {
|
||||
try testMarkdownHTML("<a href=\"https://lightpanda.io\">Lightpanda</a>", "[Lightpanda](https://lightpanda.io)\n");
|
||||
}
|
||||
|
||||
test "markdown: images" {
|
||||
try testMarkdownHTML("<img src=\"logo.png\" alt=\"Logo\">", "\n");
|
||||
}
|
||||
|
||||
test "markdown: headings" {
|
||||
try testMarkdownHTML("<h1>Title</h1><h2>Subtitle</h2>",
|
||||
\\
|
||||
\\# Title
|
||||
\\
|
||||
\\## Subtitle
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
test "markdown: code" {
|
||||
try testMarkdownHTML(
|
||||
\\<p>Use git push</p>
|
||||
\\<pre><code>line 1
|
||||
\\line 2</code></pre>
|
||||
,
|
||||
\\
|
||||
\\Use git push
|
||||
\\
|
||||
\\```
|
||||
\\line 1
|
||||
\\line 2
|
||||
\\```
|
||||
\\
|
||||
);
|
||||
}
|
||||
@@ -3,13 +3,67 @@
|
||||
|
||||
<script id=animation>
|
||||
let a1 = document.createElement('div').animate(null, null);
|
||||
testing.expectEqual('finished', a1.playState);
|
||||
testing.expectEqual('idle', a1.playState);
|
||||
|
||||
let cb = [];
|
||||
a1.ready.then(() => { cb.push('ready') });
|
||||
a1.finished.then((x) => {
|
||||
cb.push('finished');
|
||||
cb.push(a1.playState);
|
||||
cb.push(x == a1);
|
||||
});
|
||||
testing.eventually(() => testing.expectEqual(['finished', true], cb));
|
||||
a1.ready.then(() => {
|
||||
cb.push(a1.playState);
|
||||
a1.play();
|
||||
cb.push(a1.playState);
|
||||
});
|
||||
testing.eventually(() => testing.expectEqual(['idle', 'running', 'finished', true], cb));
|
||||
</script>
|
||||
|
||||
<script id=startTime>
|
||||
let a2 = document.createElement('div').animate(null, null);
|
||||
// startTime defaults to null
|
||||
testing.expectEqual(null, a2.startTime);
|
||||
// startTime is settable
|
||||
a2.startTime = 42.5;
|
||||
testing.expectEqual(42.5, a2.startTime);
|
||||
// startTime can be reset to null
|
||||
a2.startTime = null;
|
||||
testing.expectEqual(null, a2.startTime);
|
||||
</script>
|
||||
|
||||
<script id=onfinish>
|
||||
let a3 = document.createElement('div').animate(null, null);
|
||||
// onfinish defaults to null
|
||||
testing.expectEqual(null, a3.onfinish);
|
||||
|
||||
let calls = [];
|
||||
// onfinish callback should be scheduled and called asynchronously
|
||||
a3.onfinish = function() { calls.push('finish'); };
|
||||
a3.play();
|
||||
testing.eventually(() => testing.expectEqual(['finish'], calls));
|
||||
</script>
|
||||
|
||||
<script id=pause>
|
||||
let a4 = document.createElement('div').animate(null, null);
|
||||
let cb4 = [];
|
||||
a4.finished.then((x) => { cb4.push(a4.playState) });
|
||||
a4.ready.then(() => {
|
||||
a4.play();
|
||||
cb4.push(a4.playState)
|
||||
a4.pause();
|
||||
cb4.push(a4.playState)
|
||||
});
|
||||
testing.eventually(() => testing.expectEqual(['running', 'paused'], cb4));
|
||||
</script>
|
||||
|
||||
<script id=finish>
|
||||
let a5 = document.createElement('div').animate(null, null);
|
||||
testing.expectEqual('idle', a5.playState);
|
||||
|
||||
let cb5 = [];
|
||||
a5.finished.then((x) => { cb5.push(a5.playState) });
|
||||
a5.ready.then(() => {
|
||||
cb5.push(a5.playState);
|
||||
a5.play();
|
||||
});
|
||||
testing.eventually(() => testing.expectEqual(['idle', 'finished'], cb5));
|
||||
</script>
|
||||
|
||||
@@ -33,3 +33,58 @@
|
||||
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CanvasRenderingContext2D#createImageData(width, height)">
|
||||
{
|
||||
const element = document.createElement("canvas");
|
||||
const ctx = element.getContext("2d");
|
||||
|
||||
const imageData = ctx.createImageData(100, 200);
|
||||
testing.expectEqual(true, imageData instanceof ImageData);
|
||||
testing.expectEqual(imageData.width, 100);
|
||||
testing.expectEqual(imageData.height, 200);
|
||||
testing.expectEqual(imageData.data.length, 100 * 200 * 4);
|
||||
testing.expectEqual(true, imageData.data instanceof Uint8ClampedArray);
|
||||
|
||||
// All pixels should be initialized to 0.
|
||||
testing.expectEqual(imageData.data[0], 0);
|
||||
testing.expectEqual(imageData.data[1], 0);
|
||||
testing.expectEqual(imageData.data[2], 0);
|
||||
testing.expectEqual(imageData.data[3], 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CanvasRenderingContext2D#createImageData(imageData)">
|
||||
{
|
||||
const element = document.createElement("canvas");
|
||||
const ctx = element.getContext("2d");
|
||||
|
||||
const source = ctx.createImageData(50, 75);
|
||||
const imageData = ctx.createImageData(source);
|
||||
testing.expectEqual(true, imageData instanceof ImageData);
|
||||
testing.expectEqual(imageData.width, 50);
|
||||
testing.expectEqual(imageData.height, 75);
|
||||
testing.expectEqual(imageData.data.length, 50 * 75 * 4);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CanvasRenderingContext2D#putImageData">
|
||||
{
|
||||
const element = document.createElement("canvas");
|
||||
const ctx = element.getContext("2d");
|
||||
|
||||
const imageData = ctx.createImageData(10, 10);
|
||||
testing.expectEqual(true, imageData instanceof ImageData);
|
||||
// Modify some pixel data.
|
||||
imageData.data[0] = 255;
|
||||
imageData.data[1] = 0;
|
||||
imageData.data[2] = 0;
|
||||
imageData.data[3] = 255;
|
||||
|
||||
// putImageData should not throw.
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
ctx.putImageData(imageData, 10, 20);
|
||||
// With dirty rect parameters.
|
||||
ctx.putImageData(imageData, 0, 0, 0, 0, 5, 5);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -121,6 +121,29 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="propertyAssignment">
|
||||
{
|
||||
const div = $('#test-div');
|
||||
div.style.cssText = '';
|
||||
|
||||
// camelCase assignment
|
||||
div.style.opacity = '0.5';
|
||||
testing.expectEqual('0.5', div.style.opacity);
|
||||
|
||||
// bracket notation assignment
|
||||
div.style['filter'] = 'blur(5px)';
|
||||
testing.expectEqual('blur(5px)', div.style.filter);
|
||||
|
||||
// numeric value coerced to string
|
||||
div.style.opacity = 1;
|
||||
testing.expectEqual('1', div.style.opacity);
|
||||
|
||||
// assigning method names should be ignored (not intercepted)
|
||||
div.style.setProperty('color', 'blue');
|
||||
testing.expectEqual('blue', div.style.color);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="prototypeChainCheck">
|
||||
{
|
||||
const div = $('#test-div');
|
||||
|
||||
@@ -59,10 +59,6 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=constructor-invalid-colorspace>
|
||||
testing.expectError("TypeError", () => {
|
||||
new ImageData(5, 5, { colorSpace: "display-p3" });
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=single-pixel>
|
||||
@@ -73,3 +69,7 @@
|
||||
testing.expectEqual(1, img.height);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=too-large>
|
||||
testing.expectError("IndexSizeError", () => new ImageData(2_147_483_648, 2_147_483_648));
|
||||
</script>
|
||||
|
||||
@@ -129,6 +129,8 @@ pub fn initEvent(
|
||||
self._bubbles = bubbles orelse false;
|
||||
self._cancelable = cancelable orelse false;
|
||||
self._stop_propagation = false;
|
||||
self._stop_immediate_propagation = false;
|
||||
self._prevent_default = false;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Event, shutdown: bool) void {
|
||||
|
||||
@@ -58,7 +58,10 @@ pub fn constructor(
|
||||
maybe_settings: ?ConstructorSettings,
|
||||
page: *Page,
|
||||
) !*ImageData {
|
||||
if (width == 0 or height == 0) {
|
||||
// Though arguments are unsigned long, these are capped to max. i32 on Chrome.
|
||||
// https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/html/canvas/image_data.cc#L61
|
||||
const max_i32 = std.math.maxInt(i32);
|
||||
if (width == 0 or width > max_i32 or height == 0 or height > max_i32) {
|
||||
return error.IndexSizeError;
|
||||
}
|
||||
|
||||
@@ -70,7 +73,11 @@ pub fn constructor(
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const size = width * height * 4;
|
||||
var size, var overflown = @mulWithOverflow(width, height);
|
||||
if (overflown == 1) return error.IndexSizeError;
|
||||
size, overflown = @mulWithOverflow(size, 4);
|
||||
if (overflown == 1) return error.IndexSizeError;
|
||||
|
||||
return page._factory.create(ImageData{
|
||||
._width = width,
|
||||
._height = height,
|
||||
|
||||
@@ -16,40 +16,118 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../../log.zig");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Animation = @This();
|
||||
|
||||
const PlayState = enum {
|
||||
idle,
|
||||
running,
|
||||
paused,
|
||||
finished,
|
||||
};
|
||||
|
||||
_page: *Page,
|
||||
_arena: Allocator,
|
||||
|
||||
_effect: ?js.Object.Global = null,
|
||||
_timeline: ?js.Object.Global = null,
|
||||
_ready_resolver: ?js.PromiseResolver.Global = null,
|
||||
_finished_resolver: ?js.PromiseResolver.Global = null,
|
||||
_startTime: ?f64 = null,
|
||||
_onFinish: ?js.Function.Temp = null,
|
||||
_playState: PlayState = .idle,
|
||||
|
||||
// Fake the animation by passing the states:
|
||||
// .idle => .running once play() is called.
|
||||
// .running => .finished after 10ms when update() is callback.
|
||||
//
|
||||
// TODO add support for effect and timeline
|
||||
pub fn init(page: *Page) !*Animation {
|
||||
return page._factory.create(Animation{});
|
||||
const arena = try page.getArena(.{ .debug = "Animation" });
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const self = try arena.create(Animation);
|
||||
self.* = .{
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn play(_: *Animation) void {}
|
||||
pub fn pause(_: *Animation) void {}
|
||||
pub fn cancel(_: *Animation) void {}
|
||||
pub fn finish(_: *Animation) void {}
|
||||
pub fn reverse(_: *Animation) void {}
|
||||
pub fn deinit(self: *Animation, _: bool) void {
|
||||
self._page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn play(self: *Animation, page: *Page) !void {
|
||||
if (self._playState == .running) {
|
||||
return;
|
||||
}
|
||||
|
||||
// transition to running.
|
||||
self._playState = .running;
|
||||
|
||||
// Schedule the transition from .running => .finished in 10ms.
|
||||
page.js.strongRef(self);
|
||||
try page.js.scheduler.add(
|
||||
self,
|
||||
Animation.update,
|
||||
10,
|
||||
.{ .name = "animation.update" },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn pause(self: *Animation) void {
|
||||
self._playState = .paused;
|
||||
}
|
||||
|
||||
pub fn cancel(_: *Animation) void {
|
||||
log.warn(.not_implemented, "Animation.cancel", .{});
|
||||
}
|
||||
|
||||
pub fn finish(self: *Animation, page: *Page) void {
|
||||
if (self._playState == .finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._playState = .finished;
|
||||
|
||||
// resolve finished
|
||||
if (self._finished_resolver) |resolver| {
|
||||
page.js.local.?.toLocal(resolver).resolve("Animation.getFinished", self);
|
||||
}
|
||||
// call onfinish
|
||||
if (self._onFinish) |func| {
|
||||
page.js.local.?.toLocal(func).call(void, .{}) catch |err| {
|
||||
log.warn(.js, "Animation._onFinish", .{ .err = err });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse(_: *Animation) void {
|
||||
log.warn(.not_implemented, "Animation.reverse", .{});
|
||||
}
|
||||
|
||||
pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
|
||||
if (self._finished_resolver == null) {
|
||||
const resolver = page.js.local.?.createPromiseResolver();
|
||||
resolver.resolve("Animation.getFinished", self);
|
||||
self._finished_resolver = try resolver.persist();
|
||||
return resolver.promise();
|
||||
}
|
||||
return page.js.toLocal(self._finished_resolver).?.promise();
|
||||
}
|
||||
|
||||
// The ready promise is immediately resolved.
|
||||
pub fn getReady(self: *Animation, page: *Page) !js.Promise {
|
||||
// never resolved, because we're always "finished"
|
||||
if (self._ready_resolver == null) {
|
||||
const resolver = page.js.local.?.createPromiseResolver();
|
||||
resolver.resolve("Animation.getReady", self);
|
||||
self._ready_resolver = try resolver.persist();
|
||||
return resolver.promise();
|
||||
}
|
||||
@@ -72,6 +150,65 @@ pub fn setTimeline(self: *Animation, timeline: ?js.Object.Global) !void {
|
||||
self._timeline = timeline;
|
||||
}
|
||||
|
||||
pub fn getStartTime(self: *const Animation) ?f64 {
|
||||
return self._startTime;
|
||||
}
|
||||
|
||||
pub fn setStartTime(self: *Animation, value: ?f64, page: *Page) !void {
|
||||
self._startTime = value;
|
||||
|
||||
// if the startTime is null, don't play the animation.
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return self.play(page);
|
||||
}
|
||||
|
||||
pub fn getOnFinish(self: *const Animation) ?js.Function.Temp {
|
||||
return self._onFinish;
|
||||
}
|
||||
|
||||
// callback function transitionning from a state to another
|
||||
fn update(ctx: *anyopaque) !?u32 {
|
||||
const self: *Animation = @ptrCast(@alignCast(ctx));
|
||||
|
||||
switch (self._playState) {
|
||||
.running => {
|
||||
// transition to finished.
|
||||
self._playState = .finished;
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self._page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
// resolve finished
|
||||
if (self._finished_resolver) |resolver| {
|
||||
ls.toLocal(resolver).resolve("Animation.getFinished", self);
|
||||
}
|
||||
// call onfinish
|
||||
if (self._onFinish) |func| {
|
||||
ls.toLocal(func).call(void, .{}) catch |err| {
|
||||
log.warn(.js, "Animation._onFinish", .{ .err = err });
|
||||
};
|
||||
}
|
||||
},
|
||||
.idle, .paused, .finished => {},
|
||||
}
|
||||
|
||||
// No future change scheduled, set the object weak for garbage collection.
|
||||
self._page.js.weakRef(self);
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn setOnFinish(self: *Animation, cb: ?js.Function.Temp) !void {
|
||||
self._onFinish = cb;
|
||||
}
|
||||
|
||||
pub fn playState(self: *const Animation) []const u8 {
|
||||
return @tagName(self._playState);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Animation);
|
||||
|
||||
@@ -79,6 +216,8 @@ pub const JsApi = struct {
|
||||
pub const name = "Animation";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(Animation.deinit);
|
||||
};
|
||||
|
||||
pub const play = bridge.function(Animation.play, .{});
|
||||
@@ -86,12 +225,14 @@ pub const JsApi = struct {
|
||||
pub const cancel = bridge.function(Animation.cancel, .{});
|
||||
pub const finish = bridge.function(Animation.finish, .{});
|
||||
pub const reverse = bridge.function(Animation.reverse, .{});
|
||||
pub const playState = bridge.property("finished", .{ .template = false });
|
||||
pub const playState = bridge.accessor(Animation.playState, null, .{});
|
||||
pub const pending = bridge.property(false, .{ .template = false });
|
||||
pub const finished = bridge.accessor(Animation.getFinished, null, .{});
|
||||
pub const ready = bridge.accessor(Animation.getReady, null, .{});
|
||||
pub const effect = bridge.accessor(Animation.getEffect, Animation.setEffect, .{});
|
||||
pub const timeline = bridge.accessor(Animation.getTimeline, Animation.setTimeline, .{});
|
||||
pub const startTime = bridge.accessor(Animation.getStartTime, Animation.setStartTime, .{});
|
||||
pub const onfinish = bridge.accessor(Animation.getOnFinish, Animation.setOnFinish, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
|
||||
@@ -23,6 +23,8 @@ const js = @import("../../js/js.zig");
|
||||
const color = @import("../../color.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const ImageData = @import("../ImageData.zig");
|
||||
|
||||
/// This class doesn't implement a `constructor`.
|
||||
/// It can be obtained with a call to `HTMLCanvasElement#getContext`.
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
|
||||
@@ -85,6 +87,33 @@ pub fn getTextBaseline(_: *const CanvasRenderingContext2D) []const u8 {
|
||||
return "alphabetic";
|
||||
}
|
||||
|
||||
const WidthOrImageData = union(enum) {
|
||||
width: u32,
|
||||
image_data: *ImageData,
|
||||
};
|
||||
|
||||
pub fn createImageData(
|
||||
_: *const CanvasRenderingContext2D,
|
||||
width_or_image_data: WidthOrImageData,
|
||||
/// If `ImageData` variant preferred, this is null.
|
||||
maybe_height: ?u32,
|
||||
/// Can be used if width and height provided.
|
||||
maybe_settings: ?ImageData.ConstructorSettings,
|
||||
page: *Page,
|
||||
) !*ImageData {
|
||||
switch (width_or_image_data) {
|
||||
.width => |width| {
|
||||
const height = maybe_height orelse return error.TypeError;
|
||||
return ImageData.constructor(width, height, maybe_settings, page);
|
||||
},
|
||||
.image_data => |image_data| {
|
||||
return ImageData.constructor(image_data._width, image_data._height, null, page);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
|
||||
|
||||
pub fn save(_: *CanvasRenderingContext2D) void {}
|
||||
pub fn restore(_: *CanvasRenderingContext2D) void {}
|
||||
pub fn scale(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
|
||||
@@ -131,6 +160,9 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
|
||||
pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{});
|
||||
|
||||
pub const save = bridge.function(CanvasRenderingContext2D.save, .{});
|
||||
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{});
|
||||
|
||||
|
||||
@@ -37,6 +37,14 @@ pub fn asCSSStyleDeclaration(self: *CSSStyleProperties) *CSSStyleDeclaration {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
pub fn setNamed(self: *CSSStyleProperties, name: []const u8, value: []const u8, page: *Page) !void {
|
||||
if (method_names.has(name)) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
const dash_case = camelCaseToDashCase(name, &page.buf);
|
||||
try self._proto.setProperty(dash_case, value, null, page);
|
||||
}
|
||||
|
||||
pub fn getNamed(self: *CSSStyleProperties, name: []const u8, page: *Page) ![]const u8 {
|
||||
if (method_names.has(name)) {
|
||||
return error.NotHandled;
|
||||
@@ -108,6 +116,9 @@ fn isKnownCSSProperty(dash_case: []const u8) bool {
|
||||
.{ "display", {} },
|
||||
.{ "visibility", {} },
|
||||
.{ "opacity", {} },
|
||||
.{ "filter", {} },
|
||||
.{ "transform", {} },
|
||||
.{ "transition", {} },
|
||||
.{ "position", {} },
|
||||
.{ "top", {} },
|
||||
.{ "bottom", {} },
|
||||
@@ -201,5 +212,5 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const @"[]" = bridge.namedIndexed(CSSStyleProperties.getNamed, null, null, .{});
|
||||
pub const @"[]" = bridge.namedIndexed(CSSStyleProperties.getNamed, CSSStyleProperties.setNamed, null, .{});
|
||||
};
|
||||
|
||||
@@ -527,6 +527,7 @@ fn sanitizeValue(self: *Input, value: []const u8, page: *Page) ![]const u8 {
|
||||
if (c >= 'A' and c <= 'F') needs_lower = true;
|
||||
}
|
||||
if (!needs_lower) return value;
|
||||
// Normalize to lowercase per spec
|
||||
const result = try page.call_arena.alloc(u8, 7);
|
||||
result[0] = '#';
|
||||
for (value[1..], 0..) |c, j| {
|
||||
@@ -862,8 +863,6 @@ pub const Build = struct {
|
||||
self._default_value = element.getAttributeSafe(comptime .wrap("value"));
|
||||
self._default_checked = element.getAttributeSafe(comptime .wrap("checked")) != null;
|
||||
|
||||
// Current state starts equal to default
|
||||
self._value = self._default_value;
|
||||
self._checked = self._default_checked;
|
||||
|
||||
self._input_type = if (element.getAttributeSafe(comptime .wrap("type"))) |type_attr|
|
||||
|
||||
@@ -28,6 +28,7 @@ pub const Notification = @import("Notification.zig");
|
||||
pub const log = @import("log.zig");
|
||||
pub const js = @import("browser/js/js.zig");
|
||||
pub const dump = @import("browser/dump.zig");
|
||||
pub const markdown = @import("browser/markdown.zig");
|
||||
pub const build_config = @import("build_config");
|
||||
pub const crash_handler = @import("crash_handler.zig");
|
||||
|
||||
@@ -36,6 +37,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
pub const FetchOpts = struct {
|
||||
wait_ms: u32 = 5000,
|
||||
dump: dump.RootOpts,
|
||||
dump_mode: ?Config.DumpFormat = null,
|
||||
writer: ?*std.Io.Writer = null,
|
||||
};
|
||||
pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
|
||||
@@ -97,7 +99,12 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
|
||||
_ = session.wait(opts.wait_ms);
|
||||
|
||||
const writer = opts.writer orelse return;
|
||||
try dump.root(page.window._document, opts.dump, writer, page);
|
||||
if (opts.dump_mode) |mode| {
|
||||
switch (mode) {
|
||||
.html => try dump.root(page.window._document, opts.dump, writer, page),
|
||||
.markdown => try markdown.dump(page.window._document.asNode(), .{}, writer, page),
|
||||
}
|
||||
}
|
||||
try writer.flush();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,10 +108,11 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
||||
},
|
||||
.fetch => |opts| {
|
||||
const url = opts.url;
|
||||
log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .url = url, .snapshot = app.snapshot.fromEmbedded() });
|
||||
log.debug(.app, "startup", .{ .mode = "fetch", .dump_mode = opts.dump_mode, .url = url, .snapshot = app.snapshot.fromEmbedded() });
|
||||
|
||||
var fetch_opts = lp.FetchOpts{
|
||||
.wait_ms = 5000,
|
||||
.dump_mode = opts.dump_mode,
|
||||
.dump = .{
|
||||
.strip = opts.strip,
|
||||
.with_base = opts.withbase,
|
||||
@@ -120,7 +121,7 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
||||
|
||||
var stdout = std.fs.File.stdout();
|
||||
var writer = stdout.writer(&.{});
|
||||
if (opts.dump) {
|
||||
if (opts.dump_mode != null) {
|
||||
fetch_opts.writer = &writer.interface;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user