From bcea45e2070512e68326f1ba3c6440859f46be01 Mon Sep 17 00:00:00 2001 From: Wisnu Adi Nurcahyo Date: Thu, 7 Aug 2025 06:57:02 +0700 Subject: [PATCH 1/5] add progress --- README.md | 16 ++++++++-------- src/html/validation/attribute/root.zig | 1 + src/html/validation/element/common.zig | 19 ++++++++++++++++--- src/html/validation/element/root.zig | 1 + tests/html/element/content.zig | 17 +++++++++++++++++ tests/html/element/root.zig | 1 + 6 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 tests/html/element/content.zig diff --git a/README.md b/README.md index 583ede8..4b02f22 100644 --- a/README.md +++ b/README.md @@ -142,11 +142,11 @@ Invalid HTML = compile error! - [x] element name - [x] html element validation - [x] head element validation - - [ ] meta element validation + - [x] meta element validation - [x] title element validation - [x] base element validation - - [ ] link element validation - - [ ] style element validation + - [x] link element validation + - [x] style element validation - [x] body element validation - [x] article element validation - [x] section element validation @@ -158,11 +158,11 @@ Invalid HTML = compile error! - [x] h4 element validation - [x] h5 element validation - [x] h6 element validation - - [ ] hgroup element validation - - [ ] header element validation - - [ ] footer element validation - - [ ] address element validation - - [ ] p element validation + - [x] hgroup element validation + - [x] header element validation + - [x] footer element validation + - [x] address element validation + - [x] p element validation - [ ] hr element validation - [ ] pre element validation - [ ] blockquote element validation diff --git a/src/html/validation/attribute/root.zig b/src/html/validation/attribute/root.zig index 831d652..fcd6fbc 100644 --- a/src/html/validation/attribute/root.zig +++ b/src/html/validation/attribute/root.zig @@ -36,4 +36,5 @@ const validations = std.StaticStringMap(*const fn (*const []const Entity) void). .{ "header", &common.global_attribute_and_global_event_handler_only }, .{ "footer", &common.global_attribute_and_global_event_handler_only }, .{ "address", &common.global_attribute_and_global_event_handler_only }, + .{ "p", &common.global_attribute_and_global_event_handler_only }, }); diff --git a/src/html/validation/element/common.zig b/src/html/validation/element/common.zig index 03057a4..508fd09 100644 --- a/src/html/validation/element/common.zig +++ b/src/html/validation/element/common.zig @@ -15,9 +15,22 @@ pub fn validate_flow_content(children: *const []const Entity) void { continue; } - const element_name = child.definition.element.name; - if (!constant.content.FLOW_CONTENT.has(element_name)) { - @compileError(InvalidContentModel("Only flow content is supported as the child element.")); + const child_name = child.definition.element.name; + if (!constant.content.FLOW_CONTENT.has(child_name)) { + @compileError(InvalidContentModel("Only flow content is supported as the element descendants.")); + } + } +} + +pub fn validate_phrasing_content(children: *const []const Entity) void { + for (children.*) |child| { + if (child.definition != .element) { + continue; + } + + const child_name = child.definition.element.name; + if (!constant.content.PHRASING_CONTENT.has(child_name)) { + @compileError(InvalidContentModel("Only phrasing content is supported as the element descendants.")); } } } diff --git a/src/html/validation/element/root.zig b/src/html/validation/element/root.zig index 0f65ded..4ee059f 100644 --- a/src/html/validation/element/root.zig +++ b/src/html/validation/element/root.zig @@ -40,4 +40,5 @@ const validations = std.StaticStringMap(*const fn (*const []const Entity) void). .{ "header", §ion.validate_header_or_footer }, .{ "footer", §ion.validate_header_or_footer }, .{ "address", §ion.validate_address }, + .{ "p", &common.validate_phrasing_content }, }); diff --git a/tests/html/element/content.zig b/tests/html/element/content.zig new file mode 100644 index 0000000..be2b4f0 --- /dev/null +++ b/tests/html/element/content.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const testing = std.testing; +const html = @import("html"); +const Text = html.base.Text; +const Attribute = html.base.Attribute; +const P = html.element.P; +const B = html.element.B; + +test "p element must transform accordingly" { + const elm = P(.{})(.{ + Text(.{"hello, "}), + B(.{Text(.{"world"})}), + Text(.{"!"}), + }); + const expected = "

hello, world!

"; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} diff --git a/tests/html/element/root.zig b/tests/html/element/root.zig index 0cfa81e..6b5237b 100644 --- a/tests/html/element/root.zig +++ b/tests/html/element/root.zig @@ -2,4 +2,5 @@ comptime { _ = @import("metadata.zig"); _ = @import("document.zig"); _ = @import("section.zig"); + _ = @import("content.zig"); } From 49bb635e1f98422fc03bab4cf9be165e32875400 Mon Sep 17 00:00:00 2001 From: Wisnu Adi Nurcahyo Date: Sat, 9 Aug 2025 08:10:34 +0700 Subject: [PATCH 2/5] add progress --- src/html/internal/util.zig | 5 +- src/html/validation/attribute/content.zig | 42 +++++++++ src/html/validation/attribute/root.zig | 16 ++++ src/html/validation/element/content.zig | 25 ++++++ src/html/validation/element/root.zig | 16 ++++ tests/html/element/content.zig | 100 ++++++++++++++++++++++ 6 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 src/html/validation/attribute/content.zig create mode 100644 src/html/validation/element/content.zig diff --git a/src/html/internal/util.zig b/src/html/internal/util.zig index 9c9355e..e0753a7 100644 --- a/src/html/internal/util.zig +++ b/src/html/internal/util.zig @@ -14,10 +14,9 @@ pub fn fetch_entity(field: anytype) ?Entity { } pub fn fetch_entity_list(any: anytype) []const Entity { + // todo: make the value dynamic + @setEvalBranchQuota(2000); const meta_fields = std.meta.fields(@TypeOf(any)); - // this doesn't look optimal at all. need to revisit the formula provided - const max_branches = meta_fields.len * 1000; - @setEvalBranchQuota(max_branches); return struct { fn append(fields: []const std.builtin.Type.StructField) []const Entity { diff --git a/src/html/validation/attribute/content.zig b/src/html/validation/attribute/content.zig new file mode 100644 index 0000000..d70a5fb --- /dev/null +++ b/src/html/validation/attribute/content.zig @@ -0,0 +1,42 @@ +const std = @import("std"); +const internal = @import("internal"); +const util = internal.util; +const Entity = internal.entity.Entity; +const common = @import("common.zig"); +const InvalidContentAttribute = internal.constant.errors.InvalidContentAttribute; + +pub fn validate_blockquote(attributes: *const []const Entity) void { + const additional_attribute = std.StaticStringMap(void).initComptime(.{ + .{"cite"}, + }); + + for (attributes.*) |attribute| { + if (common.is_global_attribute(&attribute) or common.is_global_event_handler_attribute(&attribute)) { + continue; + } + + const attribute_name = util.to_lowercase(attribute.definition.attribute.name); + if (!additional_attribute.has(attribute_name)) { + @compileError(InvalidContentAttribute("The \"" ++ attribute_name ++ "\" attribute is not supported in the blockquote element.")); + } + } +} + +pub fn validate_ol(attributes: *const []const Entity) void { + const additional_attribute = std.StaticStringMap(void).initComptime(.{ + .{"reversed"}, + .{"start"}, + .{"type"}, + }); + + for (attributes.*) |attribute| { + if (common.is_global_attribute(&attribute) or common.is_global_event_handler_attribute(&attribute)) { + continue; + } + + const attribute_name = util.to_lowercase(attribute.definition.attribute.name); + if (!additional_attribute.has(attribute_name)) { + @compileError(InvalidContentAttribute("The \"" ++ attribute_name ++ "\" attribute is not supported in the ol element.")); + } + } +} diff --git a/src/html/validation/attribute/root.zig b/src/html/validation/attribute/root.zig index fcd6fbc..0d0b0a9 100644 --- a/src/html/validation/attribute/root.zig +++ b/src/html/validation/attribute/root.zig @@ -4,6 +4,7 @@ const util = internal.util; const common = @import("common.zig"); const metadata = @import("metadata.zig"); const section = @import("section.zig"); +const content = @import("content.zig"); const Entity = internal.entity.Entity; pub fn validate_attributes(element_name: []const u8, attributes: *const []const Entity) void { @@ -37,4 +38,19 @@ const validations = std.StaticStringMap(*const fn (*const []const Entity) void). .{ "footer", &common.global_attribute_and_global_event_handler_only }, .{ "address", &common.global_attribute_and_global_event_handler_only }, .{ "p", &common.global_attribute_and_global_event_handler_only }, + .{ "hr", &common.global_attribute_and_global_event_handler_only }, + .{ "pre", &common.global_attribute_and_global_event_handler_only }, + .{ "blockquote", &content.validate_blockquote }, + .{ "ol", &content.validate_ol }, + .{ "ul", &common.global_attribute_and_global_event_handler_only }, + .{ "menu", &common.global_attribute_and_global_event_handler_only }, + // validate li attributes + .{ "dl", &common.global_attribute_and_global_event_handler_only }, + .{ "dt", &common.global_attribute_and_global_event_handler_only }, + .{ "dd", &common.global_attribute_and_global_event_handler_only }, + .{ "figure", &common.global_attribute_and_global_event_handler_only }, + .{ "figcaption", &common.global_attribute_and_global_event_handler_only }, + .{ "main", &common.global_attribute_and_global_event_handler_only }, + .{ "search", &common.global_attribute_and_global_event_handler_only }, + .{ "div", &common.global_attribute_and_global_event_handler_only }, }); diff --git a/src/html/validation/element/content.zig b/src/html/validation/element/content.zig new file mode 100644 index 0000000..36d794c --- /dev/null +++ b/src/html/validation/element/content.zig @@ -0,0 +1,25 @@ +const std = @import("std"); +const internal = @import("internal"); +const constant = internal.constant; +const Entity = internal.entity.Entity; +const eql = std.mem.eql; +const InvalidContentModel = internal.constant.errors.InvalidContentModel; + +pub fn validate_listing(children: *const []const Entity) void { + const supported_child = std.StaticStringMap(void).initComptime(.{ + .{"li"}, + .{"script"}, + .{"template"}, + }); + + for (children.*) |child| { + if (child.definition == .comment) { + continue; + } + + const child_name = child.definition.element.name; + if (!supported_child.has(child_name)) { + @compileError(InvalidContentModel("The \"" ++ child_name ++ "\" element is not supported in the ol element.")); + } + } +} diff --git a/src/html/validation/element/root.zig b/src/html/validation/element/root.zig index 4ee059f..6aaf884 100644 --- a/src/html/validation/element/root.zig +++ b/src/html/validation/element/root.zig @@ -5,6 +5,7 @@ const common = @import("common.zig"); const document = @import("document.zig"); const metadata = @import("metadata.zig"); const section = @import("section.zig"); +const content = @import("content.zig"); const Entity = internal.entity.Entity; pub fn validate_children(element_name: []const u8, children: *const []const Entity) void { @@ -41,4 +42,19 @@ const validations = std.StaticStringMap(*const fn (*const []const Entity) void). .{ "footer", §ion.validate_header_or_footer }, .{ "address", §ion.validate_address }, .{ "p", &common.validate_phrasing_content }, + .{ "hr", &common.validate_void_element }, + .{ "pre", &common.validate_phrasing_content }, + .{ "blockquote", &common.validate_flow_content }, + .{ "ol", &content.validate_listing }, + .{ "ul", &content.validate_listing }, + .{ "menu", &content.validate_listing }, + .{ "li", &common.validate_flow_content }, + // validate dl descendants + // validate dt descendants + .{ "dd", &common.validate_flow_content }, + // validate figure descendants + .{ "figcaption", &common.validate_flow_content }, + .{ "main", &common.validate_flow_content }, + .{ "search", &common.validate_flow_content }, + // validate div descendants }); diff --git a/tests/html/element/content.zig b/tests/html/element/content.zig index be2b4f0..cbe10f0 100644 --- a/tests/html/element/content.zig +++ b/tests/html/element/content.zig @@ -4,6 +4,17 @@ const html = @import("html"); const Text = html.base.Text; const Attribute = html.base.Attribute; const P = html.element.P; +const Hr = html.element.Hr; +const Pre = html.element.Pre; +const BlockQuote = html.element.BlockQuote; +const Ol = html.element.Ol; +const Ul = html.element.Ul; +const Menu = html.element.Menu; +const Li = html.element.Li; +const Dd = html.element.Dd; +const FigCaption = html.element.FigCaption; +const Main = html.element.Main; +const Search = html.element.Search; const B = html.element.B; test "p element must transform accordingly" { @@ -15,3 +26,92 @@ test "p element must transform accordingly" { const expected = "

hello, world!

"; try testing.expectEqualSlices(u8, expected, elm.transform()); } + +test "hr element must transform accordingly" { + const elm = Hr(.{Attribute("class")("line")}); + const expected = "
"; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +test "pre element must transform accordingly" { + const elm = Pre(.{Attribute("class")("raw-code")})(.{Text(.{"whatever this is"})}); + const expected = "
whatever this is
"; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +test "blockquote element must transform accordingly" { + const elm = BlockQuote(.{Attribute("cite")("http://localhost/")})(.{ + P(.{Text(.{"add some cool quote here"})}), + }); + const expected = "

add some cool quote here

"; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +test "ol element must transform accordingly" { + const elm = Ol(.{Attribute("reversed")})(.{ + Li(.{Text(.{"first"})}), + Li(.{Text(.{"second"})}), + }); + const expected = "
  1. first
  2. second
"; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +test "ul element must transform accordingly" { + const elm = Ul(.{})(.{ + Li(.{Text(.{"first"})}), + Li(.{Text(.{"second"})}), + }); + const expected = ""; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +test "menu element must transform accordingly" { + const elm = Menu(.{})(.{ + Li(.{Text(.{"first"})}), + Li(.{Text(.{"second"})}), + }); + const expected = "
  • first
  • second
  • "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +// test li element + +// test dl element + +// test dt element + +test "dd element must transform accordingly" { + const elm = Dd(.{Attribute("class")("pronunciation")})(.{ + Text(.{"/ˈhæpinəs/"}), + }); + const expected = "
    /ˈhæpinəs/
    "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +// test figure element + +test "figcaption element must transform accordingly" { + const elm = FigCaption(.{})(.{ + P(.{Text(.{"A duck."})}), + }); + const expected = "

    A duck.

    "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +test "main element must transform accordingly" { + const elm = Main(.{})(.{ + P(.{Text(.{"yeah"})}), + }); + const expected = "

    yeah

    "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +test "search element must transform accordingly" { + const elm = Search(.{})(.{ + P(.{Text(.{"look for something?"})}), + }); + const expected = "

    look for something?

    "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +// test div element From dec907a44172938f6590ccb58c68ebb2a5a06f06 Mon Sep 17 00:00:00 2001 From: Wisnu Adi Nurcahyo Date: Sat, 9 Aug 2025 22:48:25 +0700 Subject: [PATCH 3/5] add progress --- src/html/validation/element/content.zig | 105 +++++++++++++++++++++++- src/html/validation/element/root.zig | 2 +- tests/html/element/content.zig | 23 +++++- 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/html/validation/element/content.zig b/src/html/validation/element/content.zig index 36d794c..baaeebb 100644 --- a/src/html/validation/element/content.zig +++ b/src/html/validation/element/content.zig @@ -17,9 +17,112 @@ pub fn validate_listing(children: *const []const Entity) void { continue; } + if (child.definition == .text) { + @compileError(InvalidContentModel("Text is not supported as a direct descendant of this element.")); + } + const child_name = child.definition.element.name; if (!supported_child.has(child_name)) { - @compileError(InvalidContentModel("The \"" ++ child_name ++ "\" element is not supported in the ol element.")); + @compileError(InvalidContentModel("The \"" ++ child_name ++ "\" element is not supported in this element.")); + } + } +} + +const supported_dl_child = std.StaticStringMap(void).initComptime(.{ + .{"dt"}, + .{"dd"}, + .{"div"}, + .{"script"}, + .{"template"}, +}); + +pub fn validate_dl(children: *const []const Entity) void { + comptime var found_div = false; + comptime var found_dt_or_dd = false; + + for (children.*) |child| { + if (child.definition == .comment) { + continue; + } + + if (child.definition == .text) { + @compileError(InvalidContentModel("Text is not supported as a direct descendant of this element.")); + } + + const child_name = child.definition.element.name; + if (!supported_dl_child.has(child_name)) { + @compileError(InvalidContentModel("The \"" ++ child_name ++ "\" element is not supported in the dl element.")); + } + + if (eql(u8, child_name, "div")) { + found_div = true; + } + + if (eql(u8, child_name, "dt") or eql(u8, child_name, "dd")) { + found_dt_or_dd = true; + } + } + + if ((!found_div and !found_dt_or_dd) or (found_div and found_dt_or_dd)) { + @compileError(InvalidContentModel("The dl element must have either both dt and dd as its descendants, or div with dt and dd as its descendants.")); + } + + if (found_div) { + for (children.*) |child| { + if (child.definition == .element) { + const child_name = child.definition.element.name; + if (eql(u8, child_name, "div")) { + validate_dl_internal(&child.definition.element.chlidren); + } + } + } + } else { + validate_dl_internal(children); + } +} + +fn validate_dl_internal(children: *const []const Entity) void { + comptime var last_elm = "__"; + comptime var found_dt = false; + comptime var has_one_valid_group = false; + + for (children.*) |child| { + if (child.definition == .comment) { + continue; + } + + if (child.definition == .text) { + @compileError(InvalidContentModel("Text is not supported as a direct descendant of this element.")); } + + const child_name = child.definition.element.name; + if (!supported_dl_child.has(child_name)) { + @compileError(InvalidContentModel("The \"" ++ child_name ++ "\" element is not supported in the dl element.")); + } + + if (eql(u8, child_name, "div")) { + @compileError(InvalidContentModel("A nested div element is not supported.")); + } + + if (eql(u8, child_name, "dt")) { + if (eql(u8, last_elm, "dt")) { + @compileError(InvalidContentModel("The next valid descendant after the dt element is the dd element.")); + } + last_elm = "dt"; + found_dt = true; + } + + if (eql(u8, child_name, "dd")) { + if (!found_dt and !eql(u8, last_elm, "dd")) { + @compileError(InvalidContentModel("The dd element must be after exactly one dt element.")); + } + last_elm = "dd"; + found_dt = false; + has_one_valid_group = true; + } + } + + if (!has_one_valid_group) { + @compileError(InvalidContentModel("There must be at least one group of dt element that is followed by one or more dd elements.")); } } diff --git a/src/html/validation/element/root.zig b/src/html/validation/element/root.zig index 6aaf884..7cd43d8 100644 --- a/src/html/validation/element/root.zig +++ b/src/html/validation/element/root.zig @@ -49,7 +49,7 @@ const validations = std.StaticStringMap(*const fn (*const []const Entity) void). .{ "ul", &content.validate_listing }, .{ "menu", &content.validate_listing }, .{ "li", &common.validate_flow_content }, - // validate dl descendants + .{ "dl", &content.validate_dl }, // validate dt descendants .{ "dd", &common.validate_flow_content }, // validate figure descendants diff --git a/tests/html/element/content.zig b/tests/html/element/content.zig index cbe10f0..c82f685 100644 --- a/tests/html/element/content.zig +++ b/tests/html/element/content.zig @@ -11,11 +11,14 @@ const Ol = html.element.Ol; const Ul = html.element.Ul; const Menu = html.element.Menu; const Li = html.element.Li; +const Dl = html.element.Dl; +const Dt = html.element.Dt; const Dd = html.element.Dd; const FigCaption = html.element.FigCaption; const Main = html.element.Main; const Search = html.element.Search; const B = html.element.B; +const Div = html.element.Div; test "p element must transform accordingly" { const elm = P(.{})(.{ @@ -76,7 +79,25 @@ test "menu element must transform accordingly" { // test li element -// test dl element +test "dl element must transform accordingly" { + const elm1 = Dl(.{})(.{ + Dt(.{Text(.{"first group"})}), + Dd(.{Text(.{"content x"})}), + Dd(.{Text(.{"content y"})}), + }); + const expected1 = "
    first group
    content x
    content y
    "; + try testing.expectEqualSlices(u8, expected1, elm1.transform()); + + const elm2 = Dl(.{})(.{ + Div(.{ + Dt(.{Text(.{"first group"})}), + Dd(.{Text(.{"content x"})}), + Dd(.{Text(.{"content y"})}), + }), + }); + const expected2 = "
    first group
    content x
    content y
    "; + try testing.expectEqualSlices(u8, expected2, elm2.transform()); +} // test dt element From 964438f4e4009700635787ddee3e24e5b5c7d386 Mon Sep 17 00:00:00 2001 From: Wisnu Adi Nurcahyo Date: Sun, 10 Aug 2025 09:42:44 +0700 Subject: [PATCH 4/5] add progress --- src/html/internal/util.zig | 2 +- src/html/validation/attribute/content.zig | 22 ++++++++ src/html/validation/attribute/root.zig | 2 +- src/html/validation/element/common.zig | 7 ++- src/html/validation/element/content.zig | 67 +++++++++++++++++++++-- src/html/validation/element/root.zig | 6 +- tests/html/element/content.zig | 36 ++++++++++-- 7 files changed, 126 insertions(+), 16 deletions(-) diff --git a/src/html/internal/util.zig b/src/html/internal/util.zig index e0753a7..ccc5a74 100644 --- a/src/html/internal/util.zig +++ b/src/html/internal/util.zig @@ -15,7 +15,7 @@ pub fn fetch_entity(field: anytype) ?Entity { pub fn fetch_entity_list(any: anytype) []const Entity { // todo: make the value dynamic - @setEvalBranchQuota(2000); + @setEvalBranchQuota(3000); const meta_fields = std.meta.fields(@TypeOf(any)); return struct { diff --git a/src/html/validation/attribute/content.zig b/src/html/validation/attribute/content.zig index d70a5fb..50a72fd 100644 --- a/src/html/validation/attribute/content.zig +++ b/src/html/validation/attribute/content.zig @@ -3,6 +3,7 @@ const internal = @import("internal"); const util = internal.util; const Entity = internal.entity.Entity; const common = @import("common.zig"); +const eql = std.mem.eql; const InvalidContentAttribute = internal.constant.errors.InvalidContentAttribute; pub fn validate_blockquote(attributes: *const []const Entity) void { @@ -16,6 +17,7 @@ pub fn validate_blockquote(attributes: *const []const Entity) void { } const attribute_name = util.to_lowercase(attribute.definition.attribute.name); + if (!additional_attribute.has(attribute_name)) { @compileError(InvalidContentAttribute("The \"" ++ attribute_name ++ "\" attribute is not supported in the blockquote element.")); } @@ -35,8 +37,28 @@ pub fn validate_ol(attributes: *const []const Entity) void { } const attribute_name = util.to_lowercase(attribute.definition.attribute.name); + if (!additional_attribute.has(attribute_name)) { @compileError(InvalidContentAttribute("The \"" ++ attribute_name ++ "\" attribute is not supported in the ol element.")); } } } + +pub fn validate_li(attributes: *const []const Entity) void { + // TODO: find a way to validate this if and onlly if the parent is ol element + const additional_attribute = std.StaticStringMap(void).initComptime(.{ + .{"value"}, + }); + + for (attributes.*) |attribute| { + if (common.is_global_attribute(&attribute) or common.is_global_event_handler_attribute(&attribute)) { + continue; + } + + const attribute_name = util.to_lowercase(attribute.definition.attribute.name); + + if (!additional_attribute.has(attribute_name)) { + @compileError(InvalidContentAttribute("The \"" ++ attribute_name ++ "\" attribute is not supported in the li element.")); + } + } +} diff --git a/src/html/validation/attribute/root.zig b/src/html/validation/attribute/root.zig index 0d0b0a9..eab9ae4 100644 --- a/src/html/validation/attribute/root.zig +++ b/src/html/validation/attribute/root.zig @@ -44,7 +44,7 @@ const validations = std.StaticStringMap(*const fn (*const []const Entity) void). .{ "ol", &content.validate_ol }, .{ "ul", &common.global_attribute_and_global_event_handler_only }, .{ "menu", &common.global_attribute_and_global_event_handler_only }, - // validate li attributes + .{ "li", &content.validate_li }, .{ "dl", &common.global_attribute_and_global_event_handler_only }, .{ "dt", &common.global_attribute_and_global_event_handler_only }, .{ "dd", &common.global_attribute_and_global_event_handler_only }, diff --git a/src/html/validation/element/common.zig b/src/html/validation/element/common.zig index 508fd09..637992b 100644 --- a/src/html/validation/element/common.zig +++ b/src/html/validation/element/common.zig @@ -1,5 +1,6 @@ const internal = @import("internal"); const constant = internal.constant; +const util = internal.util; const Entity = internal.entity.Entity; const InvalidContentModel = internal.constant.errors.InvalidContentModel; @@ -15,7 +16,8 @@ pub fn validate_flow_content(children: *const []const Entity) void { continue; } - const child_name = child.definition.element.name; + const child_name = util.to_lowercase(child.definition.element.name); + if (!constant.content.FLOW_CONTENT.has(child_name)) { @compileError(InvalidContentModel("Only flow content is supported as the element descendants.")); } @@ -28,7 +30,8 @@ pub fn validate_phrasing_content(children: *const []const Entity) void { continue; } - const child_name = child.definition.element.name; + const child_name = util.to_lowercase(child.definition.element.name); + if (!constant.content.PHRASING_CONTENT.has(child_name)) { @compileError(InvalidContentModel("Only phrasing content is supported as the element descendants.")); } diff --git a/src/html/validation/element/content.zig b/src/html/validation/element/content.zig index baaeebb..a3cb50b 100644 --- a/src/html/validation/element/content.zig +++ b/src/html/validation/element/content.zig @@ -1,5 +1,6 @@ const std = @import("std"); const internal = @import("internal"); +const util = internal.util; const constant = internal.constant; const Entity = internal.entity.Entity; const eql = std.mem.eql; @@ -21,7 +22,8 @@ pub fn validate_listing(children: *const []const Entity) void { @compileError(InvalidContentModel("Text is not supported as a direct descendant of this element.")); } - const child_name = child.definition.element.name; + const child_name = util.to_lowercase(child.definition.element.name); + if (!supported_child.has(child_name)) { @compileError(InvalidContentModel("The \"" ++ child_name ++ "\" element is not supported in this element.")); } @@ -49,7 +51,8 @@ pub fn validate_dl(children: *const []const Entity) void { @compileError(InvalidContentModel("Text is not supported as a direct descendant of this element.")); } - const child_name = child.definition.element.name; + const child_name = util.to_lowercase(child.definition.element.name); + if (!supported_dl_child.has(child_name)) { @compileError(InvalidContentModel("The \"" ++ child_name ++ "\" element is not supported in the dl element.")); } @@ -70,7 +73,7 @@ pub fn validate_dl(children: *const []const Entity) void { if (found_div) { for (children.*) |child| { if (child.definition == .element) { - const child_name = child.definition.element.name; + const child_name = util.to_lowercase(child.definition.element.name); if (eql(u8, child_name, "div")) { validate_dl_internal(&child.definition.element.chlidren); } @@ -95,7 +98,8 @@ fn validate_dl_internal(children: *const []const Entity) void { @compileError(InvalidContentModel("Text is not supported as a direct descendant of this element.")); } - const child_name = child.definition.element.name; + const child_name = util.to_lowercase(child.definition.element.name); + if (!supported_dl_child.has(child_name)) { @compileError(InvalidContentModel("The \"" ++ child_name ++ "\" element is not supported in the dl element.")); } @@ -126,3 +130,58 @@ fn validate_dl_internal(children: *const []const Entity) void { @compileError(InvalidContentModel("There must be at least one group of dt element that is followed by one or more dd elements.")); } } + +pub fn validate_dt(children: *const []const Entity) void { + for (children.*) |child| { + if (child.definition != .element) { + continue; + } + + const child_name = util.to_lowercase(child.definition.element.name); + const error_message = "The \"" ++ child_name ++ "\" element is not supported in the dt element."; + + if (!constant.content.FLOW_CONTENT.has(child_name)) { + @compileError(InvalidContentModel(error_message)); + } + + if (eql(u8, child_name, "header") or eql(u8, child_name, "footer")) { + @compileError(InvalidContentModel(error_message)); + } + + if (constant.content.SECTIONING_CONTENT.has(child_name) or constant.content.HEADING_CONTENT.has(child_name)) { + @compileError(InvalidContentModel(error_message)); + } + } +} + +pub fn validate_figure(children: *const []const Entity) void { + comptime var figcaption_count = 0; + comptime var flow_content_count = 0; + + for (children.*) |child| { + if (child.definition != .element) { + continue; + } + + const child_name = util.to_lowercase(child.definition.element.name); + + if (constant.content.FLOW_CONTENT.has(child_name)) { + flow_content_count += 1; + continue; + } + + if (!eql(u8, child_name, "figcaption")) { + @compileError(InvalidContentModel("The \"" ++ child_name ++ "\" element is not supported in the figure element.")); + } + + figcaption_count += 1; + } + + if (figcaption_count > 1) { + @compileError(InvalidContentModel("Only one figcaption element supported in the figure element.")); + } + + if (figcaption_count == 1 and flow_content_count == 0) { + @compileError(InvalidContentModel("At least one flow content is required after/before the figcaption element.")); + } +} diff --git a/src/html/validation/element/root.zig b/src/html/validation/element/root.zig index 7cd43d8..4191edc 100644 --- a/src/html/validation/element/root.zig +++ b/src/html/validation/element/root.zig @@ -50,11 +50,11 @@ const validations = std.StaticStringMap(*const fn (*const []const Entity) void). .{ "menu", &content.validate_listing }, .{ "li", &common.validate_flow_content }, .{ "dl", &content.validate_dl }, - // validate dt descendants + .{ "dt", &content.validate_dt }, .{ "dd", &common.validate_flow_content }, - // validate figure descendants + .{ "figure", &content.validate_figure }, .{ "figcaption", &common.validate_flow_content }, .{ "main", &common.validate_flow_content }, .{ "search", &common.validate_flow_content }, - // validate div descendants + // TODO: add div element validation }); diff --git a/tests/html/element/content.zig b/tests/html/element/content.zig index c82f685..0e4f016 100644 --- a/tests/html/element/content.zig +++ b/tests/html/element/content.zig @@ -14,11 +14,12 @@ const Li = html.element.Li; const Dl = html.element.Dl; const Dt = html.element.Dt; const Dd = html.element.Dd; +const Figure = html.element.Figure; const FigCaption = html.element.FigCaption; const Main = html.element.Main; const Search = html.element.Search; -const B = html.element.B; const Div = html.element.Div; +const B = html.element.B; test "p element must transform accordingly" { const elm = P(.{})(.{ @@ -77,7 +78,13 @@ test "menu element must transform accordingly" { try testing.expectEqualSlices(u8, expected, elm.transform()); } -// test li element +test "li element must transform accordingly" { + const elm = Ol(.{})(.{ + Li(.{Attribute("value")("1")}), + }); + const expected = "
    "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} test "dl element must transform accordingly" { const elm1 = Dl(.{})(.{ @@ -99,7 +106,13 @@ test "dl element must transform accordingly" { try testing.expectEqualSlices(u8, expected2, elm2.transform()); } -// test dt element +test "dt element must transform accordingly" { + const elm = Dt(.{})(.{ + Text(.{"A duck."}), + }); + const expected = "
    A duck.
    "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} test "dd element must transform accordingly" { const elm = Dd(.{Attribute("class")("pronunciation")})(.{ @@ -109,7 +122,14 @@ test "dd element must transform accordingly" { try testing.expectEqualSlices(u8, expected, elm.transform()); } -// test figure element +test "figure element must transform accordingly" { + const elm = Figure(.{})(.{ + FigCaption(.{Text(.{"foo"})}), + P(.{Text(.{"bar"})}), + }); + const expected = "
    foo

    bar

    "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} test "figcaption element must transform accordingly" { const elm = FigCaption(.{})(.{ @@ -135,4 +155,10 @@ test "search element must transform accordingly" { try testing.expectEqualSlices(u8, expected, elm.transform()); } -// test div element +test "div element must transform accordingly" { + const elm = Div(.{})(.{ + P(.{Text(.{"yeah what"})}), + }); + const expected = "

    yeah what

    "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} From 8d11ee78dcd24c74ec7d15eabff0c7ea1228cbba Mon Sep 17 00:00:00 2001 From: Wisnu Adi Nurcahyo Date: Sun, 10 Aug 2025 09:44:22 +0700 Subject: [PATCH 5/5] update readme --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4b02f22..b55332a 100644 --- a/README.md +++ b/README.md @@ -163,20 +163,20 @@ Invalid HTML = compile error! - [x] footer element validation - [x] address element validation - [x] p element validation - - [ ] hr element validation - - [ ] pre element validation - - [ ] blockquote element validation - - [ ] ol element validation - - [ ] ul element validation - - [ ] menu element validation - - [ ] li element validation - - [ ] dl element validation - - [ ] dt element validation - - [ ] dd element validation - - [ ] figure element validation - - [ ] figcaption element validation - - [ ] main element validation - - [ ] search element validation + - [x] hr element validation + - [x] pre element validation + - [x] blockquote element validation + - [x] ol element validation + - [x] ul element validation + - [x] menu element validation + - [x] li element validation + - [x] dl element validation + - [x] dt element validation + - [x] dd element validation + - [x] figure element validation + - [x] figcaption element validation + - [x] main element validation + - [x] search element validation - [ ] div element validation - [ ] a element validation - [ ] em element validation