diff --git a/README.md b/README.md index 583ede8..b55332a 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,25 +158,25 @@ 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 - - [ ] 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] hgroup element validation + - [x] header element validation + - [x] footer element validation + - [x] address element validation + - [x] p 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 diff --git a/src/html/internal/util.zig b/src/html/internal/util.zig index 9c9355e..ccc5a74 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(3000); 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..50a72fd --- /dev/null +++ b/src/html/validation/attribute/content.zig @@ -0,0 +1,64 @@ +const std = @import("std"); +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 { + 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.")); + } + } +} + +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 831d652..eab9ae4 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 { @@ -36,4 +37,20 @@ 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 }, + .{ "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 }, + .{ "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 }, + .{ "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/common.zig b/src/html/validation/element/common.zig index 03057a4..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,9 +16,24 @@ 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 = 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.")); + } + } +} + +pub fn validate_phrasing_content(children: *const []const Entity) void { + for (children.*) |child| { + if (child.definition != .element) { + continue; + } + + 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 new file mode 100644 index 0000000..a3cb50b --- /dev/null +++ b/src/html/validation/element/content.zig @@ -0,0 +1,187 @@ +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; +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; + } + + if (child.definition == .text) { + @compileError(InvalidContentModel("Text is not supported as a direct descendant of this element.")); + } + + 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.")); + } + } +} + +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 = 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.")); + } + + 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 = util.to_lowercase(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 = 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.")); + } + + 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.")); + } +} + +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 0f65ded..4191edc 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 { @@ -40,4 +41,20 @@ 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 }, + .{ "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 }, + .{ "dl", &content.validate_dl }, + .{ "dt", &content.validate_dt }, + .{ "dd", &common.validate_flow_content }, + .{ "figure", &content.validate_figure }, + .{ "figcaption", &common.validate_flow_content }, + .{ "main", &common.validate_flow_content }, + .{ "search", &common.validate_flow_content }, + // TODO: add div element validation }); diff --git a/tests/html/element/content.zig b/tests/html/element/content.zig new file mode 100644 index 0000000..0e4f016 --- /dev/null +++ b/tests/html/element/content.zig @@ -0,0 +1,164 @@ +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 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 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 Div = html.element.Div; +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()); +} + +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 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(.{})(.{ + 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 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")})(.{ + Text(.{"/ˈhæpinəs/"}), + }); + const expected = "
    /ˈhæpinəs/
    "; + try testing.expectEqualSlices(u8, expected, elm.transform()); +} + +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(.{})(.{ + 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 must transform accordingly" { + const elm = Div(.{})(.{ + P(.{Text(.{"yeah what"})}), + }); + const expected = "

    yeah what

    "; + 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"); }