docs: def createGroupsForCommonAttributes(elem):
      """
      Creates <g> elements to contain runs of 3 or more
      consecutive child elements having at least one common attribute.
  
      Common attributes are not promoted to the <g> by this function.
      This is handled by moveCommonAttributesToParentGroup.
  
      If all children have a common attribute, an extra <g> is not created.
  
      This function acts recursively on the given element.
      """
      num = 0
      global _num_elements_removed
  
      # TODO perhaps all of the Presentation attributes in http://www.w3.org/TR/SVG/struct.html#GElement
      # could be added here
      # Cyn: These attributes are the same as in moveAttributesToParentGroup, and must always be
      for curAttr in ['clip-rule',
                      'display-align',
                      'fill', 'fill-opacity', 'fill-rule',
                      'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch',
                      'font-style', 'font-variant', 'font-weight',
                      'letter-spacing',
                      'pointer-events', 'shape-rendering',
                      'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin',
                      'stroke-miterlimit', 'stroke-opacity', 'stroke-width',
                      'text-anchor', 'text-decoration', 'text-rendering', 'visibility',
                      'word-spacing', 'writing-mode']:
          # Iterate through the children in reverse order, so item(i) for
          # items we have yet to visit still returns the correct nodes.
          curChild = elem.childNodes.length - 1
          while curChild >= 0:
              childNode = elem.childNodes.item(curChild)
  
              if (
                  childNode.nodeType == Node.ELEMENT_NODE and
                  childNode.getAttribute(curAttr) != '' and
                  childNode.nodeName in [
                      # only attempt to group elements that the content model allows to be children of a <g>
  
                      # SVG 1.1 (see https://www.w3.org/TR/SVG/struct.html#GElement)
                      'animate', 'animateColor', 'animateMotion', 'animateTransform', 'set',  # animation elements
                      'desc', 'metadata', 'title',                                            # descriptive elements
                      'circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect',     # shape elements
                      'defs', 'g', 'svg', 'symbol', 'use',                                    # structural elements
                      'linearGradient', 'radialGradient',                                     # gradient elements
                      'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter',
                      'font', 'font-face', 'foreignObject', 'image', 'marker', 'mask',
                      'pattern', 'script', 'style', 'switch', 'text', 'view',
  
                      # SVG 1.2 (see https://www.w3.org/TR/SVGTiny12/elementTable.html)
                      'animation', 'audio', 'discard', 'handler', 'listener',
                      'prefetch', 'solidColor', 'textArea', 'video'
                  ]
              ):
                  # We're in a possible run! Track the value and run length.
                  value = childNode.getAttribute(curAttr)
                  runStart, runEnd = curChild, curChild
                  # Run elements includes only element tags, no whitespace/comments/etc.
                  # Later, we calculate a run length which includes these.
                  runElements = 1
  
                  # Backtrack to get all the nodes having the same
                  # attribute value, preserving any nodes in-between.
                  while runStart > 0:
                      nextNode = elem.childNodes.item(runStart - 1)
                      if nextNode.nodeType == Node.ELEMENT_NODE:
                          if nextNode.getAttribute(curAttr) != value:
                              break
                          else:
                              runElements += 1
                              runStart -= 1
                      else:
                          runStart -= 1
  
                  if runElements >= 3:
                      # Include whitespace/comment/etc. nodes in the run.
                      while runEnd < elem.childNodes.length - 1:
                          if elem.childNodes.item(runEnd + 1).nodeType == Node.ELEMENT_NODE:
                              break
                          else:
                              runEnd += 1
  
                      runLength = runEnd - runStart + 1
                      if runLength == elem.childNodes.length:  # Every child has this
                          # If the current parent is a <g> already,
                          if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']:
                              # do not act altogether on this attribute; all the
                              # children have it in common.
                              # Let moveCommonAttributesToParentGroup do it.
                              curChild = -1
                              continue
                          # otherwise, it might be an <svg> element, and
                          # even if all children have the same attribute value,
                          # it's going to be worth making the <g> since
                          # <svg> doesn't support attributes like 'stroke'.
                          # Fall through.
  
                      # Create a <g> element from scratch.
                      # We need the Document for this.
                      document = elem.ownerDocument
                      group = document.createElementNS(NS['SVG'], 'g')
                      # Move the run of elements to the group.
                      # a) ADD the nodes to the new group.
                      group.childNodes[:] = elem.childNodes[runStart:runEnd + 1]
                      for child in group.childNodes:
                          child.parentNode = group
                      # b) REMOVE the nodes from the element.
                      elem.childNodes[runStart:runEnd + 1] = []
                      # Include the group in elem's children.
                      elem.childNodes.insert(runStart, group)
                      group.parentNode = elem
                      num += 1
                      curChild = runStart - 1
                      _num_elements_removed -= 1
                  else:
                      curChild -= 1
              else:
                  curChild -= 1
  
      # each child gets the same treatment, recursively
      for childNode in elem.childNodes:
          if childNode.nodeType == Node.ELEMENT_NODE:
              num += createGroupsForCommonAttributes(childNode)
  
      return num