html-nesting.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /**
  2. * Copied from https://github.com/MananTank/validate-html-nesting
  3. * with ISC license
  4. *
  5. * To avoid runtime dependency on validate-html-nesting
  6. * This file should not change very often in the original repo
  7. * but we may need to keep it up-to-date from time to time.
  8. */
  9. /**
  10. * returns true if given parent-child nesting is valid HTML
  11. */
  12. export function isValidHTMLNesting(parent: string, child: string): boolean {
  13. // if we know the list of children that are the only valid children for the given parent
  14. if (parent in onlyValidChildren) {
  15. return onlyValidChildren[parent].has(child)
  16. }
  17. // if we know the list of parents that are the only valid parents for the given child
  18. if (child in onlyValidParents) {
  19. return onlyValidParents[child].has(parent)
  20. }
  21. // if we know the list of children that are NOT valid for the given parent
  22. if (parent in knownInvalidChildren) {
  23. // check if the child is in the list of invalid children
  24. // if so, return false
  25. if (knownInvalidChildren[parent].has(child)) return false
  26. }
  27. // if we know the list of parents that are NOT valid for the given child
  28. if (child in knownInvalidParents) {
  29. // check if the parent is in the list of invalid parents
  30. // if so, return false
  31. if (knownInvalidParents[child].has(parent)) return false
  32. }
  33. return true
  34. }
  35. const headings = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
  36. const emptySet = new Set([])
  37. /**
  38. * maps element to set of elements that can be it's children, no other */
  39. const onlyValidChildren: Record<string, Set<string>> = {
  40. head: new Set([
  41. 'base',
  42. 'basefront',
  43. 'bgsound',
  44. 'link',
  45. 'meta',
  46. 'title',
  47. 'noscript',
  48. 'noframes',
  49. 'style',
  50. 'script',
  51. 'template',
  52. ]),
  53. optgroup: new Set(['option']),
  54. select: new Set(['optgroup', 'option']),
  55. math: new Set(['mrow']),
  56. script: new Set(),
  57. // table
  58. table: new Set(['caption', 'colgroup', 'tbody', 'tfoot', 'thead']),
  59. tr: new Set(['td', 'th']),
  60. colgroup: new Set(['col']),
  61. tbody: new Set(['tr']),
  62. thead: new Set(['tr']),
  63. tfoot: new Set(['tr']),
  64. // these elements can not have any children elements
  65. iframe: emptySet,
  66. option: emptySet,
  67. textarea: emptySet,
  68. style: emptySet,
  69. title: emptySet,
  70. }
  71. /** maps elements to set of elements which can be it's parent, no other */
  72. const onlyValidParents: Record<string, Set<string>> = {
  73. // sections
  74. html: emptySet,
  75. body: new Set(['html']),
  76. head: new Set(['html']),
  77. // table
  78. td: new Set(['tr']),
  79. colgroup: new Set(['table']),
  80. caption: new Set(['table']),
  81. tbody: new Set(['table']),
  82. tfoot: new Set(['table']),
  83. col: new Set(['colgroup']),
  84. th: new Set(['tr']),
  85. thead: new Set(['table']),
  86. tr: new Set(['tbody', 'thead', 'tfoot']),
  87. // data list
  88. dd: new Set(['dl', 'div']),
  89. dt: new Set(['dl', 'div']),
  90. // other
  91. figcaption: new Set(['figure']),
  92. // li: new Set(["ul", "ol"]),
  93. summary: new Set(['details']),
  94. area: new Set(['map']),
  95. } as const
  96. /** maps element to set of elements that can not be it's children, others can */
  97. const knownInvalidChildren: Record<string, Set<string>> = {
  98. p: new Set([
  99. 'address',
  100. 'article',
  101. 'aside',
  102. 'blockquote',
  103. 'center',
  104. 'details',
  105. 'dialog',
  106. 'dir',
  107. 'div',
  108. 'dl',
  109. 'fieldset',
  110. 'figure',
  111. 'footer',
  112. 'form',
  113. 'h1',
  114. 'h2',
  115. 'h3',
  116. 'h4',
  117. 'h5',
  118. 'h6',
  119. 'header',
  120. 'hgroup',
  121. 'hr',
  122. 'li',
  123. 'main',
  124. 'nav',
  125. 'menu',
  126. 'ol',
  127. 'p',
  128. 'pre',
  129. 'section',
  130. 'table',
  131. 'ul',
  132. ]),
  133. svg: new Set([
  134. 'b',
  135. 'blockquote',
  136. 'br',
  137. 'code',
  138. 'dd',
  139. 'div',
  140. 'dl',
  141. 'dt',
  142. 'em',
  143. 'embed',
  144. 'h1',
  145. 'h2',
  146. 'h3',
  147. 'h4',
  148. 'h5',
  149. 'h6',
  150. 'hr',
  151. 'i',
  152. 'img',
  153. 'li',
  154. 'menu',
  155. 'meta',
  156. 'ol',
  157. 'p',
  158. 'pre',
  159. 'ruby',
  160. 's',
  161. 'small',
  162. 'span',
  163. 'strong',
  164. 'sub',
  165. 'sup',
  166. 'table',
  167. 'u',
  168. 'ul',
  169. 'var',
  170. ]),
  171. } as const
  172. /** maps element to set of elements that can not be it's parent, others can */
  173. const knownInvalidParents: Record<string, Set<string>> = {
  174. a: new Set(['a']),
  175. button: new Set(['button']),
  176. dd: new Set(['dd', 'dt']),
  177. dt: new Set(['dd', 'dt']),
  178. form: new Set(['form']),
  179. li: new Set(['li']),
  180. h1: headings,
  181. h2: headings,
  182. h3: headings,
  183. h4: headings,
  184. h5: headings,
  185. h6: headings,
  186. }