╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Markdown.zig ─────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
328 // whether `dest` starts with a url scheme 329 fn hasScheme(dest: []const u8) bool { 330 const colon = std.mem.indexOfScalar(u8, dest, ':') orelse return false; 331 if (colon == 0 or !std.ascii.isAlphabetic(dest[0])) return false; 332 for (dest[0..colon]) |c| { 333 if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '.' and c != '-') return false; 334 } 335 return true; 336 } 337 338 fn put(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), widget: ui.Widget) !void { 339 var owned = widget; 340 errdefer owned.deinit(allocator); 341 try box.children.put(allocator, owned.getFocus().id, .{ .widget = owned, .rect = null, .min_size = null }); 342 } 343 344 // a heading in its level's title font, word-wrapped to the width, or bold 345 // text when there's no font for it. 346 pub const Heading = struct { 347 focus: *Focus, 348 text: RichText, 349 // the font this heading draws in, if any, and the text it draws 350 font: ?enum { title, sub_title } = null, 351 plain: []const u8 = "", 352 font_box: wgt.TextBox, 353 354 fn init(allocator: std.mem.Allocator, page_arena: *std.heap.ArenaAllocator, level: u3, spans: []const RichText.Span, fonts: bool) !Heading { 355 const focus = try Focus.create(allocator, .container); 356 errdefer focus.destroy(allocator); 357 358 // the bold fallback leads with the level's #s, so levels stay apart 359 const marked = try page_arena.allocator().alloc(RichText.Span, spans.len + 1); 360 marked[0] = .{ .text = try page_arena.allocator().print("{s} ", .{"######"[0..level]}), .style = .{ .bold = true } }; 361 @memcpy(marked[1..], spans); 362 var text = try RichText.init(allocator, marked); 363 errdefer text.deinit(allocator); 364 365 var font_box = try wgt.TextBox.init(allocator, "", .{ .border = null, .wrap_kind = .none }); 366 errdefer font_box.deinit(allocator); 367 var self: Heading = .{ .focus = focus, .text = text, .font_box = font_box }; 368 369 var plain: std.ArrayList(u8) = .empty; 370 var has_links = false; 371 for (spans) |span| { 372 try plain.appendSlice(page_arena.allocator(), span.text); 373 has_links = has_links or span.link.len > 0; 374 } 375 self.plain = std.mem.trim(u8, plain.items, " "); 376 if (fonts and level <= 2 and !has_links and self.plain.len > 0) self.font = if (level == 1) .title else .sub_title; 377 return self; 378 } 379 380 pub fn deinit(self: *Heading, allocator: std.mem.Allocator) void { 381 self.focus.destroy(allocator); 382 self.text.deinit(allocator); 383 self.font_box.deinit(allocator); 384 } 385 386 pub fn build(self: *Heading, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 387 self.clearGrid(); 388 self.focus.clear(); 389 if (try self.wrapFont(allocator, constraint.max_size.width)) { 390 try self.buildShown(allocator, constraint, root_focus, &self.font_box); 391 } else { 392 try self.buildShown(allocator, constraint, root_focus, &self.text); 393 } 394 } 395 396 // fill the font box with the heading wrapped at `max_width`, or return 397 // false when there's no font 398 fn wrapFont(self: *Heading, allocator: std.mem.Allocator, max_width: ?usize) !bool { 399 const font = self.font orelse return false; 400 // a glyph and the gap after it, so n glyphs take n * char_width - 1 columns 401 const char_width: usize = if (font == .title) 4 else 3; 402 const max_chars = if (max_width) |w| (w + 1) / char_width else null; 403 404 var arena = std.heap.ArenaAllocator.init(allocator); 405 defer arena.deinit(); 406 const aa = arena.allocator(); 407 // the fonts only cover ascii, so each byte is one char 408 const chars = try aa.alloc(u21, self.plain.len); 409 for (self.plain, chars) |c, *char| char.* = c; 410 var lines: std.ArrayList(wgt.Line) = .empty; 411 try wgt.wrapLines(aa, &lines, chars, .word, max_chars); 412 413 // a blank row between lines keeps the glyphs from touching 414 var out: std.ArrayList(u8) = .empty; 415 for (lines.items, 0..) |line, i| { 416 if (i > 0) try out.appendSlice(aa, "\n\n"); 417 const text = std.mem.trimEnd(u8, self.plain[line.start..line.end], " "); 418 try out.appendSlice(aa, switch (font) { 419 .title => (try ui.Title.init(&arena, text, .solid)).content, 420 .sub_title => (try ui.SubTitle.init(&arena, text)).content, 421 }); 422 } 423 try self.font_box.setContent(allocator, out.items); 424 return true; 425 } 426 427 fn buildShown(self: *Heading, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus, widget: anytype) !void { 428 try widget.build(allocator, constraint, root_focus); 429 const grid = widget.getGrid() orelse return; 430 try self.focus.addChild(allocator, widget.getFocus(), grid.size, 0, 0); 431 } 432 433 pub fn input(self: *Heading, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 434 _ = self; 435 _ = allocator; 436 _ = key; 437 _ = root_focus; 438 } 439 440 pub fn clearGrid(self: *Heading) void { 441 self.text.clearGrid(); 442 self.font_box.clearGrid(); 443 } 444 445 pub fn getGrid(self: Heading) ?Grid { 446 // only the one last built has a grid 447 return self.font_box.getGrid() orelse self.text.getGrid(); 448 } 449 450 pub fn getFocus(self: *Heading) *Focus { 451 return self.focus; 452 } 453 }; 454 455 // a marker column beside a child, so wrapped lines hang under the child's 456 // first line: a list item's bullet, or a quote's bar on every row. 457 pub const Gutter = struct { 458 focus: *Focus, 459 grid: ?Grid, 460 child: wgt.Box(ui.Widget), 461 marker: []const u8, 462 // draw the marker on every row rather than only the first 463 repeat: bool, 464 style: Grid.Style, 465 466 // takes ownership of `child` on success 467 fn init(allocator: std.mem.Allocator, marker: []const u8, repeat: bool, style: Grid.Style, child: wgt.Box(ui.Widget)) !Gutter { 468 return .{ .focus = try Focus.create(allocator, .container), .grid = null, .child = child, .marker = marker, .repeat = repeat, .style = style }; 469 } 470 471 pub fn deinit(self: *Gutter, allocator: std.mem.Allocator) void { 472 self.focus.destroy(allocator); 473 self.clearGrid(); 474 self.child.deinit(allocator); 475 } 476 477 pub fn build(self: *Gutter, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 478 self.clearGrid(); 479 self.focus.clear(); 480 const marker_width = try xitui.width.displayWidth(self.marker); 481 if (constraint.max_size.width) |w| if (w <= marker_width) return; 482 if (constraint.max_size.height == 0) return; 483 484 try self.child.build(allocator, .{ 485 .min_size = .{ .width = null, .height = null }, 486 .max_size = .{ .width = if (constraint.max_size.width) |w| w - marker_width else null, .height = constraint.max_size.height }, 487 }, root_focus); 488 const child_grid = self.child.getGrid(); 489 const child_size: layout.Size = if (child_grid) |g| g.size else .{ .width = 0, .height = 0 }; 490 491 var grid = try Grid.init(allocator, .{ .width = marker_width + child_size.width, .height = @max(1, child_size.height) }); 492 errdefer grid.deinit(); 493 for (0..if (self.repeat) grid.size.height else 1) |y| { 494 var x: usize = 0; 495 var utf8 = (try std.unicode.Utf8View.init(self.marker)).iterator(); 496 while (utf8.nextCodepoint()) |rune| { 497 (try grid.cell(x, y)).style = self.style; 498 try grid.setRune(x, y, rune); 499 x += xitui.width.cellWidth(rune); 500 } 501 } 502 if (child_grid) |g| { 503 try grid.drawGrid(g, marker_width, 0); 504 try self.focus.addChild(allocator, self.child.getFocus(), g.size, marker_width, 0); 505 } 506 self.grid = grid; 507 } 508 509 pub fn input(self: *Gutter, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 510 _ = self; 511 _ = allocator; 512 _ = key; 513 _ = root_focus; 514 } 515 516 pub fn clearGrid(self: *Gutter) void { 517 if (self.grid) |*grid| { 518 grid.deinit(); 519 self.grid = null; 520 } 521 self.child.clearGrid(); 522 } 523 524 pub fn getGrid(self: Gutter) ?Grid { 525 return self.grid; 526 } 527 528 pub fn getFocus(self: *Gutter) *Focus { 529 return self.focus; 530 } 531 }; 532