/** A strongly typed module system implementation of the Document Object Model (DOM) Based on the WHATWG's HTML Living Standard https://html.spec.whatwg.org (CC-BY 4.0) Inspired by https://github.com/knupfer/type-of-html by @knupfer (BSD-3-Clause) Similar work from the OCaml ecosystem: https://github.com/ocsigen/tyxml */ { lib, ... }: let # https://html.spec.whatwg.org/multipage/dom.html#content-models # https://html.spec.whatwg.org/multipage/dom.html#kinds-of-content content-categories = [ "none" # https://html.spec.whatwg.org/multipage/dom.html#the-nothing-content-model "text" # https://html.spec.whatwg.org/multipage/dom.html#text-content "metadata" # https://html.spec.whatwg.org/multipage/dom.html#metadata-content "flow" # https://html.spec.whatwg.org/multipage/dom.html#flow-content "sectioning" # https://html.spec.whatwg.org/multipage/dom.html#sectioning-content "heading" # https://html.spec.whatwg.org/multipage/dom.html#heading-content "phrasing" # https://html.spec.whatwg.org/multipage/dom.html#phrasing-content "embedded" # https://html.spec.whatwg.org/multipage/dom.html#embedded-content-2 "interactive" # https://html.spec.whatwg.org/multipage/dom.html#interactive-content "palpable" # https://html.spec.whatwg.org/multipage/dom.html#palpable-content "scripting" # https://html.spec.whatwg.org/multipage/dom.html#script-supporting-elements ]; # base type for all DOM elements element = { name, config, ... }: { options = with lib; { attrs = mkAttrs { }; categories = mkOption { type = types.listOfUnique (types.enum content-categories); }; __toString = mkOption { internal = true; type = with types; functionTo str; }; }; }; # options with types for all the defined DOM elements element-types = lib.mapAttrs (name: value: mkOption { type = types.submodule e; }) elements; # attrset of categories, where values are module options with the type of the # elements that belong to these categories categories = with lib; genAttrs content-categories (category: (mapAttrs (_: e: mkOption { type = types.submodule e; }) # HACK: don't evaluate the submodule types, just grab the config directly (filterAttrs (_: e: elem category (e { name = "dummy"; }).config.categories) elements)) ); global-attrs = let inherit (lib) mkOption types; in { class = mkOption { type = with types; listOf nonEmptyStr; }; hidden = mkOption { type = types.bool; }; id = mkOption { type = types.nonEmptyStr; }; lang = mkOption { # TODO: https://www.rfc-editor.org/rfc/rfc5646.html type = types.str; }; style = mkOption { # TODO: CSS type ;..) type = types.str; }; title = mkOption { type = types.lines; }; # TODO: more global attributes # https://html.spec.whatwg.org/#global-attributes # https://html.spec.whatwg.org/#attr-aria-* # https://html.spec.whatwg.org/multipage/microdata.html#encoding-microdata }; mkAttrs = attrs: with lib; mkOption { type = types.submodule { options = global-attrs // attrs; }; default = { }; }; print-element = name: attrs: content: with lib; let attributes = join " " (mapAttrsToList # TODO: this needs to be smarter for boolean attributes # where the value must be written out explicitly. # probably the attribute itself should have its own `__toString`. (name: value: if isBool value then name else "${name}=${value}") attrs); in lib.squash '' <${name}${optionalString (stringLength attributes > 0) " ${attributes}"}> ${lib.indent " " content} ''; elements = rec { document = { ... }: { imports = [ element ]; options = { inherit (element-types) html; }; config.categories = [ ]; config.__toString = self: '' ${self.html} ''; }; html = { name, ... }: { imports = [ element ]; options = { inherit (element-types) head body; }; config.categories = [ ]; config.__toString = self: print-element name self.attrs '' ${self.head} ${self.body} ''; }; head = { name, ... }: { imports = [ element ]; options = with lib; { # https://html.spec.whatwg.org/multipage/semantics.html#the-head-element:concept-element-content-model # XXX: this doesn't implement the iframe srcdoc semantics # as those have questionable value and would complicate things a bit. # it should be possible though, by passing a flag via module arguments. inherit (element-types) title; base = mkOption { type = with types; nullOr (submodule base); default = null; }; content = with types; listOf (attrTag ( removeAttrs categories.metadata [ "title" "base" ] )); }; config.categories = [ ]; config.__toString = self: print-element name self.attrs '' ${self.title} ${with lib; optionalString (!isNull self.base) self.base} ${join "\n" (map (s: "${s}") self.content)} ''; }; title = { name, ... }: { # TODO config.categories = [ "metadata" ]; }; base = { name, ... }: { # TODO config.categories = [ "metadata" ]; }; body = { name, ... }: { # TODO }; }; in elements