tiny.markdown.@"inline"
Defined in tiny.markdown.
API (8)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/markdown/src/inline.zig
zig
const std = @import("std");const termtex = @import("termtex");pub const Role = enum { strong, emphasis, strong_emphasis, strikethrough, strong_strikethrough, emphasis_strikethrough, strong_emphasis_strikethrough, code, math, link, strong_link, emphasis_link, strong_emphasis_link, strikethrough_link, strong_strikethrough_link, emphasis_strikethrough_link, strong_emphasis_strikethrough_link, image,};pub const Span = struct { start: usize, end: usize, role: Role, link: ?usize = null, math: ?usize = null, image: ?usize = null,};pub const Link = struct { url: []u8,};pub const Math = struct { source: []u8,};pub const Image = struct { url: []u8,};pub const Line = struct { text: []u8, spans: []Span, links: []Link = &.{}, math: []Math = &.{}, images: []Image = &.{}, pub fn deinit(self: *Line, allocator: std.mem.Allocator) void { for (self.links) |link| allocator.free(link.url); for (self.math) |math| allocator.free(math.source); for (self.images) |image| allocator.free(image.url); if (self.links.len != 0) allocator.free(self.links); if (self.math.len != 0) allocator.free(self.math); if (self.images.len != 0) allocator.free(self.images); allocator.free(self.text); allocator.free(self.spans); self.* = undefined; }};pub fn parse(allocator: std.mem.Allocator, text: []const u8) std.mem.Allocator.Error!Line { return parseWithMath(.terminal, allocator, text);}pub fn parseSource(allocator: std.mem.Allocator, text: []const u8) std.mem.Allocator.Error!Line { return parseWithMath(.source, allocator, text);}const MathPresentation = enum { source, terminal,};fn parseWithMath(comptime math_presentation: MathPresentation, allocator: std.mem.Allocator, text: []const u8) std.mem.Allocator.Error!Line { var rendered: std.ArrayListUnmanaged(u8) = .empty; errdefer rendered.deinit(allocator); var spans: std.ArrayListUnmanaged(Span) = .empty; errdefer spans.deinit(allocator); var links: std.ArrayListUnmanaged(Link) = .empty; errdefer { for (links.items) |link| allocator.free(link.url); links.deinit(allocator); } var math_sources: std.ArrayListUnmanaged(Math) = .empty; errdefer { for (math_sources.items) |item| allocator.free(item.source); math_sources.deinit(allocator); } var image_sources: std.ArrayListUnmanaged(Image) = .empty; errdefer { for (image_sources.items) |item| allocator.free(item.url); image_sources.deinit(allocator); } var strong_delimiter: ?[]const u8 = null; var emphasis_delimiter: ?[]const u8 = null; var strikethrough_delimiter: ?[]const u8 = null; var in_code = false; var segment_start: usize = 0; var index: usize = 0; while (index < text.len) { if (!in_code) { if (strong_delimiter) |delimiter| { if (std.mem.startsWith(u8, text[index..], delimiter) and canCloseDelimiter(text, index, delimiter)) { try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); strong_delimiter = null; index += delimiter.len; segment_start = index; continue; } } else if (strongDelimiterAt(text, index)) |delimiter| { if (!(canOpenDelimiter(text, index, delimiter) and closingDelimiter(text, index + delimiter.len, delimiter))) { index += delimiter.len; continue; } try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); strong_delimiter = delimiter; index += delimiter.len; segment_start = index; continue; } } if (!in_code) { if (emphasis_delimiter) |delimiter| { if (std.mem.startsWith(u8, text[index..], delimiter) and canCloseDelimiter(text, index, delimiter)) { try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); emphasis_delimiter = null; index += delimiter.len; segment_start = index; continue; } } else if (emphasisDelimiterAt(text, index)) |delimiter| { if (!(canOpenDelimiter(text, index, delimiter) and closingDelimiter(text, index + delimiter.len, delimiter))) { index += delimiter.len; continue; } try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); emphasis_delimiter = delimiter; index += delimiter.len; segment_start = index; continue; } } if (!in_code) { if (strikethrough_delimiter) |delimiter| { if (std.mem.startsWith(u8, text[index..], delimiter) and canCloseDelimiter(text, index, delimiter)) { try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); strikethrough_delimiter = null; index += delimiter.len; segment_start = index; continue; } } else if (strikethroughDelimiterAt(text, index)) |delimiter| { if (!(canOpenDelimiter(text, index, delimiter) and closingDelimiter(text, index + delimiter.len, delimiter))) { index += delimiter.len; continue; } try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); strikethrough_delimiter = delimiter; index += delimiter.len; segment_start = index; continue; } } if (text[index] == '`' and (in_code or closingDelimiter(text, index + 1, "`"))) { try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); in_code = !in_code; index += 1; segment_start = index; continue; } if (!in_code and (text[index] == '$' or std.mem.startsWith(u8, text[index..], "\\("))) { if (try parseMath(math_presentation, allocator, text, index)) |parsed_math| { defer allocator.free(parsed_math.text); try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); try appendMathSegment(allocator, &rendered, &spans, &math_sources, parsed_math.text, parsed_math.source); index = parsed_math.end; segment_start = index; continue; } } if (!in_code and text[index] == '!') { if (parseImage(text, index)) |image| { try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); try appendImageSegment(allocator, &rendered, &spans, &image_sources, image.text, image.url); index = image.end; segment_start = index; continue; } } if (!in_code and text[index] == '[') { if (parseLink(text, index)) |link| { try appendSegment(allocator, &rendered, &spans, text[segment_start..index], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); try appendLinkSegment(allocator, &rendered, &spans, &links, link.text, inlineLinkRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null), link.url); index = link.end; segment_start = index; continue; } } index += 1; } try appendSegment(allocator, &rendered, &spans, text[segment_start..], inlineRole(strong_delimiter != null, emphasis_delimiter != null, strikethrough_delimiter != null, in_code)); const owned_text = try rendered.toOwnedSlice(allocator); errdefer allocator.free(owned_text); const owned_spans = try spans.toOwnedSlice(allocator); errdefer allocator.free(owned_spans); const owned_links = try links.toOwnedSlice(allocator); errdefer { for (owned_links) |link| allocator.free(link.url); allocator.free(owned_links); } const owned_math = try math_sources.toOwnedSlice(allocator); errdefer { for (owned_math) |item| allocator.free(item.source); allocator.free(owned_math); } const owned_images = try image_sources.toOwnedSlice(allocator); errdefer { for (owned_images) |item| allocator.free(item.url); allocator.free(owned_images); } return .{ .text = owned_text, .spans = owned_spans, .links = owned_links, .math = owned_math, .images = owned_images, };}fn appendSegment( allocator: std.mem.Allocator, rendered: *std.ArrayListUnmanaged(u8), spans: *std.ArrayListUnmanaged(Span), segment: []const u8, role: ?Role,) std.mem.Allocator.Error!void { if (segment.len == 0) return; const start = rendered.items.len; try rendered.appendSlice(allocator, segment); const end = rendered.items.len; if (role) |active| try spans.append(allocator, .{ .start = start, .end = end, .role = active });}fn appendLinkSegment( allocator: std.mem.Allocator, rendered: *std.ArrayListUnmanaged(u8), spans: *std.ArrayListUnmanaged(Span), links: *std.ArrayListUnmanaged(Link), segment: []const u8, role: Role, url: []const u8,) std.mem.Allocator.Error!void { if (segment.len == 0) return; var owned_url = try allocator.dupe(u8, url); errdefer if (owned_url.len != 0) allocator.free(owned_url); const link_index = links.items.len; try links.append(allocator, .{ .url = owned_url }); owned_url = ""; const start = rendered.items.len; try rendered.appendSlice(allocator, segment); const end = rendered.items.len; try spans.append(allocator, .{ .start = start, .end = end, .role = role, .link = link_index });}fn appendMathSegment( allocator: std.mem.Allocator, rendered: *std.ArrayListUnmanaged(u8), spans: *std.ArrayListUnmanaged(Span), math: *std.ArrayListUnmanaged(Math), segment: []const u8, source: []const u8,) std.mem.Allocator.Error!void { if (segment.len == 0) return; var owned_source = try allocator.dupe(u8, source); errdefer allocator.free(owned_source); const math_index = math.items.len; try math.append(allocator, .{ .source = owned_source }); owned_source = ""; const start = rendered.items.len; try rendered.appendSlice(allocator, segment); const end = rendered.items.len; try spans.append(allocator, .{ .start = start, .end = end, .role = .math, .math = math_index });}fn appendImageSegment( allocator: std.mem.Allocator, rendered: *std.ArrayListUnmanaged(u8), spans: *std.ArrayListUnmanaged(Span), images: *std.ArrayListUnmanaged(Image), segment: []const u8, url: []const u8,) std.mem.Allocator.Error!void { const display = if (segment.len == 0) "image" else segment; var owned_url = try allocator.dupe(u8, url); errdefer if (owned_url.len != 0) allocator.free(owned_url); const image_index = images.items.len; try images.append(allocator, .{ .url = owned_url }); owned_url = ""; const start = rendered.items.len; try rendered.appendSlice(allocator, display); const end = rendered.items.len; try spans.append(allocator, .{ .start = start, .end = end, .role = .image, .image = image_index });}fn inlineRole(in_strong: bool, in_emphasis: bool, in_strikethrough: bool, in_code: bool) ?Role { if (in_code) return .code; if (in_strong and in_emphasis and in_strikethrough) return .strong_emphasis_strikethrough; if (in_strong and in_strikethrough) return .strong_strikethrough; if (in_emphasis and in_strikethrough) return .emphasis_strikethrough; if (in_strong and in_emphasis) return .strong_emphasis; if (in_strikethrough) return .strikethrough; if (in_strong) return .strong; if (in_emphasis) return .emphasis; return null;}fn inlineLinkRole(in_strong: bool, in_emphasis: bool, in_strikethrough: bool) Role { if (in_strong and in_emphasis and in_strikethrough) return .strong_emphasis_strikethrough_link; if (in_strong and in_strikethrough) return .strong_strikethrough_link; if (in_emphasis and in_strikethrough) return .emphasis_strikethrough_link; if (in_strong and in_emphasis) return .strong_emphasis_link; if (in_strikethrough) return .strikethrough_link; if (in_strong) return .strong_link; if (in_emphasis) return .emphasis_link; return .link;}fn closingDelimiter(text: []const u8, start: usize, delimiter: []const u8) bool { if (start >= text.len) return false; var index = start; while (std.mem.indexOf(u8, text[index..], delimiter)) |relative| { const actual = index + relative; if (canCloseDelimiter(text, actual, delimiter)) return true; index = actual + delimiter.len; if (index >= text.len) return false; } return false;}fn strongDelimiterAt(text: []const u8, index: usize) ?[]const u8 { if (std.mem.startsWith(u8, text[index..], "**")) return "**"; if (std.mem.startsWith(u8, text[index..], "__")) return "__"; return null;}fn emphasisDelimiterAt(text: []const u8, index: usize) ?[]const u8 { if (text[index] == '*') return "*"; if (text[index] == '_') return "_"; return null;}fn strikethroughDelimiterAt(text: []const u8, index: usize) ?[]const u8 { if (std.mem.startsWith(u8, text[index..], "~~")) return "~~"; return null;}fn canOpenDelimiter(text: []const u8, index: usize, delimiter: []const u8) bool { if (delimiter[0] != '_') return true; if (index > 0 and std.ascii.isAlphanumeric(text[index - 1])) return false; const after = index + delimiter.len; return after < text.len and !isWhitespace(text[after]);}fn canCloseDelimiter(text: []const u8, index: usize, delimiter: []const u8) bool { if (delimiter[0] != '_') return true; if (index == 0 or isWhitespace(text[index - 1])) return false; const after = index + delimiter.len; return after >= text.len or !std.ascii.isAlphanumeric(text[after]);}fn isWhitespace(byte: u8) bool { return byte == ' ' or byte == '\t' or byte == '\r' or byte == '\n';}const ParsedLink = struct { text: []const u8, url: []const u8, end: usize,};const ParsedImage = struct { text: []const u8, url: []const u8, end: usize,};const ParsedMath = struct { text: []u8, source: []const u8, end: usize,};const MathDelimiter = struct { open: []const u8, close: []const u8,};fn parseMath(comptime presentation: MathPresentation, allocator: std.mem.Allocator, text: []const u8, start: usize) std.mem.Allocator.Error!?ParsedMath { const delimiter = mathDelimiterAt(text, start) orelse return null; const source_start = start + delimiter.open.len; var index = source_start; while (index < text.len) { if (std.mem.startsWith(u8, text[index..], delimiter.close)) { if (index == source_start) return null; if (std.mem.eql(u8, delimiter.close, "$") and index + 1 < text.len and text[index + 1] == '$') return null; const source = text[source_start..index]; const rendered = switch (presentation) { .source => try allocator.dupe(u8, source), .terminal => termtex.renderInlineAlloc(allocator, source, .{}) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return null, }, }; return .{ .text = rendered, .source = source, .end = index + delimiter.close.len }; } if (text[index] == '\\') { index += 1; if (index < text.len) index += 1; continue; } if (text[index] == '\n' or text[index] == '\r') return null; const len = std.unicode.utf8ByteSequenceLength(text[index]) catch 1; index += @min(len, text.len - index); } return null;}fn mathDelimiterAt(text: []const u8, start: usize) ?MathDelimiter { if (start >= text.len) return null; if (text[start] == '$') { if (start + 2 >= text.len) return null; if (start + 1 < text.len and text[start + 1] == '$') return null; return .{ .open = "$", .close = "$" }; } if (std.mem.startsWith(u8, text[start..], "\\(")) return .{ .open = "\\(", .close = "\\)" }; return null;}fn parseLink(text: []const u8, start: usize) ?ParsedLink { if (start + 3 >= text.len or text[start] != '[') return null; const label_start = start + 1; const label_end_rel = std.mem.indexOfScalar(u8, text[label_start..], ']') orelse return null; const label_end = label_start + label_end_rel; if (label_end == label_start) return null; if (label_end + 1 >= text.len or text[label_end + 1] != '(') return null; const url_start = label_end + 2; const url_end_rel = std.mem.indexOfScalar(u8, text[url_start..], ')') orelse return null; const url_end = url_start + url_end_rel; if (url_end == url_start) return null; return .{ .text = text[label_start..label_end], .url = text[url_start..url_end], .end = url_end + 1, };}fn parseImage(text: []const u8, start: usize) ?ParsedImage { if (start + 4 >= text.len or text[start] != '!' or text[start + 1] != '[') return null; const label_start = start + 2; const label_end_rel = std.mem.indexOfScalar(u8, text[label_start..], ']') orelse return null; const label_end = label_start + label_end_rel; if (label_end + 1 >= text.len or text[label_end + 1] != '(') return null; const url_start = label_end + 2; const url_end_rel = std.mem.indexOfScalar(u8, text[url_start..], ')') orelse return null; const url_end = url_start + url_end_rel; if (url_end == url_start) return null; return .{ .text = text[label_start..label_end], .url = text[url_start..url_end], .end = url_end + 1, };}test "parse returns display text and styled byte ranges" { const allocator = std.testing.allocator; var line = try parse(allocator, "plain **bold** *em* ***both*** `code` [docs](https://example.com/docs) **[strong](https://example.com/strong)** *[soft](https://example.com/soft)* **open"); defer line.deinit(allocator); try std.testing.expectEqualStrings("plain bold em both code docs strong soft **open", line.text); try std.testing.expectEqual(@as(usize, 7), line.spans.len); try std.testing.expectEqual(Role.strong, line.spans[0].role); try std.testing.expectEqualStrings("bold", line.text[line.spans[0].start..line.spans[0].end]); try std.testing.expectEqual(Role.emphasis, line.spans[1].role); try std.testing.expectEqualStrings("em", line.text[line.spans[1].start..line.spans[1].end]); try std.testing.expectEqual(Role.strong_emphasis, line.spans[2].role); try std.testing.expectEqualStrings("both", line.text[line.spans[2].start..line.spans[2].end]); try std.testing.expectEqual(Role.code, line.spans[3].role); try std.testing.expectEqualStrings("code", line.text[line.spans[3].start..line.spans[3].end]); try std.testing.expectEqual(Role.link, line.spans[4].role); try std.testing.expectEqualStrings("docs", line.text[line.spans[4].start..line.spans[4].end]); try std.testing.expectEqualStrings("https://example.com/docs", line.links[line.spans[4].link.?].url); try std.testing.expectEqual(Role.strong_link, line.spans[5].role); try std.testing.expectEqualStrings("strong", line.text[line.spans[5].start..line.spans[5].end]); try std.testing.expectEqualStrings("https://example.com/strong", line.links[line.spans[5].link.?].url); try std.testing.expectEqual(Role.emphasis_link, line.spans[6].role); try std.testing.expectEqualStrings("soft", line.text[line.spans[6].start..line.spans[6].end]); try std.testing.expectEqualStrings("https://example.com/soft", line.links[line.spans[6].link.?].url);}test "parse renders math through termtex" { const allocator = std.testing.allocator; var line = try parse(allocator, "area $\\pi r^2$ and bad $\\frac{x$ stays"); defer line.deinit(allocator); try std.testing.expectEqualStrings("area πr² and bad $\\frac{x$ stays", line.text); try std.testing.expectEqual(@as(usize, 1), line.spans.len); try std.testing.expectEqual(Role.math, line.spans[0].role); try std.testing.expectEqual(@as(?usize, 0), line.spans[0].math); try std.testing.expectEqualStrings("πr²", line.text[line.spans[0].start..line.spans[0].end]); try std.testing.expectEqual(@as(usize, 1), line.math.len); try std.testing.expectEqualStrings("\\pi r^2", line.math[0].source);}test "parse renders paren math through termtex" { const allocator = std.testing.allocator; var line = try parse(allocator, "area \\(\\pi r^2\\) and bad \\(\\frac{x stays"); defer line.deinit(allocator); try std.testing.expectEqualStrings("area πr² and bad \\(\\frac{x stays", line.text); try std.testing.expectEqual(@as(usize, 1), line.spans.len); try std.testing.expectEqual(Role.math, line.spans[0].role); try std.testing.expectEqual(@as(?usize, 0), line.spans[0].math); try std.testing.expectEqualStrings("πr²", line.text[line.spans[0].start..line.spans[0].end]); try std.testing.expectEqual(@as(usize, 1), line.math.len); try std.testing.expectEqualStrings("\\pi r^2", line.math[0].source);}test "parseSource preserves math for semantic renderers" { const allocator = std.testing.allocator; var line = try parseSource(allocator, "area $\\pi r^2$"); defer line.deinit(allocator); try std.testing.expectEqualStrings("area \\pi r^2", line.text); try std.testing.expectEqual(@as(usize, 1), line.spans.len); try std.testing.expectEqual(Role.math, line.spans[0].role); try std.testing.expectEqualStrings("\\pi r^2", line.math[0].source);}test "parse records image links" { const allocator = std.testing.allocator; var line = try parse(allocator, "open  and "); defer line.deinit(allocator); try std.testing.expectEqualStrings("open plot and image", line.text); try std.testing.expectEqual(@as(usize, 2), line.spans.len); try std.testing.expectEqual(Role.image, line.spans[0].role); try std.testing.expectEqual(@as(?usize, 0), line.spans[0].image); try std.testing.expectEqualStrings("plot", line.text[line.spans[0].start..line.spans[0].end]); try std.testing.expectEqualStrings("plot.svg", line.images[0].url); try std.testing.expectEqual(Role.image, line.spans[1].role); try std.testing.expectEqual(@as(?usize, 1), line.spans[1].image); try std.testing.expectEqualStrings("empty.png", line.images[1].url);}test "parse supports underscore emphasis without snake case" { const allocator = std.testing.allocator; var line = try parse(allocator, "plain _em_ __strong__ ___both___ snake_case"); defer line.deinit(allocator); try std.testing.expectEqualStrings("plain em strong both snake_case", line.text); try std.testing.expectEqual(@as(usize, 3), line.spans.len); try std.testing.expectEqual(Role.emphasis, line.spans[0].role); try std.testing.expectEqualStrings("em", line.text[line.spans[0].start..line.spans[0].end]); try std.testing.expectEqual(Role.strong, line.spans[1].role); try std.testing.expectEqualStrings("strong", line.text[line.spans[1].start..line.spans[1].end]); try std.testing.expectEqual(Role.strong_emphasis, line.spans[2].role); try std.testing.expectEqualStrings("both", line.text[line.spans[2].start..line.spans[2].end]); try std.testing.expect(std.mem.indexOf(u8, line.text, "snake_case") != null);}test "parse supports strikethrough" { const allocator = std.testing.allocator; var line = try parse(allocator, "plain ~~old~~ **~~bold old~~** ~~[docs](https://example.com/docs)~~ ~~open"); defer line.deinit(allocator); try std.testing.expectEqualStrings("plain old bold old docs ~~open", line.text); try std.testing.expectEqual(@as(usize, 3), line.spans.len); try std.testing.expectEqual(Role.strikethrough, line.spans[0].role); try std.testing.expectEqualStrings("old", line.text[line.spans[0].start..line.spans[0].end]); try std.testing.expectEqual(Role.strong_strikethrough, line.spans[1].role); try std.testing.expectEqualStrings("bold old", line.text[line.spans[1].start..line.spans[1].end]); try std.testing.expectEqual(Role.strikethrough_link, line.spans[2].role); try std.testing.expectEqualStrings("docs", line.text[line.spans[2].start..line.spans[2].end]); try std.testing.expectEqualStrings("https://example.com/docs", line.links[line.spans[2].link.?].url);}test "parse leaves links literal inside code spans" { const allocator = std.testing.allocator; var line = try parse(allocator, "`[docs](https://example.com/docs)`"); defer line.deinit(allocator); try std.testing.expectEqualStrings("[docs](https://example.com/docs)", line.text); try std.testing.expectEqual(@as(usize, 1), line.spans.len); try std.testing.expectEqual(Role.code, line.spans[0].role); try std.testing.expect(line.spans[0].link == null); try std.testing.expectEqual(@as(usize, 0), line.links.len);}Source: lib/markdown/src/root.zig:10
zig
pub const @"inline" = @import("inline.zig");Audit
| Definitions | 1 |
|---|---|
| Public names | 1 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |