const std = @import("std");
pub fn markdownSection(source: []const u8, heading: []const u8) ?[]const u8 {
var marker_buffer: [96]u8 = undefined;
const marker = std.fmt.bufPrint(&marker_buffer, "## {s}\n", .{heading}) catch return null;
const marker_start = std.mem.indexOf(u8, source, marker) orelse return null;
const content_start = marker_start + marker.len;
if (std.mem.indexOfPos(u8, source, content_start, marker) != null) return null;
const remaining = source[content_start..];
const relative_end = std.mem.indexOf(u8, remaining, "\n## ") orelse remaining.len;
const section = std.mem.trim(u8, remaining[0..relative_end], " \t\r\n");
if (section.len == 0) return null;
return section;
}
pub fn occurrenceCount(source: []const u8, needle: []const u8) u32 {
std.debug.assert(needle.len > 0);
var count: u32 = 0;
var offset: usize = 0;
while (std.mem.indexOfPos(u8, source, offset, needle)) |index| {
count += 1;
offset = index + needle.len;
}
return count;
}
pub fn renderInlineList(values: []const []const u8, writer: *std.Io.Writer) !void {
if (values.len == 0) {
try writer.writeAll("none");
return;
}
for (values, 0..) |value, index| {
if (index > 0) try writer.writeAll(", ");
try writer.writeAll(value);
}
}
pub fn renderInlineCodeList(values: []const []const u8, writer: *std.Io.Writer) !void {
if (values.len == 0) {
try writer.writeAll("none");
return;
}
for (values, 0..) |value, index| {
if (index > 0) try writer.writeAll(", ");
try writer.print("`{s}`", .{value});
}
}
pub fn writeWrapped(
writer: *std.Io.Writer,
source: []const u8,
first_prefix: []const u8,
next_prefix: []const u8,
width_max: usize,
) !void {
var words = std.mem.tokenizeAny(u8, source, " \t\r\n");
var width = first_prefix.len;
var first = true;
try writer.writeAll(first_prefix);
while (words.next()) |word| {
const separator: usize = if (first) 0 else 1;
if (width + separator + word.len > width_max) {
try writer.writeByte('\n');
try writer.writeAll(next_prefix);
width = next_prefix.len;
} else if (!first) {
try writer.writeByte(' ');
width += 1;
}
try writer.writeAll(word);
width += word.len;
first = false;
}
}
pub fn nameValid(name: []const u8) bool {
if (name.len == 0 or name.len > 64) return false;
if (name[0] == '-' or name[name.len - 1] == '-') return false;
var previous_hyphen = false;
for (name) |byte| {
const valid = std.ascii.isLower(byte) or std.ascii.isDigit(byte) or byte == '-';
if (!valid) return false;
if (byte == '-' and previous_hyphen) return false;
previous_hyphen = byte == '-';
}
return true;
}
pub fn symbolValid(symbol: []const u8) bool {
if (symbol.len == 0 or symbol.len > 64) return false;
if (!std.ascii.isLower(symbol[0])) return false;
if (symbol[symbol.len - 1] == '_') return false;
var previous_underscore = false;
for (symbol) |byte| {
const valid = std.ascii.isLower(byte) or std.ascii.isDigit(byte) or byte == '_';
if (!valid) return false;
if (byte == '_' and previous_underscore) return false;
previous_underscore = byte == '_';
}
return true;
}
pub fn conceptIdValid(id: []const u8) bool {
return prefixedNameValid(id, "concept-");
}
pub fn prefixedNameValid(id: []const u8, prefix: []const u8) bool {
if (!std.mem.startsWith(u8, id, prefix)) return false;
return nameValid(id);
}
pub fn oracleIdValid(id: []const u8) bool {
if (id.len == 0 or id.len > 64) return false;
const invariant = std.mem.startsWith(u8, id, "INV-");
const forbidden = std.mem.startsWith(u8, id, "NO-");
if (!invariant) {
if (!forbidden) return false;
}
if (id[id.len - 1] == '-') return false;
var previous_hyphen = false;
for (id) |byte| {
const valid = std.ascii.isUpper(byte) or std.ascii.isDigit(byte) or byte == '-';
if (!valid) return false;
if (byte == '-') {
if (previous_hyphen) return false;
}
previous_hyphen = byte == '-';
}
return true;
}
pub fn markdownHasAnchor(source: []const u8, expected: []const u8) bool {
var lines = std.mem.splitScalar(u8, source, '\n');
while (lines.next()) |line| {
var level_end: usize = 0;
while (level_end < line.len and line[level_end] == '#') level_end += 1;
if (level_end == 0 or level_end >= line.len or line[level_end] != ' ') continue;
if (headingMatchesAnchor(line[level_end + 1 ..], expected)) return true;
}
return false;
}
pub fn headingMatchesAnchor(heading: []const u8, expected: []const u8) bool {
var buffer: [256]u8 = undefined;
var count: usize = 0;
var pending_hyphen = false;
for (heading) |byte| {
if (std.ascii.isAlphanumeric(byte) or byte == '_') {
if (pending_hyphen and count > 0 and buffer[count - 1] != '-') {
if (count == buffer.len) return false;
buffer[count] = '-';
count += 1;
}
pending_hyphen = false;
if (count == buffer.len) return false;
buffer[count] = std.ascii.toLower(byte);
count += 1;
} else if (byte == '-' or std.ascii.isWhitespace(byte)) {
pending_hyphen = true;
}
}
return std.mem.eql(u8, buffer[0..count], expected);
}
pub fn writeCollapsedWhitespace(writer: *std.Io.Writer, source: []const u8) !void {
var whitespace_pending = false;
var wrote_byte = false;
for (source) |byte| {
if (std.ascii.isWhitespace(byte)) {
whitespace_pending = true;
continue;
}
if (whitespace_pending and wrote_byte) try writer.writeByte(' ');
try writer.writeByte(byte);
whitespace_pending = false;
wrote_byte = true;
}
}
pub fn writeXmlWrappedDescription(writer: *std.Io.Writer, source: []const u8) !void {
const indent = " ";
const width_max = 100;
var words = std.mem.tokenizeAny(u8, source, " \t\r\n");
var width: usize = indent.len;
var word_count: u32 = 0;
try writer.writeAll(" \n");
try writer.writeAll(indent);
while (words.next()) |word| {
const escaped_width = xmlEscapedWidth(word);
const separator_width: usize = if (word_count == 0) 0 else 1;
if (width + separator_width + escaped_width > width_max) {
try writer.writeAll("\n");
try writer.writeAll(indent);
width = indent.len;
} else if (separator_width == 1) {
try writer.writeByte(' ');
width += 1;
}
try writeXmlEscaped(writer, word, false);
width += escaped_width;
word_count += 1;
}
try writer.writeAll("\n \n");
}
pub fn xmlEscapedWidth(source: []const u8) usize {
var width: usize = 0;
for (source) |byte| {
width += switch (byte) {
'&' => 5,
'<', '>' => 4,
else => 1,
};
}
return width;
}
pub fn writeXmlEscaped(writer: *std.Io.Writer, source: []const u8, collapse: bool) !void {
var whitespace_pending = false;
for (source) |byte| {
if (collapse and std.ascii.isWhitespace(byte)) {
whitespace_pending = true;
continue;
}
if (whitespace_pending) {
try writer.writeByte(' ');
whitespace_pending = false;
}
switch (byte) {
'&' => try writer.writeAll("&"),
'<' => try writer.writeAll("<"),
'>' => try writer.writeAll(">"),
else => try writer.writeByte(byte),
}
}
}
test "names and identifiers reject malformed input" {
try std.testing.expect(nameValid("filesystem-capabilities"));
try std.testing.expect(!nameValid("Filesystem"));
try std.testing.expect(!nameValid("two--hyphens"));
try std.testing.expect(conceptIdValid("concept-action"));
}
test "heading anchors are deterministic" {
try std.testing.expect(markdownHasAnchor(
"## Filesystem revalidation\n",
"filesystem-revalidation",
));
try std.testing.expect(!markdownHasAnchor(
"## Filesystem validation\n",
"filesystem-revalidation",
));
}
test "catalog text escapes XML and collapses whitespace" {
var output = std.Io.Writer.Allocating.init(std.testing.allocator);
defer output.deinit();
try writeXmlEscaped(&output.writer, "one\n two & ", true);
try std.testing.expectEqualStrings("one two & <three>", output.written());
}