forked from fediversity/fediversity
		
	ensure the section hierarchy is spec-compliant
- automatically assign heading levels - check that the maximum nesting depth is not exceeded
This commit is contained in:
		
							parent
							
								
									4aeb9579d6
								
							
						
					
					
						commit
						661158223a
					
				
					 2 changed files with 105 additions and 26 deletions
				
			
		|  | @ -117,4 +117,45 @@ in | |||
|       in | ||||
|       pkgs.runCommand "source" { } script; | ||||
|   }; | ||||
| 
 | ||||
|   # TODO: this is an artefact of exploration; needs to be adapted to actual use | ||||
|   config.templates.table-of-contents = { config, ... }: | ||||
|     let | ||||
|       outline = { ... }: { | ||||
|         options = { | ||||
|           value = mkOption { | ||||
|             # null denotes root | ||||
|             type = with types; nullOr (either str (listOf (attrTag categories.phrasing))); | ||||
|             subsections = mkOption { | ||||
|               type = with types; listOf (submodule outline); | ||||
|               default = with lib; map | ||||
|                 # TODO: go into depth manually here, | ||||
|                 #       we don't want to pollute the DOM implementation | ||||
|                 (c: (lib.head (attrValues c)).outline) | ||||
|                 (filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content); | ||||
|             }; | ||||
|           }; | ||||
|           __toString = mkOption { | ||||
|             type = with types; functionTo str; | ||||
|             # TODO: convert to HTML | ||||
|             default = self: lib.squash '' | ||||
|               ${if isNull self.value then "root" else self.value} | ||||
|               ${if self.subsections != [] then | ||||
|               "  " + lib.indent "  " (lib.join "\n" self.subsections) else ""} | ||||
|             ''; | ||||
|           }; | ||||
|         }; | ||||
|       }; | ||||
|     in | ||||
|     { | ||||
|       options.outline = mkOption { | ||||
|         type = types.submodule outline; | ||||
|         default = { | ||||
|           value = null; | ||||
|           subsections = with lib; | ||||
|             map (c: (lib.head (attrValues c)).outline) | ||||
|               (filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content); | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
| } | ||||
|  |  | |||
|  | @ -26,14 +26,6 @@ let | |||
|     "scripting" # https://html.spec.whatwg.org/multipage/dom.html#script-supporting-elements | ||||
|   ]; | ||||
| 
 | ||||
|   get-section-depth = content: with lib; foldl' | ||||
|     (acc: elem: | ||||
|       if isAttrs elem | ||||
|       then max acc (lib.head (attrValues elem)).section-depth | ||||
|       else acc # TODO: parse with e.g. https://github.com/remarkjs/remark to avoid raw strings | ||||
|     ) 0 | ||||
|     content; | ||||
| 
 | ||||
|   # base type for all DOM elements | ||||
|   element = { ... }: { | ||||
|     # TODO: add fields for upstream documentation references | ||||
|  | @ -49,15 +41,6 @@ let | |||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   # TODO: rename to something about sectioning | ||||
|   content-element = { ... }: { | ||||
|     options.section-depth = mkOption { | ||||
|       type = types.ints.unsigned; | ||||
|       default = 0; | ||||
|       internal = true; | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   # options with types for all the defined DOM elements | ||||
|   element-types = lib.mapAttrs | ||||
|     (name: value: mkOption { type = types.submodule value; }) | ||||
|  | @ -369,18 +352,74 @@ let | |||
|     }; | ||||
| 
 | ||||
|     body = { config, name, ... }: { | ||||
|       imports = [ element content-element ]; | ||||
|       imports = [ element ]; | ||||
|       options = { | ||||
|         attrs = mkAttrs { }; | ||||
|         content = mkOption { | ||||
|           type = with types; | ||||
|             let | ||||
|               # Type check that ensures spec-compliant section hierarchy | ||||
|               # https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines-2:concept-heading-7 | ||||
|               with-section-constraints = baseType: baseType // { | ||||
|                 merge = loc: defs: | ||||
|                   with lib; | ||||
|                   let | ||||
|                     find-and-attach = def: | ||||
|                       let | ||||
|                         process-with-depth = depth: content: | ||||
|                           map | ||||
|                             (x: | ||||
|                               if isAttrs x && x ? section && x.section ? heading | ||||
|                               then x // { | ||||
|                                 section = x.section // { | ||||
|                                   heading-level = depth; | ||||
|                                   content = process-with-depth (depth + 1) (x.section.content or [ ]); | ||||
|                                 }; | ||||
|                               } | ||||
|                               else x | ||||
|                             ) | ||||
|                             content; | ||||
| 
 | ||||
|                         find-with-depth = depth: content: | ||||
|                           let | ||||
|                             sections = map (v: { inherit (def) file; value = v; depth = depth; }) | ||||
|                               (filter (x: isAttrs x && x ? section && x.section ? heading) content); | ||||
|                             subsections = concatMap | ||||
|                               (x: | ||||
|                                 if isAttrs x && x ? section && x.section ? content | ||||
|                                 then find-with-depth (depth + 1) x.section.content | ||||
|                                 else [ ]) | ||||
|                               content; | ||||
|                           in | ||||
|                           sections ++ subsections; | ||||
| 
 | ||||
|                       in | ||||
|                       { | ||||
|                         inherit def; | ||||
|                         processed = process-with-depth 1 def.value; | ||||
|                         validation = find-with-depth 1 def.value; | ||||
|                       }; | ||||
| 
 | ||||
|                     processed = map find-and-attach defs; | ||||
|                     all-sections = flatten (map (p: p.validation) processed); | ||||
|                     too-deep = filter (sec: sec.depth > 6) all-sections; | ||||
|                   in | ||||
|                   if too-deep != [ ] then | ||||
|                     throw '' | ||||
|                       The option `${lib.options.showOption loc}` has sections nested too deeply: | ||||
|                       ${concatMapStrings (sec: "  - depth ${toString sec.depth} section in ${toString sec.file}\n") too-deep} | ||||
|                       Section hierarchy must not be deeper than 6 levels.'' | ||||
|                   else baseType.merge loc (map (p: p.def // { value = p.processed; }) processed); | ||||
|               }; | ||||
|             in | ||||
|             # HACK: bail out for now | ||||
|             # TODO: find a reasonable cut-off for where to place raw content | ||||
|             listOf (either str (attrTag categories.flow)); | ||||
|             with-section-constraints | ||||
|               # TODO: find a reasonable cut-off for where to place raw content | ||||
|               (listOf (either str (attrTag categories.flow))); | ||||
|           default = [ ]; | ||||
|         }; | ||||
|       }; | ||||
|       config.section-depth = get-section-depth config.content; | ||||
| 
 | ||||
|       config.categories = [ ]; | ||||
|       config.__toString = self: with lib; | ||||
|         print-element name self.attrs ( | ||||
|  | @ -393,8 +432,9 @@ let | |||
|             self.content) | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     section = { config, name, ... }: { | ||||
|       imports = [ element content-element ]; | ||||
|       imports = [ element ]; | ||||
|       options = { | ||||
|         # setting to an attribute set will wrap the section in <section> | ||||
|         attrs = mkOption { | ||||
|  | @ -413,6 +453,7 @@ let | |||
|             options = { | ||||
|               attrs = mkAttrs { }; | ||||
|               content = mkOption { | ||||
|                 # https://html.spec.whatwg.org/multipage/sections.html#the-h1,-h2,-h3,-h4,-h5,-and-h6-elements | ||||
|                 type = with types; either str (listOf (attrTag categories.phrasing)); | ||||
|               }; | ||||
|             }; | ||||
|  | @ -429,14 +470,12 @@ let | |||
|           default = [ ]; | ||||
|         }; | ||||
|       }; | ||||
|       config.section-depth = get-section-depth config.content + 1; | ||||
|       options.heading-level = mkOption { | ||||
|         # XXX: this will proudly fail if the invariant is violated, | ||||
|         #      but the error message will be inscrutable | ||||
|         type = with types; ints.between 1 6; | ||||
|         internal = true; | ||||
|       }; | ||||
|       config.heading-level = cfg.section-depth - config.section-depth + 1; | ||||
|       config.categories = [ "flow" "sectioning" "palpable" ]; | ||||
|       config.__toString = self: with lib; | ||||
|         let | ||||
|  | @ -457,12 +496,11 @@ let | |||
|   }; | ||||
| in | ||||
| { | ||||
|   imports = [ element content-element ]; | ||||
|   imports = [ element ]; | ||||
|   options = { | ||||
|     inherit (element-types) html; | ||||
|   }; | ||||
| 
 | ||||
|   config.section-depth = config.html.body.section-depth; | ||||
|   config.categories = [ ]; | ||||
|   config.__toString = self: '' | ||||
|     <!DOCTYPE HTML > | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Valentin Gagarin
							Valentin Gagarin