about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-02-08 13:39:14 +0100
committerbptato <nincsnevem662@gmail.com>2024-02-08 13:39:14 +0100
commit065001ec238913862022cadae105326c097e58e1 (patch)
tree6d64e336f45e4afe9ae509f8811822551c80345f /src
parentf666dbafec85ba6ed64db8123ceae28b80c4cb3b (diff)
downloadchawan-065001ec238913862022cadae105326c097e58e1.tar.gz
dom: enumize attribute names
Diffstat (limited to 'src')
-rw-r--r--src/css/cascade.nim36
-rw-r--r--src/css/match.nim3
-rw-r--r--src/css/selectorparser.nim14
-rw-r--r--src/html/catom.nim66
-rw-r--r--src/html/dom.nim315
-rw-r--r--src/html/enums.nim58
-rw-r--r--src/server/buffer.nim17
-rw-r--r--src/xhr/formdata.nim35
8 files changed, 338 insertions, 206 deletions
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 4df558c0..0168e940 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -107,44 +107,44 @@ func calcPresentationalHints(element: Element): CSSComputedValues =
       new(result)
     result{a} = b
   template map_width =
-    let s = parseDimensionValues(element.attr("width"))
+    let s = parseDimensionValues(element.attr(atWidth))
     if s.isSome:
       set_cv "width", s.get
   template map_height =
-    let s = parseDimensionValues(element.attr("height"))
+    let s = parseDimensionValues(element.attr(atHeight))
     if s.isSome:
       set_cv "height", s.get
   template map_width_nozero =
-    let s = parseDimensionValues(element.attr("width"))
+    let s = parseDimensionValues(element.attr(atWidth))
     if s.isSome and s.get.num != 0:
       set_cv "width", s.get
   template map_height_nozero =
-    let s = parseDimensionValues(element.attr("height"))
+    let s = parseDimensionValues(element.attr(atHeight))
     if s.isSome and s.get.num != 0:
       set_cv "height", s.get
   template map_bgcolor =
-    let s = element.attr("bgcolor")
+    let s = element.attr(atBgcolor)
     if s != "":
       let c = parseLegacyColor(s)
       if c.isSome:
         set_cv "background-color", c.get
   template map_size =
-    let s = element.attrul("size")
+    let s = element.attrul(atSize)
     if s.isSome:
       set_cv "width", CSSLength(num: float64(s.get), unit: UNIT_CH)
   template map_valign =
-    case element.attr("valign").toLowerAscii()
+    case element.attr(atValign).toLowerAscii()
     of "top": set_cv "vertical-align", CSSVerticalAlign(keyword: VERTICAL_ALIGN_TOP)
     of "middle": set_cv "vertical-align", CSSVerticalAlign(keyword: VERTICAL_ALIGN_MIDDLE)
     of "bottom": set_cv "vertical-align", CSSVerticalAlign(keyword: VERTICAL_ALIGN_BOTTOM)
     of "baseline": set_cv "vertical-align", CSSVerticalAlign(keyword: VERTICAL_ALIGN_BASELINE)
   template map_align =
-    case element.attr("align").toLowerAscii()
+    case element.attr(atAlign).toLowerAscii()
     of "center", "middle": set_cv "text-align", TEXT_ALIGN_CHA_CENTER
     of "left": set_cv "text-align", TEXT_ALIGN_CHA_LEFT
     of "right": set_cv "text-align", TEXT_ALIGN_CHA_RIGHT
   template map_table_align =
-    case element.attr("align").toLowerAscii()
+    case element.attr(atAlign).toLowerAscii()
     of "left":
      set_cv "float", FLOAT_LEFT
     of "right":
@@ -153,31 +153,31 @@ func calcPresentationalHints(element: Element): CSSComputedValues =
       set_cv "margin-left", CSSLengthAuto #TODO should be inline-start
       set_cv "margin-right", CSSLengthAuto #TODO should be inline-end
   template map_text =
-    let s = element.attr("text")
+    let s = element.attr(atText)
     if s != "":
       let c = parseLegacyColor(s)
       if c.isSome:
         set_cv "color", c.get
   template map_color =
-    let s = element.attr("color")
+    let s = element.attr(atColor)
     if s != "":
       let c = parseLegacyColor(s)
       if c.isSome:
         set_cv "color", c.get
   template map_colspan =
-    let colspan = element.attrulgz("colspan")
+    let colspan = element.attrulgz(atColspan)
     if colspan.isSome:
       let i = colspan.get
       if i <= 1000:
         set_cv "-cha-colspan", int(i)
   template map_rowspan =
-    let rowspan = element.attrul("rowspan")
+    let rowspan = element.attrul(atRowspan)
     if rowspan.isSome:
       let i = rowspan.get
       if i <= 65534:
         set_cv "-cha-rowspan", int(i)
   template map_list_type_ol =
-    let ctype = element.attr("type")
+    let ctype = element.attr(atType)
     if ctype.len > 0:
       case ctype[0]
       of '1': set_cv "list-style-type", LIST_STYLE_TYPE_DECIMAL
@@ -187,7 +187,7 @@ func calcPresentationalHints(element: Element): CSSComputedValues =
       of 'I': set_cv "list-style-type", LIST_STYLE_TYPE_UPPER_ROMAN
       else: discard
   template map_list_type_ul =
-    let ctype = element.attr("type")
+    let ctype = element.attr(atType)
     if ctype.len > 0:
       case ctype.toLowerAscii()
       of "none": set_cv "list-style-type", LIST_STYLE_TYPE_NONE
@@ -226,8 +226,8 @@ func calcPresentationalHints(element: Element): CSSComputedValues =
     map_text
   of TAG_TEXTAREA:
     let textarea = HTMLTextAreaElement(element)
-    let cols = textarea.attrul("cols").get(20)
-    let rows = textarea.attrul("rows").get(1)
+    let cols = textarea.attrul(atCols).get(20)
+    let rows = textarea.attrul(atRows).get(1)
     set_cv "width", CSSLength(unit: UNIT_CH, num: float64(cols))
     set_cv "height", CSSLength(unit: UNIT_EM, num: float64(rows))
   of TAG_FONT:
