diff --git a/presentation/dom.nix b/presentation/dom.nix new file mode 100644 index 00000000..67dbb9f8 --- /dev/null +++ b/presentation/dom.nix @@ -0,0 +1,178 @@ +/** + 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