@@ -369,7 +369,7 @@ proc applyRulesFrameInvalid(frame: CascadeFrame, ua, user: CSSStylesheet,
         styledText.pseudo = pseudo
         styledParent.children.add(styledText)
     of PSEUDO_IMAGE:
-      let src = Element(styledParent.node).attr("src")
+      let src = Element(styledParent.node).attr(atSrc)
       let content = CSSContent(t: CONTENT_IMAGE, s: src)
       let styledText = styledParent.newStyledReplacement(content)
       styledText.pseudo = pseudo
diff --git a/src/css/match.nim b/src/css/match.nim
index 0d1f507f..6482855b 100644
--- a/src/css/match.nim
+++ b/src/css/match.nim
@@ -8,6 +8,7 @@ import css/selectorparser
 import css/stylednode
 import html/catom
 import html/dom
+import html/enums
 import utils/twtstr
 
 import chame/tags
@@ -139,7 +140,7 @@ func pseudoSelectorMatches[T: Element|StyledNode](elem: T, sel: Selector, felem:
   of PSEUDO_LANG:
     return sel.pseudo.s == "en" #TODO languages?
   of PSEUDO_LINK:
-    return elem.tagType in {TAG_A, TAG_AREA} and elem.attrb("href")
+    return elem.tagType in {TAG_A, TAG_AREA} and elem.attrb(atHref)
   of PSEUDO_VISITED:
     return false
 
diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim
index eacb48d9..ca691050 100644
--- a/src/css/selectorparser.nim
+++ b/src/css/selectorparser.nim
@@ -59,7 +59,9 @@ type
       when defined(debug):
         classs: string
     of ATTR_SELECTOR:
-      attr*: string
+      attr*: CAtom
+      when defined(debug):
+        attrs: string
       value*: string
       rel*: SelectorRelation
     of UNIVERSAL_SELECTOR: #TODO namespaces?
@@ -135,7 +137,11 @@ func `$`*(sel: Selector): string =
     of FLAG_NONE: ""
     of FLAG_I: " i"
     of FLAG_S: " s"
-    return '[' & sel.attr & rel & sel.value & flag & ']'
+    let attrs = when defined(debug):
+      sel.attrs
+    else:
+      "ATOM" & $int(sel.attr)
+    return '[' & attrs & rel & sel.value & flag & ']'
   of CLASS_SELECTOR:
     when defined(debug):
       return "." & sel.classs
@@ -364,7 +370,7 @@ proc parseAttributeSelector(state: var SelectorParser,
   if not state2.has():
     return Selector(
       t: ATTR_SELECTOR,
-      attr: attr.value,
+      attr: state.factory.toAtom(attr.value),
       rel: SelectorRelation(t: RELATION_EXISTS)
     )
   let delim = get_tok state2.consume()
@@ -395,7 +401,7 @@ proc parseAttributeSelector(state: var SelectorParser,
       flag = FLAG_S
   return Selector(
     t: ATTR_SELECTOR,
-    attr: attr.value,
+    attr: state.factory.toAtom(attr.value),
     value: value.value,
     rel: SelectorRelation(
       t: rel,
diff --git a/src/html/catom.nim b/src/html/catom.nim
index 62066663..e908a5f4 100644
--- a/src/html/catom.nim
+++ b/src/html/catom.nim
@@ -1,5 +1,7 @@
 import std/hashes
 
+import html/enums
+
 import chame/tags
 
 #TODO use a better hash map
@@ -7,33 +9,34 @@ const CAtomFactoryStrMapLength = 1024 # must be a power of 2
 static:
   doAssert (CAtomFactoryStrMapLength and (CAtomFactoryStrMapLength - 1)) == 0
 
+# Null atom + mapped tag types + mapped attr types
+const AttrMapNum = 1 + ({TagType.low..TagType.high} - {TAG_UNKNOWN}).card +
+  ({AttrType.low..AttrType.high} - {atUnknown}).card
+
 type
   CAtom* = distinct int
 
-  #TODO could be a ptr probably
-  CAtomFactory* = ref object of RootObj
+  CAtomFactoryInit = object
+    obj: CAtomFactoryObj
+    attrToAtom: array[AttrType, CAtom]
+    atomToAttr: array[AttrMapNum, AttrType]
+
+  CAtomFactoryObj = object
     strMap: array[CAtomFactoryStrMapLength, seq[CAtom]]
     atomMap: seq[string]
 
+  #TODO could be a ptr probably
+  CAtomFactory* = ref CAtomFactoryObj
+
 const CAtomNull* = CAtom(0)
 
 # Mandatory Atom functions
 func `==`*(a, b: CAtom): bool {.borrow.}
 func hash*(atom: CAtom): Hash {.borrow.}
 
-func toAtom*(factory: CAtomFactory, s: string): CAtom
+func `$`*(a: CAtom): string {.borrow.}
 
-proc newCAtomFactory*(): CAtomFactory =
-  const minCap = int(TagType.high) + 1
-  let factory = CAtomFactory(
-    atomMap: newSeqOfCap[string](minCap),
-  )
-  factory.atomMap.add("") # skip TAG_UNKNOWN
-  for tagType in TagType(int(TAG_UNKNOWN) + 1) .. TagType.high:
-    discard factory.toAtom($tagType)
-  return factory
-
-func toAtom*(factory: CAtomFactory, s: string): CAtom =
+func toAtom(factory: var CAtomFactoryObj, s: string): CAtom =
   let h = s.hash()
   let i = h and (factory.strMap.len - 1)
   for atom in factory.strMap[i]:
@@ -46,15 +49,48 @@ func toAtom*(factory: CAtomFactory, s: string): CAtom =
   factory.strMap[i].add(atom)
   return atom
 
+const factoryInit = (func(): CAtomFactoryInit =
+  var init = CAtomFactoryInit()
+  # Null atom
+  init.obj.atomMap.add("")
+  # TagType: 1..TagType.high
+  for tagType in TagType(1) .. TagType.high:
+    discard init.obj.toAtom($tagType)
+  # Attr: may overlap with TagType; exclude atUnknown
+  for attrType in AttrType(1) .. AttrType.high:
+    let atom = init.obj.toAtom($attrType)
+    init.attrToAtom[attrType] = atom
+    init.atomToAttr[int(atom)] = attrType
+  return init
+)()
+
+proc newCAtomFactory*(): CAtomFactory =
+  let factory = new(CAtomFactory)
+  factory[] = factoryInit.obj
+  return factory
+
+func toAtom*(factory: CAtomFactory, s: string): CAtom =
+  return factory[].toAtom(s)
+
 func toAtom*(factory: CAtomFactory, tagType: TagType): CAtom =
   assert tagType != TAG_UNKNOWN
   return CAtom(tagType)
 
+func toAtom*(factory: CAtomFactory, attrType: AttrType): CAtom =
+  assert attrType != atUnknown
+  return factoryInit.attrToAtom[attrType]
+
 func toStr*(factory: CAtomFactory, atom: CAtom): string =
   return factory.atomMap[int(atom)]
 
 func toTagType*(factory: CAtomFactory, atom: CAtom): TagType =
   let i = int(atom)
-  if i > 0 and i <= int(high(TagType)):
+  if i in 1 .. int(TagType.high):
     return TagType(atom)
   return TAG_UNKNOWN
+
+func toAttrType*(factory: CAtomFactory, atom: CAtom): AttrType =
+  let i = int(atom)
+  if i < factoryInit.atomToAttr.len:
+    return factoryInit.atomToAttr[i]
+  return atUnknown
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 700d1d22..34696ad3 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -215,7 +215,7 @@ type
     id*: CAtom
     name*: CAtom
     classList* {.jsget.}: DOMTokenList
-    attrs: seq[AttrData]
+    attrs*: seq[AttrData] #TODO TODO TODO unextern
     attributesInternal: NamedNodeMap
     hover*: bool
     invalid*: bool
@@ -699,7 +699,7 @@ type
     REFLECT_STR, REFLECT_BOOL, REFLECT_LONG, REFLECT_ULONG_GZ, REFLECT_ULONG
 
   ReflectEntry = object
-    attrname: string
+    attrname: AttrType
     funcname: string
     tags: set[TagType]
     case t: ReflectType
@@ -709,22 +709,27 @@ type
       u: uint32
     else: discard
 
+func attrType0(s: static string): AttrType =
+  return parseEnum[AttrType](s)
+
 template toset(ts: openarray[TagType]): set[TagType] =
   var tags: system.set[TagType]
   for tag in ts:
     tags.incl(tag)
   tags
 
-func makes(name: string, ts: set[TagType]): ReflectEntry =
+func makes(name: static string, ts: set[TagType]): ReflectEntry =
+  const attrname = attrType0(name)
   ReflectEntry(
-    attrname: name,
+    attrname: attrname,
     funcname: name,
     t: REFLECT_STR,
     tags: ts
   )
 
-func makes(attrname: string, funcname: string, ts: set[TagType]):
+func makes(attrname, funcname: static string, ts: set[TagType]):
     ReflectEntry =
+  const attrname = attrType0(attrname)
   ReflectEntry(
     attrname: attrname,
     funcname: funcname,
@@ -732,14 +737,16 @@ func makes(attrname: string, funcname: string, ts: set[TagType]):
     tags: ts
   )
 
-func makes(name: string, ts: varargs[TagType]): ReflectEntry =
+func makes(name: static string, ts: varargs[TagType]): ReflectEntry =
   makes(name, toset(ts))
 
-func makes(attrname, funcname: string, ts: varargs[TagType]): ReflectEntry =
+func makes(attrname, funcname: static string, ts: varargs[TagType]):
+    ReflectEntry =
   makes(attrname, funcname, toset(ts))
 
-func makeb(attrname, funcname: string, ts: varargs[TagType]):
+func makeb(attrname, funcname: static string, ts: varargs[TagType]):
     ReflectEntry =
+  const attrname = attrType0(attrname)
   ReflectEntry(
     attrname: attrname,
     funcname: funcname,
@@ -747,21 +754,25 @@ func makeb(attrname, funcname: string, ts: varargs[TagType]):
     tags: toset(ts)
   )
 
-func makeb(name: string, ts: varargs[TagType]): ReflectEntry =
+func makeb(name: static string, ts: varargs[TagType]): ReflectEntry =
   makeb(name, name, ts)
 
-template makeul(name: string, ts: varargs[TagType], default = 0u32): ReflectEntry =
+func makeul(name: static string, ts: varargs[TagType], default = 0u32):
+    ReflectEntry =
+  const attrname = attrType0(name)
   ReflectEntry(
-    attrname: name,
+    attrname: attrname,
     funcname: name,
     t: REFLECT_ULONG,
     tags: toset(ts),
     u: default
   )
 
-template makeulgz(name: string, ts: varargs[TagType], default = 0u32): ReflectEntry =
+func makeulgz(name: static string, ts: varargs[TagType], default = 0u32):
+    ReflectEntry =
+  const attrname = attrType0(name)
   ReflectEntry(
-    attrname: name,
+    attrname: attrname,
     funcname: name,
     t: REFLECT_ULONG_GZ,
     tags: toset(ts),
@@ -799,11 +810,10 @@ const ReflectTable0 = [
 ]
 
 # Forward declarations
-func attr*(element: Element, s: CAtom): string
-func attr*(element: Element, s: string): string
+func attr*(element: Element, s: AttrType): string
 func attrb*(element: Element, s: CAtom): bool
 proc attr*(element: Element, name: CAtom, value: string)
-proc attr*(element: Element, name, value: string)
+proc attr*(element: Element, name: AttrType, value: string)
 func baseURL*(document: Document): URL
 proc delAttr(element: Element, i: int, keep = false)
 proc reflectAttrs(element: Element, name: CAtom, value: string)
@@ -816,12 +826,18 @@ func document*(node: Node): Document =
 proc toAtom*(document: Document, s: string): CAtom =
   return document.factory.toAtom(s)
 
+proc toAtom*(document: Document, at: AttrType): CAtom =
+  return document.factory.toAtom(at)
+
 proc toStr(document: Document, atom: CAtom): string =
   return document.factory.toStr(atom)
 
 proc toTagType*(document: Document, atom: CAtom): TagType =
   return document.factory.toTagType(atom)
 
+proc toAttrType(document: Document, atom: CAtom): AttrType =
+  return document.factory.toAttrType(atom)
+
 proc toAtom*(document: Document, tagType: TagType): CAtom =
   return document.factory.toAtom(tagType)
 
@@ -852,6 +868,9 @@ func findAttr(element: Element, qualifiedName: CAtom): int =
       return i
   return -1
 
+func findAttr(element: Element, qualifiedName: AttrType): int =
+  return element.findAttr(element.document.toAtom(qualifiedName))
+
 func findAttrNS(element: Element, namespace, qualifiedName: CAtom): int =
   for i, attr in element.attrs:
     if attr.namespace == namespace and attr.qualifiedName == qualifiedName:
@@ -1560,7 +1579,7 @@ proc jsLocalName(attr: Attr): string {.jsfget: "localName".} =
 proc jsValue(attr: Attr): string {.jsfget: "value".} =
   return attr.data.value
 
-func name(attr: Attr): string {.jsfget.} =
+func jsName(attr: Attr): string {.jsfget: "name".} =
   return attr.ownerElement.document.toStr(attr.data.qualifiedName)
 
 func findAttr(map: NamedNodeMap, dataIdx: int): int =
@@ -1699,7 +1718,7 @@ func scriptingEnabled*(element: Element): bool =
 
 func isSubmitButton*(element: Element): bool =
   if element of HTMLButtonElement:
-    return element.attr("type") == "submit"
+    return element.attr(atType) == "submit"
   elif element of HTMLInputElement:
     let element = HTMLInputElement(element)
     return element.inputType in {INPUT_SUBMIT, INPUT_IMAGE}
@@ -1998,27 +2017,29 @@ func attr*(element: Element, s: CAtom): string =
     return element.attrs[i].value
   return ""
 
-func attr*(element: Element, s: string): string =
+func attr*(element: Element, s: AttrType): string =
   return element.attr(element.document.toAtom(s))
 
-func attrl*(element: Element, s: string): Option[int32] =
+func attrl*(element: Element, s: AttrType): Option[int32] =
   return parseInt32(element.attr(s))
 
-func attrulgz*(element: Element, s: string): Option[uint32] =
+func attrulgz*(element: Element, s: AttrType): Option[uint32] =
   let x = parseUInt32(element.attr(s))
   if x.isSome and x.get > 0:
     return x
+  return none(uint32)
 
-func attrul*(element: Element, s: string): Option[uint32] =
+func attrul*(element: Element, s: AttrType): Option[uint32] =
   let x = parseUInt32(element.attr(s))
   if x.isSome and x.get >= 0:
     return x
+  return none(uint32)
 
 func attrb*(element: Element, s: CAtom): bool =
   return element.findAttr(s) != -1
 
-func attrb*(element: Element, s: string): bool =
-  let atom = element.document.toAtom(s)
+func attrb*(element: Element, at: AttrType): bool =
+  let atom = element.document.toAtom(at)
   return element.attrb(atom)
 
 # https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments
@@ -2097,9 +2118,9 @@ func outerHTML(element: Element): string {.jsfget.} =
   return element.serializeFragmentInner(TAG_UNKNOWN)
 
 func crossOrigin0(element: HTMLElement): CORSAttribute =
-  if not element.attrb("crossorigin"):
+  if not element.attrb(atCrossorigin):
     return NO_CORS
-  case element.attr("crossorigin")
+  case element.attr(atCrossorigin)
   of "anonymous", "":
     return ANONYMOUS
   of "use-credentials":
@@ -2114,7 +2135,7 @@ func crossOrigin(element: HTMLImageElement): CORSAttribute {.jsfget.} =
   return element.crossOrigin0
 
 func referrerpolicy(element: HTMLScriptElement): Option[ReferrerPolicy] =
-  getReferrerPolicy(element.attr("referrerpolicy"))
+  getReferrerPolicy(element.attr(atReferrerpolicy))
 
 proc sheets*(document: Document): seq[CSSStylesheet] =
   if document.cachedSheetsInvalid:
@@ -2139,9 +2160,9 @@ func inputString*(input: HTMLInputElement): string =
     if input.checked: "*"
     else: " "
   of INPUT_SEARCH, INPUT_TEXT, INPUT_EMAIL, INPUT_URL, INPUT_TEL:
-    input.value.padToWidth(int(input.attrulgz("size").get(20)))
+    input.value.padToWidth(int(input.attrulgz(atSize).get(20)))
   of INPUT_PASSWORD:
-    '*'.repeat(input.value.len).padToWidth(int(input.attrulgz("size").get(20)))
+    '*'.repeat(input.value.len).padToWidth(int(input.attrulgz(atSize).get(20)))
   of INPUT_RESET:
     if input.value != "": input.value
     else: "RESET"
@@ -2150,16 +2171,17 @@ func inputString*(input: HTMLInputElement): string =
     else: "SUBMIT"
   of INPUT_FILE:
     if input.file.isNone:
-      "".padToWidth(int(input.attrulgz("size").get(20)))
+      "".padToWidth(int(input.attrulgz(atSize).get(20)))
     else:
-      input.file.get.path.serialize_unicode().padToWidth(int(input.attrulgz("size").get(20)))
+      input.file.get.path.serialize_unicode()
+        .padToWidth(int(input.attrulgz(atSize).get(20)))
   else: input.value
 
 func textAreaString*(textarea: HTMLTextAreaElement): string =
   let split = textarea.value.split('\n')
-  let rows = int(textarea.attrul("rows").get(1))
+  let rows = int(textarea.attrul(atRows).get(1))
   for i in 0 ..< rows:
-    let cols = int(textarea.attrul("cols").get(20))
+    let cols = int(textarea.attrul(atCols).get(20))
     if cols > 2:
       if i < split.len:
         result &= '[' & split[i].padToWidth(cols - 2) & "]\n"
@@ -2178,21 +2200,21 @@ func isButton*(element: Element): bool =
 
 func action*(element: Element): string =
   if element.isSubmitButton():
-    if element.attrb("formaction"):
-      return element.attr("formaction")
+    if element.attrb(atFormaction):
+      return element.attr(atFormaction)
   if element of FormAssociatedElement:
     let element = FormAssociatedElement(element)
     if element.form != nil:
-      if element.form.attrb("action"):
-        return element.form.attr("action")
+      if element.form.attrb(atAction):
+        return element.form.attr(atAction)
   if element of HTMLFormElement:
-    return element.attr("action")
+    return element.attr(atAction)
   return ""
 
 func enctype*(element: Element): FormEncodingType =
   if element.isSubmitButton():
-    if element.attrb("formenctype"):
-      return case element.attr("formenctype").toLowerAscii()
+    if element.attrb(atFormenctype):
+      return case element.attr(atFormenctype).toLowerAscii()
       of "application/x-www-form-urlencoded": FORM_ENCODING_TYPE_URLENCODED
       of "multipart/form-data": FORM_ENCODING_TYPE_MULTIPART
       of "text/plain": FORM_ENCODING_TYPE_TEXT_PLAIN
@@ -2201,8 +2223,8 @@ func enctype*(element: Element): FormEncodingType =
   if element of HTMLInputElement:
     let element = HTMLInputElement(element)
     if element.form != nil:
-      if element.form.attrb("enctype"):
-        return case element.attr("enctype").toLowerAscii()
+      if element.form.attrb(atEnctype):
+        return case element.attr(atEnctype).toLowerAscii()
         of "application/x-www-form-urlencoded": FORM_ENCODING_TYPE_URLENCODED
         of "multipart/form-data": FORM_ENCODING_TYPE_MULTIPART
         of "text/plain": FORM_ENCODING_TYPE_TEXT_PLAIN
@@ -2222,17 +2244,17 @@ func formmethod*(element: Element): FormMethod =
     # The standard says nothing about this, but this code path is reached
     # on implicit form submission and other browsers seem to agree on this
     # behavior.
-    return parseFormMethod(element.attr("method"))
+    return parseFormMethod(element.attr(atMethod))
 
   if element.isSubmitButton():
-    if element.attrb("formmethod"):
-      return parseFormMethod(element.attr("formmethod"))
+    if element.attrb(atFormmethod):
+      return parseFormMethod(element.attr(atFormmethod))
 
   if element of FormAssociatedElement:
     let element = FormAssociatedElement(element)
     if element.form != nil:
-      if element.form.attrb("method"):
-        return parseFormMethod(element.form.attr("method"))
+      if element.form.attrb(atMethod):
+        return parseFormMethod(element.form.attr(atMethod))
 
   return FORM_METHOD_GET
 
@@ -2269,57 +2291,57 @@ proc parseColor(element: Element, s: string): RGBAColor =
 
 #TODO ??
 func target0*(element: Element): string =
-  if element.attrb("target"):
-    return element.attr("target")
+  if element.attrb(atTarget):
+    return element.attr(atTarget)
   for base in element.document.elements(TAG_BASE):
-    if base.attrb("target"):
-      return base.attr("target")
+    if base.attrb(atTarget):
+      return base.attr(atTarget)
   return ""
 
 # HTMLHyperlinkElementUtils (for <a> and <area>)
 func href0[T: HTMLAnchorElement|HTMLAreaElement](element: T): string =
-  if element.attrb("href"):
-    let url = parseURL(element.attr("href"), some(element.document.baseURL))
-    if url.isSome:
-      return $url.get
+  let url = parseURL(element.attr(atHref), some(element.document.baseURL))
+  if url.isSome:
+    return $url.get
+  return ""
 
 # <base>
 func href(base: HTMLBaseElement): string {.jsfget.} =
-  if base.attrb("href"):
-    #TODO with fallback base url
-    let url = parseURL(base.attr("href"))
-    if url.isSome:
-      return $url.get
+  #TODO with fallback base url
+  let url = parseURL(base.attr(atHref))
+  if url.isSome:
+    return $url.get
+  return ""
 
 # <a>
 func href*(anchor: HTMLAnchorElement): string {.jsfget.} =
   anchor.href0
 
 proc href(anchor: HTMLAnchorElement, href: string) {.jsfset.} =
-  anchor.attr("href", href)
+  anchor.attr(atHref, href)
 
 func `$`(anchor: HTMLAnchorElement): string {.jsfunc.} =
   anchor.href
 
 proc setRelList(anchor: HTMLAnchorElement, s: string) {.jsfset: "relList".} =
-  anchor.attr("rel", s)
+  anchor.attr(atRel, s)
 
 # <area>
 func href(area: HTMLAreaElement): string {.jsfget.} =
   area.href0
 
 proc href(area: HTMLAreaElement, href: string) {.jsfset.} =
-  area.attr("href", href)
+  area.attr(atHref, href)
 
 func `$`(area: HTMLAreaElement): string {.jsfunc.} =
   area.href
 
 proc setRelList(area: HTMLAreaElement, s: string) {.jsfset: "relList".} =
-  area.attr("rel", s)
+  area.attr(atRel, s)
 
 # <label>
 func control*(label: HTMLLabelElement): FormAssociatedElement {.jsfget.} =
-  let f = label.attr("for")
+  let f = label.attr(atFor)
   if f != "":
     let elem = label.document.getElementById(f)
     #TODO the supported check shouldn't be needed, just labelable
@@ -2339,7 +2361,7 @@ func form(label: HTMLLabelElement): HTMLFormElement {.jsfget.} =
 
 # <link>
 proc setRelList(link: HTMLLinkElement, s: string) {.jsfset: "relList".} =
-  link.attr("rel", s)
+  link.attr(atRel, s)
 
 proc setSheet*(link: HTMLLinkElement, sheet: CSSStylesheet) =
   link.sheet = sheet
@@ -2348,7 +2370,7 @@ proc setSheet*(link: HTMLLinkElement, sheet: CSSStylesheet) =
 
 # <form>
 proc setRelList(form: HTMLFormElement, s: string) {.jsfset: "relList".} =
-  form.attr("rel", s)
+  form.attr(atRel, s)
 
 # <input>
 func jsForm(this: HTMLInputElement): HTMLFormElement {.jsfget: "form".} =
@@ -2424,7 +2446,7 @@ proc newHTMLElement*(document: Document, localName: CAtom,
     result = HTMLInputElement()
   of TAG_A:
     let anchor = HTMLAnchorElement()
-    let localName = document.toAtom("rel")
+    let localName = document.toAtom(atRel)
     anchor.relList = DOMTokenList(element: anchor, localName: localName)
     result = anchor
   of TAG_SELECT:
@@ -2451,12 +2473,12 @@ proc newHTMLElement*(document: Document, localName: CAtom,
     result = HTMLStyleElement()
   of TAG_LINK:
     let link = HTMLLinkElement()
-    let localName = document.toAtom("rel") #TODO enumize
+    let localName = document.toAtom(atRel)
     link.relList = DOMTokenList(element: link, localName: localName)
     result = link
   of TAG_FORM:
     let form = HTMLFormElement()
-    let localName = document.toAtom("rel") #TODO enumize
+    let localName = document.toAtom(atRel)
     form.relList = DOMTokenList(element: form, localName: localName)
     result = form
   of TAG_TEMPLATE:
@@ -2484,7 +2506,7 @@ proc newHTMLElement*(document: Document, localName: CAtom,
     result = HTMLImageElement()
   of TAG_AREA:
     let area = HTMLAreaElement()
-    let localName = document.toAtom("rel") #TODO enumize
+    let localName = document.toAtom(atRel)
     area.relList = DOMTokenList(element: result, localName: localName)
     result = area
   else:
@@ -2493,7 +2515,7 @@ proc newHTMLElement*(document: Document, localName: CAtom,
   result.namespace = namespace
   result.namespacePrefix = prefix
   result.document_internal = document
-  let localName = document.toAtom("classList") #TODO enumize
+  let localName = document.toAtom(atClassList)
   result.classList = DOMTokenList(element: result, localName: localName)
   result.index = -1
   result.dataset = DOMStringMap(target: result)
@@ -2552,8 +2574,8 @@ func baseURL*(document: Document): URL =
   #TODO frozen base url...
   var href = ""
   for base in document.elements(TAG_BASE):
-    if base.attrb("href"):
-      href = base.attr("href")
+    if base.attrb(atHref):
+      href = base.attr(atHref)
   if href == "":
     return document.url
   if document.url == nil:
@@ -2571,7 +2593,7 @@ func parseURL*(document: Document, s: string): Option[URL] =
   return parseURL(s, some(document.baseURL))
 
 func media*[T: HTMLLinkElement|HTMLStyleElement](element: T): string =
-  return element.attr("media")
+  return element.attr(atMedia)
 
 func title*(document: Document): string =
   for title in document.elements(TAG_TITLE):
@@ -2581,9 +2603,9 @@ func title*(document: Document): string =
 # https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-disabled
 func isDisabled*(option: HTMLOptionElement): bool =
   if option.parentElement of HTMLOptGroupElement and
-      option.parentElement.attrb("disabled"):
+      option.parentElement.attrb(atDisabled):
     return true
-  return option.attrb("disabled")
+  return option.attrb(atDisabled)
 
 func text(option: HTMLOptionElement): string {.jsfget.} =
   var s = ""
@@ -2595,8 +2617,8 @@ func text(option: HTMLOptionElement): string {.jsfget.} =
   return s.stripAndCollapse()
 
 func value*(option: HTMLOptionElement): string {.jsfget.} =
-  if option.attrb("value"):
-    return option.attr("value")
+  if option.attrb(atValue):
+    return option.attr(atValue)
   return option.text
 
 proc invalidateCollections(node: Node) =
@@ -2605,6 +2627,7 @@ proc invalidateCollections(node: Node) =
 
 proc delAttr(element: Element, i: int, keep = false) =
   let map = element.attributesInternal
+  let name = element.attrs[i].qualifiedName
   element.attrs.delete(i) # ordering matters
   if map != nil:
     # delete from attrlist + adjust indices invalidated
@@ -2625,6 +2648,7 @@ proc delAttr(element: Element, i: int, keep = false) =
         )
         attr.dataIdx = 0
       map.attrlist.del(j) # ordering does not matter
+  element.reflectAttrs(name, "")
   element.invalidateCollections()
   element.invalid = true
 
@@ -2683,22 +2707,24 @@ proc style*(element: Element): CSSStyleDeclaration {.jsfget.} =
   return element.style_cached
 
 proc reflectAttrs(element: Element, name: CAtom, value: string) =
-  #TODO enumize
-  let name = element.document.toStr(name)
-  template reflect_str(element: Element, n: static string, val: untyped) =
+  let name = element.document.toAttrType(name)
+  template reflect_str(element: Element, n: AttrType, val: untyped) =
     if name == n:
       element.val = value
       return
-  template reflect_str(element: Element, n: static string, val, fun: untyped) =
+  template reflect_atom(element: Element, n: AttrType, val: untyped) =
+    if name == n:
+      element.val = element.document.toAtom(value)
+      return
+  template reflect_str(element: Element, n: AttrType, val, fun: untyped) =
     if name == n:
       element.val = fun(value)
       return
-  template reflect_bool(element: Element, n: static string, val: untyped) =
+  template reflect_bool(element: Element, n: AttrType, val: untyped) =
     if name == n:
       element.val = true
       return
-  template reflect_domtoklist(element: Element, n: static string,
-      val: untyped) =
+  template reflect_domtoklist(element: Element, n: AttrType, val: untyped) =
     if name == n:
       element.val.toks.setLen(0)
       for x in value.split(AsciiWhitespace):
@@ -2707,46 +2733,42 @@ proc reflectAttrs(element: Element, name: CAtom, value: string) =
           if a notin element.val:
             element.val.toks.add(a)
       return
-  if name == "id":
-    element.id = element.document.toAtom(value)
-    return
-  if name == "name":
-    element.name = element.document.toAtom(value)
-    return
-  element.reflect_domtoklist "class", classList
+  element.reflect_atom atId, id
+  element.reflect_atom atName, name
+  element.reflect_domtoklist atClass, classList
   #TODO internalNonce
-  if name == "style":
+  if name == atStyle:
     element.style_cached = newCSSStyleDeclaration(element, value)
     return
   case element.tagType
   of TAG_INPUT:
     let input = HTMLInputElement(element)
-    input.reflect_str "value", value
-    input.reflect_str "type", inputType, inputType
-    input.reflect_bool "checked", checked
+    input.reflect_str atValue, value
+    input.reflect_str atType, inputType, inputType
+    input.reflect_bool atChecked, checked
   of TAG_OPTION:
     let option = HTMLOptionElement(element)
-    option.reflect_bool "selected", selected
+    option.reflect_bool atSelected, selected
   of TAG_BUTTON:
     let button = HTMLButtonElement(element)
-    button.reflect_str "type", ctype, (func(s: string): ButtonType =
+    button.reflect_str atType, ctype, (func(s: string): ButtonType =
       case s.toLowerAscii()
       of "submit": return BUTTON_SUBMIT
       of "reset": return BUTTON_RESET
       of "button": return BUTTON_BUTTON)
   of TAG_LINK:
     let link = HTMLLinkElement(element)
-    link.reflect_domtoklist "rel", relList
+    link.reflect_domtoklist atRel, relList
   of TAG_A:
     let anchor = HTMLAnchorElement(element)
-    anchor.reflect_domtoklist "rel", relList
+    anchor.reflect_domtoklist atRel, relList
   of TAG_AREA:
     let area = HTMLAreaElement(element)
-    area.reflect_domtoklist "rel", relList
+    area.reflect_domtoklist atRel, relList
   of TAG_CANVAS:
-    if name == "width" or name == "height":
-      let w = element.attrul("width").get(300)
-      let h = element.attrul("height").get(150)
+    if name == atWidth or name == atHeight:
+      let w = element.attrul(atWidth).get(300)
+      let h = element.attrul(atHeight).get(150)
       let canvas = HTMLCanvasElement(element)
       if canvas.bitmap.width != w or canvas.bitmap.height != h:
         canvas.bitmap = newBitmap(w, h)
@@ -2767,6 +2789,9 @@ proc attr*(element: Element, name: CAtom, value: string) =
     ))
   element.reflectAttrs(name, value)
 
+proc attr*(element: Element, name: AttrType, value: string) =
+  element.attr(element.document.toAtom(name), value)
+
 proc attrns*(element: Element, localName: CAtom, prefix: NamespacePrefix,
     namespace: Namespace, value: sink string) =
   if prefix == NO_PREFIX and namespace == NO_NAMESPACE:
@@ -2798,27 +2823,24 @@ proc attrns*(element: Element, localName: CAtom, prefix: NamespacePrefix,
     ))
   element.reflectAttrs(qualifiedName, value)
 
-proc attr*(element: Element, name, value: string) =
-  let name = element.document.toAtom(name)
-  element.attr(name, value)
-
-proc attrl(element: Element, name: string, value: int32) =
+proc attrl(element: Element, name: AttrType, value: int32) =
   element.attr(name, $value)
 
-proc attrul(element: Element, name: string, value: uint32) =
+proc attrul(element: Element, name: AttrType, value: uint32) =
   element.attr(name, $value)
 
-proc attrulgz(element: Element, name: string, value: uint32) =
+proc attrulgz(element: Element, name: AttrType, value: uint32) =
   if value > 0:
     element.attrul(name, value)
 
 proc setAttribute(element: Element, qualifiedName, value: string):
     Err[DOMException] {.jsfunc.} =
   ?validateAttributeName(qualifiedName)
-  let qualifiedName = if element.namespace == Namespace.HTML and not element.document.isxml:
-    qualifiedName.toLowerAscii()
+  let qualifiedName = if element.namespace == Namespace.HTML and
+      not element.document.isxml:
+    element.document.toAtom(qualifiedName.toLowerAscii())
   else:
-    qualifiedName
+    element.document.toAtom(qualifiedName)
   element.attr(qualifiedName, value)
   return ok()
 
@@ -2875,7 +2897,7 @@ proc toggleAttribute(element: Element, qualifiedName: string,
   return ok(true)
 
 proc value(attr: Attr, s: string) {.jsfset.} =
-  attr.ownerElement.attr(attr.name, s)
+  attr.ownerElement.attr(attr.data.qualifiedName, s)
 
 proc setNamedItem(map: NamedNodeMap, attr: Attr): DOMResult[Attr]
     {.jsfunc.} =
@@ -2918,7 +2940,7 @@ proc removeNamedItemNS(map: NamedNodeMap, namespace, localName: string):
   return errDOMException("Item not found", "NotFoundError")
 
 proc jsId(element: Element, id: string) {.jsfset: "id".} =
-  element.attr("id", id)
+  element.attr(atId, id)
 
 # Pass an index to avoid searching for the node in parent's child list.
 proc remove*(node: Node, suppressObservers: bool) =
@@ -2965,17 +2987,17 @@ proc resetElement*(element: Element) =
     let input = HTMLInputElement(element)
     case input.inputType
     of INPUT_SEARCH, INPUT_TEXT, INPUT_PASSWORD:
-      input.value = input.attr("value")
+      input.value = input.attr(atValue)
     of INPUT_CHECKBOX, INPUT_RADIO:
-      input.checked = input.attrb("checked")
+      input.checked = input.attrb(atChecked)
     of INPUT_FILE:
       input.file = none(URL)
     else: discard
     input.invalid = true
   of TAG_SELECT:
     let select = HTMLSelectElement(element)
-    if not select.attrb("multiple"):
-      if select.attrul("size").get(1) == 1:
+    if not select.attrb(atMultiple):
+      if select.attrul(atSize).get(1) == 1:
         var i = 0
         var firstOption: HTMLOptionElement
         for option in select.options:
@@ -3028,12 +3050,11 @@ proc resetFormOwner(element: FormAssociatedElement) =
     if element.tagType notin ListedElements:
       return
     let lastForm = element.findAncestor({TAG_FORM})
-    if not element.attrb("form") and lastForm == element.form:
+    if not element.attrb(atForm) and lastForm == element.form:
       return
   element.form = nil
-  if element.tagType in ListedElements and element.attrb("form") and
-      element.isConnected:
-    let form = element.document.getElementById(element.attr("form"))
+  if element.tagType in ListedElements and element.isConnected:
+    let form = element.document.getElementById(element.attr(atForm))
     if form of HTMLFormElement:
       element.setForm(HTMLFormElement(form))
 
@@ -3269,12 +3290,12 @@ proc reset*(form: HTMLFormElement) =
     control.invalid = true
 
 proc renderBlocking*(element: Element): bool =
-  if "render" in element.attr("blocking").split(AsciiWhitespace):
+  if "render" in element.attr(atBlocking).split(AsciiWhitespace):
     return true
   if element of HTMLScriptElement:
     let element = HTMLScriptElement(element)
     if element.ctype == CLASSIC and element.parserDocument != nil and
-        not element.attrb("async") and not element.attrb("defer"):
+        not element.attrb(atAsync) and not element.attrb(atDefer):
       return true
   return false
 
@@ -3431,17 +3452,18 @@ proc prepare*(element: HTMLScriptElement) =
     return
   let parserDocument = element.parserDocument
   element.parserDocument = nil
-  if parserDocument != nil and not element.attrb("async"):
+  if parserDocument != nil and not element.attrb(atAsync):
     element.forceAsync = true
   let sourceText = element.childTextContent
-  if not element.attrb("src") and sourceText == "":
+  if not element.attrb(atSrc) and sourceText == "":
     return
   if not element.isConnected:
     return
-  let typeString = if element.attr("type") != "":
-    element.attr("type").strip(chars = AsciiWhitespace).toLowerAscii()
-  elif element.attr("language") != "":
-    "text/" & element.attr("language").toLowerAscii()
+  let t = element.attr(atType)
+  let typeString = if t != "":
+    t.strip(chars = AsciiWhitespace).toLowerAscii()
+  elif (let l = element.attr(atLanguage); l != ""):
+    "text/" & l.toLowerAscii()
   else:
     "text/javascript"
   if typeString.isJavaScriptType():
@@ -3461,31 +3483,32 @@ proc prepare*(element: HTMLScriptElement) =
     return
   if not element.scriptingEnabled:
     return
-  if element.attrb("nomodule") and element.ctype == CLASSIC:
+  if element.attrb(atNomodule) and element.ctype == CLASSIC:
     return
   #TODO content security policy
-  if element.ctype == CLASSIC and element.attrb("event") and element.attrb("for"):
-    let f = element.attr("for").strip(chars = AsciiWhitespace)
-    let event = element.attr("event").strip(chars = AsciiWhitespace)
+  if element.ctype == CLASSIC and element.attrb(atEvent) and
+      element.attrb(atFor):
+    let f = element.attr(atFor).strip(chars = AsciiWhitespace)
+    let event = element.attr(atEvent).strip(chars = AsciiWhitespace)
     if not f.equalsIgnoreCase("window"):
       return
     if not event.equalsIgnoreCase("onload") and not event.equalsIgnoreCase("onload()"):
       return
-  let cs = getCharset(element.attr("charset"))
+  let cs = getCharset(element.attr(atCharset))
   let encoding = if cs != CHARSET_UNKNOWN: cs else: element.document.charset
   let classicCORS = element.crossOrigin
   var options = ScriptOptions(
     nonce: element.internalNonce,
-    integrity: element.attr("integrity"),
+    integrity: element.attr(atIntegrity),
     parserMetadata: if element.parserDocument != nil: PARSER_INSERTED else: NOT_PARSER_INSERTED,
     referrerpolicy: element.referrerpolicy
   )
   #TODO settings object
-  if element.attrb("src"):
+  if element.attrb(atSrc):
     if element.ctype == IMPORTMAP:
       #TODO fire error event
       return
-    let src = element.attr("src")
+    let src = element.attr(atSrc)
     if src == "":
       #TODO fire error event
       return
@@ -3500,7 +3523,8 @@ proc prepare*(element: HTMLScriptElement) =
     if element in element.document.renderBlockingElements:
       options.renderBlocking = true
     if element.ctype == CLASSIC:
-      element.fetchClassicScript(url.get, options, classicCORS, encoding, markAsReady)
+      element.fetchClassicScript(url.get, options, classicCORS, encoding,
+        markAsReady)
     else:
       element.fetchExternalModuleGraph(url.get, options, markAsReady)
   else:
@@ -3512,9 +3536,10 @@ proc prepare*(element: HTMLScriptElement) =
     else:
       #TODO MODULE, IMPORTMAP
       element.markAsReady(ScriptResult(t: RESULT_NULL))
-  if element.ctype == CLASSIC and element.attrb("src") or element.ctype == MODULE:
+  if element.ctype == CLASSIC and element.attrb(atSrc) or
+      element.ctype == MODULE:
     let prepdoc = element.preparationTimeDocument
-    if element.attrb("async"):
+    if element.attrb(atAsync):
       prepdoc.scriptsToExecSoon.add(element)
       element.onReady = (proc() =
         element.execute()
@@ -3532,7 +3557,7 @@ proc prepare*(element: HTMLScriptElement) =
             script.execute()
             prepdoc.scriptsToExecInOrder.shrink(1)
       )
-    elif element.ctype == MODULE or element.attrb("defer"):
+    elif element.ctype == MODULE or element.attrb(atDefer):
       element.parserDocument.scriptsToExecOnLoad.addFirst(element)
       element.onReady = (proc() =
         element.readyForParserExec = true
diff --git a/src/html/enums.nim b/src/html/enums.nim
index 1ef29400..baa948a0 100644
--- a/src/html/enums.nim
+++ b/src/html/enums.nim
@@ -30,6 +30,64 @@ type
     DOCUMENT_FRAGMENT_NODE = 11,
     NOTATION_NODE = 12
 
+  AttrType* = enum
+    atUnknown = ""
+    atAction = "action"
+    atAlign = "align"
+    atAlt = "alt"
+    atAsync = "async"
+    atBgcolor = "bgcolor"
+    atBlocking = "blocking"
+    atCharset = "charset"
+    atChecked = "checked"
+    atClass = "class"
+    atClassList
+    atColor = "color"
+    atCols = "cols"
+    atColspan = "colspan"
+    atCrossorigin = "crossorigin"
+    atDefer = "defer"
+    atDirname = "dirname"
+    atDisabled = "disabled"
+    atEnctype = "enctype"
+    atEvent = "event"
+    atFor = "for"
+    atForm = "form"
+    atFormaction = "formaction"
+    atFormenctype = "formenctype"
+    atFormmethod = "formmethod"
+    atHeight = "height"
+    atHref = "href"
+    atId = "id"
+    atIntegrity = "integrity"
+    atIsmap = "ismap"
+    atLanguage = "language"
+    atMedia = "media"
+    atMethod = "method"
+    atMultiple = "multiple"
+    atName = "name"
+    atNomodule = "nomodule"
+    atReferrerpolicy = "referrerpolicy"
+    atRel = "rel"
+    atRequired = "required"
+    atRows = "rows"
+    atRowspan = "rowspan"
+    atSelected = "selected"
+    atSize = "size"
+    atSizes = "sizes"
+    atSlot = "slot"
+    atSrc = "src"
+    atSrcset = "srcset"
+    atStyle = "style"
+    atTarget = "target"
+    atText = "text"
+    atTitle = "title"
+    atType = "type"
+    atUsemap = "usemap"
+    atValign = "valign"
+    atValue = "value"
+    atWidth = "width"
+
 const InputTypeWithSize* = {
   INPUT_SEARCH, INPUT_TEXT, INPUT_EMAIL, INPUT_PASSWORD, INPUT_URL, INPUT_TEL
 }
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 919604c7..704a5d1d 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -291,14 +291,15 @@ func getTitleAttr(node: StyledNode): string =
     return ""
   if node.t == STYLED_ELEMENT and node.node != nil:
     let element = Element(node.node)
-    if element.attrb("title"):
-      return element.attr("title")
+    if element.attrb(atTitle):
+      return element.attr(atTitle)
   if node.node != nil:
     var node = node.node
     for element in node.ancestors:
-      if element.attrb("title"):
-        return element.attr("title")
+      if element.attrb(atTitle):
+        return element.attr(atTitle)
   #TODO pseudo-elements
+  return ""
 
 const ClickableElements = {
   TAG_A, TAG_INPUT, TAG_OPTION, TAG_BUTTON, TAG_TEXTAREA, TAG_LABEL
@@ -702,7 +703,7 @@ proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.pr
 
 proc loadResource(buffer: Buffer, link: HTMLLinkElement): EmptyPromise =
   let document = buffer.document
-  let href = link.attr("href")
+  let href = link.attr(atHref)
   if href == "": return
   let url = parseURL(href, document.url.some)
   if url.isSome:
@@ -734,7 +735,7 @@ proc loadResource(buffer: Buffer, link: HTMLLinkElement): EmptyPromise =
 
 proc loadResource(buffer: Buffer, elem: HTMLImageElement): EmptyPromise =
   let document = buffer.document
-  let src = elem.attr("src")
+  let src = elem.attr(atSrc)
   if src == "": return
   let url = parseURL(src, document.url.some)
   if url.isSome:
@@ -767,7 +768,7 @@ proc loadResources(buffer: Buffer): EmptyPromise =
       case elem.tagType
       of TAG_LINK:
         let elem = HTMLLinkElement(elem)
-        if elem.attr("rel") == "stylesheet":
+        if elem.attr(atRel) == "stylesheet":
           p = buffer.loadResource(elem)
       of TAG_IMG:
         let elem = HTMLImageElement(elem)
@@ -1497,7 +1498,7 @@ proc click(buffer: Buffer, select: HTMLSelectElement): ClickResult =
       selected.add(i)
     inc i
   let select = SelectResult(
-    multiple: select.attrb("multiple"),
+    multiple: select.attrb(atMultiple),
     options: options,
     selected: selected
   )
diff --git a/src/xhr/formdata.nim b/src/xhr/formdata.nim
index 7d83d5b2..77430509 100644
--- a/src/xhr/formdata.nim
+++ b/src/xhr/formdata.nim
@@ -112,24 +112,23 @@ proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
   var entrylist: seq[FormDataEntry]
   for field in form.controls:
     if field.findAncestor({TAG_DATALIST}) != nil or
-        field.attrb("disabled") or
+        field.attrb(atDisabled) or
         field.isButton() and Element(field) != submitter:
       continue
 
     if field.tagType == TAG_INPUT:
       let field = HTMLInputElement(field)
       if field.inputType == INPUT_IMAGE:
-        let name = if field.attr("name") != "":
-          field.attr("name") & '.'
-        else:
-          ""
+        var name = field.attr(atName)
+        if name != "":
+          name &= '.'
         entrylist.add((name & 'x', $field.xcoord))
         entrylist.add((name & 'y', $field.ycoord))
         continue
 
     #TODO custom elements
 
-    let name = field.attr("name")
+    let name = field.attr(atName)
 
     if name == "":
       continue
@@ -139,16 +138,21 @@ proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
       for option in field.options:
         if option.selected or option.isDisabled:
           entrylist.add((name, option.value))
-    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}:
-      let value = if field.attr("value") != "":
-        field.attr("value")
+    elif field of HTMLInputElement and
+        HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}:
+      let v = field.attr(atValue)
+      let value = if v != "":
+        v
       else:
         "on"
       entrylist.add((name, value))
-    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_FILE:
+    elif field of HTMLInputElement and
+        HTMLInputElement(field).inputType == INPUT_FILE:
       #TODO file
       discard
-    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"):
+    elif field of HTMLInputElement and
+        HTMLInputElement(field).inputType == INPUT_HIDDEN and
+        name.equalsIgnoreCase("_charset_"):
       let charset = if encoding != "":
         encoding
       else:
@@ -163,10 +167,11 @@ proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
       of TAG_TEXTAREA:
         entrylist.add((name, HTMLTextAreaElement(field).value))
       else: assert false, "Tag type " & $field.tagType & " not accounted for in constructEntryList"
-    if field.tagType == TAG_TEXTAREA or
-        field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}:
-      if field.attr("dirname") != "":
-        let dirname = field.attr("dirname")
+    if field of HTMLTextAreaElement or
+        field of HTMLInputElement and
+        HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}:
+      let dirname = field.attr(atDirname)
+      if dirname != "":
         let dir = "ltr" #TODO bidi
         entrylist.add((dirname, dir))