module m_common_element

#ifndef DUMMYLIB
  ! Structure and manipulation of element specification

  use fox_m_fsys_array_str, only: str_vs, vs_str_alloc, vs_vs_alloc
  use fox_m_fsys_string_list, only: string_list, init_string_list, &
    destroy_string_list, add_string, tokenize_to_string_list, &
    registered_string
  use m_common_charset, only: isInitialNameChar, isNameChar, &
    upperCase, XML_WHITESPACE
  use m_common_content_model, only: content_particle_t, newCP, destroyCPtree, &
    OP_MIXED, OP_CHOICE, OP_SEQ, OP_NAME, &
    REP_QUESTION_MARK, REP_ASTERISK, &
    transformCPPlus, dumpCPtree
  use m_common_error, only: error_stack, add_error, in_error
  use m_common_namecheck, only: checkName, checkNames, checkNCName, &
    checkNCNames, checkQName, checkNmtoken, checkNmtokens

  implicit none
  private

  integer, parameter :: ST_START               = 0
  integer, parameter :: ST_EMPTYANY            = 1
  integer, parameter :: ST_FIRSTCHILD          = 2
  integer, parameter :: ST_END                 = 3
  integer, parameter :: ST_PCDATA              = 4
  integer, parameter :: ST_NAME                = 5
  integer, parameter :: ST_CHILD               = 6
  integer, parameter :: ST_AFTERBRACKET        = 7
  integer, parameter :: ST_AFTERLASTBRACKET    = 8
  integer, parameter :: ST_SEPARATOR           = 9
  integer, parameter :: ST_AFTERNAME           = 10
  integer, parameter :: ST_ATTTYPE             = 11
  integer, parameter :: ST_AFTER_NOTATION      = 12
  integer, parameter :: ST_NOTATION_LIST       = 13
  integer, parameter :: ST_ENUMERATION         = 14
  integer, parameter :: ST_ENUM_NAME           = 15
  integer, parameter :: ST_AFTER_ATTTYPE_SPACE = 16
  integer, parameter :: ST_AFTER_ATTTYPE       = 17
  integer, parameter :: ST_DEFAULT_DECL        = 18
  integer, parameter :: ST_AFTERDEFAULTDECL    = 19
  integer, parameter :: ST_DEFAULTVALUE        = 20

  integer, parameter :: ATT_NULL = 0

  integer, parameter :: ATT_CDATA = 1
  integer, parameter :: ATT_ID = 2
  integer, parameter :: ATT_IDREF = 3
  integer, parameter :: ATT_IDREFS = 4
  integer, parameter :: ATT_ENTITY = 5
  integer, parameter :: ATT_ENTITIES = 6
  integer, parameter :: ATT_NMTOKEN = 7
  integer, parameter :: ATT_NMTOKENS = 8
  integer, parameter :: ATT_NOTATION = 9
  integer, parameter :: ATT_ENUM = 10
  integer, parameter :: ATT_CDANO = 11
  integer, parameter :: ATT_CDAMB = 12

  character(len=8), parameter :: ATT_TYPES(12) = (/ &
    "CDATA   ", &
    "ID      ", &
    "IDREF   ", &
    "IDREFS  ", &
    "ENTITY  ", &
    "ENTITIES", &
    "NMTOKEN ", &
    "NMTOKENS", &
    "NOTATION", &
    "ENUM    ", &
    "CDANO   ", &
    "CDAMB   "/)

  integer, parameter :: ATT_REQUIRED = 1
  integer, parameter :: ATT_IMPLIED = 2
  integer, parameter :: ATT_DEFAULT = 4
  integer, parameter :: ATT_FIXED = 3


  type attribute_t
    character, pointer :: name(:) => null()
    integer :: attType = ATT_NULL 
    integer :: attDefault = ATT_NULL
    type(string_list) :: enumerations
    character, pointer :: default(:) => null()
    logical :: internal = .true.
  end type attribute_t

  type attribute_list
    type(attribute_t), pointer :: list(:) => null()
  end type attribute_list

  type element_t
    character, pointer :: name(:) => null()
    logical :: empty = .false.
    logical :: any = .false.
    logical :: mixed = .false.
    logical :: id_declared = .false.
    logical :: internal = .true.
    type (content_particle_t), pointer :: cp => null()
    character, pointer :: model(:) => null()
    type(attribute_list) :: attlist
  end type element_t

  type element_list
    type(element_t), pointer :: list(:) => null()
  end type element_list


  public :: element_t
  public :: element_list

  public :: attribute_t
  public :: attribute_list

  public :: init_element_list
  public :: destroy_element_list
  public :: existing_element
  public :: declared_element
  public :: get_element
  public :: add_element

  public :: parse_dtd_element

  public :: init_attribute_list
  public :: destroy_attribute_list


  public :: parse_dtd_attlist

  public :: report_declarations

  public :: attribute_has_default
  public :: get_attlist_size
  public :: get_attribute_declaration
  public :: express_attribute_declaration

  public :: att_value_normalize

  public :: get_att_type_enum

  public :: ATT_NULL
  public :: ATT_CDATA
  public :: ATT_ID 
  public :: ATT_IDREF
  public :: ATT_IDREFS
  public :: ATT_ENTITY
  public :: ATT_ENTITIES
  public :: ATT_NMTOKEN
  public :: ATT_NMTOKENS
  public :: ATT_NOTATION
  public :: ATT_ENUM

  public :: ATT_CDANO
  public :: ATT_CDAMB

  public :: ATT_REQUIRED
  public :: ATT_IMPLIED
  public :: ATT_DEFAULT
  public :: ATT_FIXED

  public :: ATT_TYPES

  interface get_attribute_declaration
    module procedure get_attdecl_by_index
    module procedure get_attdecl_by_name
  end interface

contains

  subroutine init_element_list(e_list)
    type(element_list), intent(inout) :: e_list

    allocate(e_list%list(0))
  end subroutine init_element_list

  subroutine destroy_element_list(e_list)
    type(element_list), intent(inout) :: e_list

    integer :: i

    do i = 1, size(e_list%list)
      deallocate(e_list%list(i)%name)
      if (associated(e_list%list(i)%cp)) call destroyCPtree(e_list%list(i)%cp)
      if (associated(e_list%list(i)%model)) deallocate(e_list%list(i)%model)
      call destroy_attribute_list(e_list%list(i)%attlist)
    enddo
    deallocate(e_list%list)
  end subroutine destroy_element_list

  function existing_element(e_list, name) result(p)
    type(element_list), intent(in) :: e_list
    character(len=*), intent(in) :: name
    logical :: p

    integer :: i

    p = .false.
    do i = 1, size(e_list%list)
      if (str_vs(e_list%list(i)%name)==name) then
        p = .true.
        exit
      endif
    enddo
  end function existing_element

  function declared_element(e_list, name) result(p)
    type(element_list), intent(in) :: e_list
    character(len=*), intent(in) :: name
    logical :: p

    integer :: i

    p = .false.
    do i = 1, size(e_list%list)
      if (str_vs(e_list%list(i)%name)==name) then
        p = associated(e_list%list(i)%model)
        exit
      endif
    enddo
  end function declared_element

  function get_element(e_list, name) result(e)
    type(element_list), intent(in) :: e_list
    character(len=*), intent(in) :: name
    type(element_t), pointer :: e

    integer :: i

    do i = 1, size(e_list%list)
      if (str_vs(e_list%list(i)%name)==name) then
        e => e_list%list(i)
        return
      endif
    enddo
    e => null()
  end function get_element

  function add_element(e_list, name) result(e)
    type(element_list), intent(inout) :: e_list
    character(len=*), intent(in) :: name
    type(element_t), pointer :: e

    type(element_t), pointer :: temp(:)
    integer :: i

    temp => e_list%list

    allocate(e_list%list(size(temp)+1))
    do i = 1, size(temp)
      e_list%list(i)%name => temp(i)%name
      e_list%list(i)%model => temp(i)%model
      e_list%list(i)%empty = temp(i)%empty
      e_list%list(i)%any = temp(i)%any
      e_list%list(i)%mixed = temp(i)%mixed
      e_list%list(i)%cp => temp(i)%cp
      e_list%list(i)%id_declared = temp(i)%id_declared
      e_list%list(i)%internal = temp(i)%internal
      e_list%list(i)%attlist%list => temp(i)%attlist%list
    enddo
    deallocate(temp)
    e => e_list%list(i)
    e%name => vs_str_alloc(name)
    call init_attribute_list(e%attlist)

  end function add_element

  subroutine parse_dtd_element(contents, xv, stack, element, internal)
    character(len=*), intent(in) :: contents
    integer, intent(in) :: xv
    type(error_stack), intent(inout) :: stack
    type(element_t), pointer :: element
    logical, intent(in) :: internal

    integer :: state
    integer :: i, nbrackets
    logical :: mixed, empty, any
    character :: c
    character, pointer :: order(:), name(:), temp(:)
    type(content_particle_t), pointer :: top, current, tcp
    logical :: mixed_additional, firstChild

    ! FIXME should we check namespaces here (for element names)
    ! checking duplicates - valid or wf? - and only for MIXED?

    order => null()
    name => null()
    temp => null()

    any = .false.
    empty = .false.
    mixed = .false.
    nbrackets = 0
    mixed_additional = .false.
    firstChild = .true.
    state = ST_START

    top => null()

    do i = 1, len(contents) + 1
      if (i<=len(contents)) then
        c = contents(i:i)
      else
        c = ' '
      endif

      if (state==ST_START) then
        !write(*,*)'ST_START'
        if (verify(c, XML_WHITESPACE)==0) then
          continue
        elseif (verify(c, 'EMPTYANY')==0) then
          name => vs_str_alloc(c)
          state = ST_EMPTYANY
        elseif (c=='(') then
          order => vs_str_alloc(" ")
          nbrackets = 1
          top => newCP()
          current => top
          state = ST_FIRSTCHILD
        else
          call add_error(stack, &
            'Unexpected character "'//c//'" at start of ELEMENT specification')
          goto 100
        endif

      elseif (state==ST_EMPTYANY) then
        !write(*,*)'ST_EMPTYANY'
        if (verify(c, upperCase)==0) then
          temp => name
          name => vs_str_alloc(str_vs(temp)//c)
          deallocate(temp)
        elseif (verify(c, XML_WHITESPACE)==0) then
          if (str_vs(name)=='EMPTY') then
            empty = .true.
            top => newCP(empty=.true.)
            current => top
          elseif (str_vs(name)=='ANY') then
            any = .true.
            top => newCP(any=.true.)
            current => top
          else
            call add_error(stack, &
              'Unexpected ELEMENT specification; expecting EMPTY or ANY')
            goto 100
          endif
          deallocate(name)
          state = ST_END
        else
          call add_error(stack, &
            'Unexpected ELEMENT specification; expecting EMPTY or ANY')
          goto 100
        endif

      elseif (state==ST_FIRSTCHILD) then
        !write(*,*)'ST_FIRSTCHILD'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (c=='#') then
          mixed = .true.
          state = ST_PCDATA
          name => vs_str_alloc("")
        elseif (isInitialNameChar(c, xv)) then
          allocate(name(1))
          name(1) = c
          state = ST_NAME
        elseif (c=='(') then
          nbrackets = nbrackets + 1
          deallocate(order)
          tcp => newCP()
          current%firstChild => tcp
          tcp%parent => current
          current => tcp
          order => vs_str_alloc("  ")
          state = ST_CHILD
        else
          call add_error(stack, &
            'Unexpected character in ELEMENT specification')
          goto 100
        endif

      elseif (state==ST_PCDATA) then
        !write(*,*)'ST_PCDATA'
        if (verify(c, 'PCDATA')==0) then
          temp => name
          name => vs_str_alloc(str_vs(temp)//c)
          deallocate(temp)
        elseif (verify(c, XML_WHITESPACE)==0) then
          if (str_vs(name)=='PCDATA') then
            deallocate(name)
          else
            call add_error(stack, &
              'Unexpected token after #')
            goto 100
          endif
          ! Must be first child
          current%operator = OP_MIXED
          tcp => newCP(name="#PCDATA")
          current%firstChild => tcp
          tcp%parent => current
          current => tcp
          firstChild = .false.
          state = ST_SEPARATOR
        elseif (c==')') then
          if (str_vs(name)=='PCDATA') then
            deallocate(name)
            nbrackets = 0
            state = ST_AFTERLASTBRACKET
            deallocate(order)
          else
            call add_error(stack, &
              'Unexpected token after #')
            goto 100
          endif
          ! Must be first child
          current%operator = OP_MIXED
          tcp => newCP(name="#PCDATA")
          current%firstChild => tcp
          tcp%parent => current
          firstChild = .false.
        elseif (c=='|') then
          if (str_vs(name)=='PCDATA') then
            firstChild = .false.
            deallocate(name)
          else
            call add_error(stack, &
              'Unexpected token after #')
            goto 100
          endif
          ! Must be first child
          current%operator = OP_MIXED
          tcp => newCP(name="#PCDATA")
          current%firstChild => tcp
          tcp%parent => current
          current => tcp
          firstChild = .false.
          order(1) = '|'
          state = ST_CHILD
        elseif (c==',') then
          call add_error(stack, &
            'Ordered specification not allowed for Mixed elements')
          goto 100
        else
          call add_error(stack, &
            'Unexpected character in ELEMENT specification')
          goto 100
        endif

      elseif (state==ST_NAME) then
        !write(*,*)'ST_NAME'
        if (isNameChar(c, xv)) then
          temp => name
          name => vs_str_alloc(str_vs(temp)//c)
          deallocate(temp)
        elseif (scan(c, "?+*")>0) then
          if (mixed) then
            call add_error(stack, &
              'Repeat operators forbidden for Mixed elements')
            goto 100
          endif
          tcp => newCP(name=str_vs(name), repeat=c)
          deallocate(name)
          if (firstChild) then
            current%firstChild => tcp
            tcp%parent => current
            firstChild = .false.
          else
            current%nextSibling => tcp
            tcp%parent => current%parent
          endif
          current => tcp
          if (c=="+") call transformCPPlus(current)
          state = ST_SEPARATOR
        elseif (verify(c, XML_WHITESPACE)==0) then
          if (mixed) mixed_additional = .true.
          tcp => newCP(name=str_vs(name))
          deallocate(name)
          if (firstChild) then
            current%firstChild => tcp
            tcp%parent => current
            firstChild = .false.
          else
            current%nextSibling => tcp
            tcp%parent => current%parent
          endif
          current => tcp
          state = ST_SEPARATOR
        elseif (scan(c,',|')>0) then
          if (order(nbrackets)=='') then
            order(nbrackets)=c
          elseif (order(nbrackets)/=c) then
            call add_error(stack, &
              'Cannot mix ordered and unordered elements')
            goto 100
          endif
          if (mixed) mixed_additional = .true.
          tcp => newCP(name=str_vs(name))
          deallocate(name)
          if (firstChild) then
            current%firstChild => tcp
            tcp%parent => current
            firstChild = .false.
          else
            current%nextSibling => tcp
            tcp%parent => current%parent
          endif
          current => tcp
          if (c=="|".and.current%parent%operator/=OP_MIXED) &
            current%parent%operator = OP_CHOICE
          state = ST_CHILD
        elseif (c==')') then
          if (mixed) mixed_additional = .true.
          nbrackets = nbrackets - 1
          if (nbrackets==0) then
            state = ST_AFTERLASTBRACKET
            deallocate(order)
          else
            temp => order
            allocate(order(nbrackets))
            order = temp(:size(order))
            deallocate(temp)
            state = ST_AFTERBRACKET
          endif
          tcp => newCP(name=str_vs(name))
          deallocate(name)
          if (firstChild) then
            current%firstChild => tcp
            tcp%parent => current
            firstChild = .false.
          else
            current%nextSibling => tcp
            tcp%parent => current%parent
            current => current%parent
            if (.not.check_duplicates(current)) &
              goto 100
          endif
        else
          call add_error(stack, &
            'Unexpected character found after element name')
          goto 100
        endif

      elseif (state==ST_CHILD) then
        !write(*,*)'ST_CHILD'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (c=='#') then
          call add_error(stack, &
            '# forbidden except as first child element')
          goto 100
        elseif (isInitialNameChar(c, xv)) then
          name => vs_str_alloc(c)
          state = ST_NAME
        elseif (c=='(') then
          if (mixed) then
            call add_error(stack, &
              'Nested brackets forbidden for Mixed content')
            goto 100
          endif
          tcp => newCP()
          if (firstChild) then
            current%firstChild => tcp
            tcp%parent => current
          else
            current%nextSibling => tcp
            tcp%parent => current%parent
            firstChild = .true.
          endif
          current => tcp
          nbrackets = nbrackets + 1
          temp => order
          order => vs_str_alloc(str_vs(temp)//" ")
          deallocate(temp)
        else
          call add_error(stack, &
            'Unexpected character "'//c//'" found after (')
          goto 100
        endif

      elseif (state==ST_SEPARATOR) then
        !write(*,*)'ST_SEPARATOR'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (c=='#') then
          call add_error(stack, &
            '#PCDATA must be first in list')
          goto 100
        elseif (scan(c,'|,')>0) then
          if (order(nbrackets)=='') then
            order(nbrackets) = c
          elseif (order(nbrackets)/=c) then
            call add_error(stack, &
              'Cannot mix ordered and unordered elements')
            goto 100
          endif
          if (c=="|".and.current%parent%operator/=OP_MIXED) &
            current%parent%operator = OP_CHOICE
          state = ST_CHILD
        elseif (c==')') then
          nbrackets = nbrackets - 1
          if (nbrackets==0) then
            state = ST_AFTERLASTBRACKET
            deallocate(order)
          else
            temp => order
            allocate(order(nbrackets))
            order = temp(:size(order))
            deallocate(temp)
            state = ST_AFTERBRACKET
          endif
          current => current%parent
          if (.not.check_duplicates(current)) &
            goto 100
        else
          call add_error(stack, &
            'Unexpected character found in element declaration.')
          goto 100
        endif

      elseif (state==ST_AFTERBRACKET) then
        !write(*,*)'ST_AFTERBRACKET'
        if (c=='*') then
          current%repeater = REP_ASTERISK
          state = ST_SEPARATOR
        elseif (c=='+') then
          call transformCPPlus(current)
          state = ST_SEPARATOR
        elseif (c=='?') then
          current%repeater = REP_QUESTION_MARK
          state = ST_SEPARATOR
        elseif (verify(c, XML_WHITESPACE)==0) then
          state = ST_SEPARATOR
        elseif (scan(c,'|,')>0) then
          if (order(nbrackets)=='') then
            order(nbrackets) = c
          elseif (order(nbrackets)/=c) then
            call add_error(stack, &
              'Cannot mix ordered and unordered elements')
            goto 100
          endif
          if (c=="|".and.current%parent%operator/=OP_MIXED) &
            current%parent%operator = OP_CHOICE
          state = ST_CHILD
        elseif (c==')') then
          nbrackets = nbrackets - 1
          if (nbrackets==0) then
            deallocate(order)
            state = ST_AFTERLASTBRACKET
          else
            temp => order
            allocate(order(nbrackets))
            order = temp(:size(order))
            deallocate(temp)
            state = ST_AFTERBRACKET
          endif
          current => current%parent
          if (.not.check_duplicates(current)) &
            goto 100
        else
          call add_error(stack, &
            'Unexpected character "'//c//'"found after ")"')
          goto 100
        endif

      elseif (state==ST_AFTERLASTBRACKET) then
        !write(*,*)'ST_AFTERLASTBRACKET'
        if (c=='*') then
          state = ST_END
          current%repeater = REP_ASTERISK
        elseif (c=='+') then
          if (mixed) then
            call add_error(stack, &
              '+ operator disallowed for Mixed elements')
            goto 100
          endif
          call transformCPPlus(current)
          state = ST_END
        elseif (c=='?') then
          if (mixed) then
            call add_error(stack, &
              '? operator disallowed for Mixed elements')
            goto 100
          endif
          current%repeater = REP_QUESTION_MARK
          state = ST_END
        elseif (verify(c, XML_WHITESPACE)==0) then
          if (mixed) then
            if (mixed_additional) then
              call add_error(stack, &
                'Missing "*" at end of Mixed element specification')
              goto 100
            endif
          endif
          state = ST_END
        else
          call add_error(stack, &
            'Unexpected character "'//c//'" found after final ")"')
          goto 100
        endif

      elseif (state==ST_END) then
        !write(*,*)'ST_END'
        if (verify(c, XML_WHITESPACE)==0) then
          continue
        else
          call add_error(stack, &
            'Unexpected token found after end of element specification')
          goto 100
        endif

      endif

    enddo

    if (state/=ST_END) then
      call add_error(stack, "Error in parsing contents of element declaration")
      goto 100
    endif

    if (associated(element)) then
      element%any = any
      element%empty = empty
      element%mixed = mixed
      element%model => vs_str_alloc(trim(strip_spaces(contents)))
      element%cp => top
      element%internal = internal
!      call dumpCPtree(top)
    else
      if (associated(top)) call destroyCPtree(top)
    endif
    return

100 if (associated(order)) deallocate(order)
    if (associated(name)) deallocate(name)
    if (associated(top)) call destroyCPtree(top)

    contains
      function strip_spaces(s1) result(s2)
        character(len=*) :: s1
        character(len=len(s1)) :: s2
        integer :: i, i2
        i2 = 1
        do i = 1, len(s1)
          if (verify(s1(i:i), XML_WHITESPACE)==0) cycle
          s2(i2:i2) = s1(i:i)
          i2 = i2 + 1
        end do
        s2(i2:) = ''
      end function strip_spaces

      function check_duplicates(cp) result(p)
        type(content_particle_t), pointer :: cp
        logical :: p

        type(string_list) :: sl
        type(content_particle_t), pointer :: tcp

        if (cp%operator==OP_SEQ) then
          p = .true.
          return
        endif

        call init_string_list(sl)
        tcp => cp%firstChild
        p = .false.
        do while (associated(tcp))
          if (tcp%operator==OP_NAME) then
            if (registered_string(sl, str_vs(tcp%name))) then
              call destroy_string_list(sl)
              if (cp%operator==OP_MIXED) then
                call add_error(stack, &
                  "Duplicate element names found in MIXED")
              elseif (cp%operator==OP_CHOICE) then
                call add_error(stack, &
                  "Duplicate element names found in CHOICE ("//str_vs(tcp%name)//")")
              endif
              return
            else
              call add_string(sl, str_vs(tcp%name))
            endif
          endif
          tcp => tcp%nextSibling
        enddo
        p = .true.
        call destroy_string_list(sl)
      end function check_duplicates
  end subroutine parse_dtd_element


  subroutine init_attribute_list(a_list)
    type(attribute_list), intent(inout) :: a_list

    allocate(a_list%list(0))
  end subroutine init_attribute_list

  subroutine destroy_attribute_t(a)
    type(attribute_t), pointer :: a 

    if (associated(a%name)) deallocate(a%name)
    if (associated(a%default)) deallocate(a%default)
    call destroy_string_list(a%enumerations)

    deallocate(a)
  end subroutine destroy_attribute_t

  subroutine destroy_attribute_list(a_list)
    type(attribute_list), intent(inout) :: a_list

    integer :: i

    do i = 1, size(a_list%list)
      deallocate(a_list%list(i)%name)
      if (associated(a_list%list(i)%default)) deallocate(a_list%list(i)%default)
      call destroy_string_list(a_list%list(i)%enumerations)
    enddo
    deallocate(a_list%list)

  end subroutine destroy_attribute_list

  function existing_attribute(a_list, name) result(p)
    type(attribute_list), intent(inout) :: a_list
    character(len=*), intent(in) :: name
    logical :: p

    integer :: i
    p = .false.
    do i = 1, size(a_list%list)
      p = (str_vs(a_list%list(i)%name)==name)
      if (p) exit
    enddo
  end function existing_attribute

  function add_attribute(a_list, name, internal) result(a)
    type(attribute_list), intent(inout) :: a_list
    character(len=*), intent(in) :: name
    logical, intent(in) :: internal
    type(attribute_t), pointer :: a

    integer :: i
    type(attribute_t), pointer :: temp(:)

    temp => a_list%list
    allocate(a_list%list(size(temp)+1))
    do i = 1, size(temp)
      a_list%list(i)%name => temp(i)%name
      a_list%list(i)%atttype = temp(i)%atttype
      a_list%list(i)%attdefault = temp(i)%attdefault
      a_list%list(i)%default => temp(i)%default
      a_list%list(i)%enumerations%list => temp(i)%enumerations%list
      a_list%list(i)%internal = temp(i)%internal
    enddo
    deallocate(temp)
    a => a_list%list(i)

    a%name => vs_str_alloc(name)
    call init_string_list(a%enumerations)
    a%internal = internal

  end function add_attribute
  
  function get_attribute(a_list, name) result(a)
    type(attribute_list), intent(inout) :: a_list
    character(len=*), intent(in) :: name
    type(attribute_t), pointer :: a

    integer :: i
    do i = 1, size(a_list%list)
      if (str_vs(a_list%list(i)%name)==name) then
        a => a_list%list(i)
        exit
      endif
    enddo
  end function get_attribute

  subroutine parse_dtd_attlist(contents, xv, namespaces, validCheck, stack, elem, internal)
    character(len=*), intent(in) :: contents
    integer, intent(in) :: xv
    logical, intent(in) :: validCheck
    logical, intent(in) :: namespaces
    type(error_stack), intent(inout) :: stack
    type(element_t), pointer :: elem
    logical, intent(in) :: internal

    integer :: i
    integer :: state
    character :: c, q
    character, pointer :: name(:), attType(:), default(:), value(:), temp(:)

    type(attribute_t), pointer :: ca
    type(attribute_t), pointer :: ignore_att

    ignore_att => null()
    ! We need ignore_att to process but not take account of duplicate attributes
    ! elem is optional so we can not record declarations if necessary.
    ca => null()
    name => null()
    attType => null()
    default => null()
    value => null()
    temp => null()

    state = ST_START

    do i = 1, len(contents) + 1
      if (in_error(stack)) exit
      if (i<=len(contents)) then
        c = contents(i:i)
      else
        c = " "
      endif

      if (state==ST_START) then
        !write(*,*)'ST_START'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (isInitialNameChar(c, xv)) then
          name => vs_str_alloc(c)
          state = ST_NAME
        else
          call add_error(stack, &
            'Unexpected character in Attlist')
        endif

      elseif (state==ST_NAME) then
        !write(*,*)'ST_NAME'
        if (isNameChar(c, xv)) then
          temp => vs_str_alloc(str_vs(name)//c)
          deallocate(name)
          name => temp
        elseif (verify(c, XML_WHITESPACE)==0) then
          if (namespaces.and..not.checkQName(str_vs(name), xv)) then
            call add_error(stack, &
              "Attribute name in ATTLIST must be QName")
          elseif (associated(elem)) then
            if (existing_attribute(elem%attlist, str_vs(name))) then
              if (associated(ignore_att)) call destroy_attribute_t(ignore_att)
              allocate(ignore_att)
              call init_string_list(ignore_att%enumerations)
              ignore_att%name => vs_vs_alloc(name)
              ca => ignore_att
            else
              ca => add_attribute(elem%attlist, str_vs(name), internal)
            endif
          else
            if (associated(ignore_att)) call destroy_attribute_t(ignore_att)
            allocate(ignore_att)
            call init_string_list(ignore_att%enumerations)
            ignore_att%name => vs_vs_alloc(name)
            ca => ignore_att
          endif
          deallocate(name)
          state = ST_AFTERNAME
        else
          call add_error(stack, &
            'Unexpected character in Attlist Name')
        endif

      elseif (state==ST_AFTERNAME) then
        !write(*,*)'ST_AFTERNAME'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (verify(c, upperCase)==0) then
          attType => vs_str_alloc(c)
          state = ST_ATTTYPE
        elseif (c=='(') then
          allocate(value(0))
          ca%attType = ATT_ENUM
          state = ST_ENUMERATION
        else
          call add_error(stack, &
            'Unexpected error after Attlist Name')
        endif

      elseif (state==ST_ATTTYPE) then
        !write(*,*)'ST_ATTTYPE'
        if (verify(c, upperCase)==0) then
          temp => attType
          attType => vs_str_alloc(str_vs(temp)//c)
          deallocate(temp)
        elseif (verify(c, XML_WHITESPACE)==0) then
          ! xml:id constraint
          if (str_vs(ca%name)=="xml:id" &
            .and..not.str_vs(attType)=="ID") then
            call add_error(stack, &
              "xml:id attribute must be declared as type ID")
          elseif (str_vs(attType)=='CDATA') then
            ca%attType = ATT_CDATA
            state = ST_AFTER_ATTTYPE
          elseif (str_vs(attType)=='ID') then
            if (validCheck) then
              ! Validity Constraint: One ID per Element Type
              if (associated(elem)) then
                if (elem%id_declared) then
                  call add_error(stack, &
                    "Cannot have two declared attributes of type ID on one element type.")
                else
                  elem%id_declared = .true.
                endif
              endif
            endif
            ca%attType = ATT_ID
            state = ST_AFTER_ATTTYPE
          elseif (str_vs(attType)=='IDREF') then
            ca%attType = ATT_IDREF
            state = ST_AFTER_ATTTYPE
          elseif (str_vs(attType)=='IDREFS') then
            ca%attType = ATT_IDREFS
            state = ST_AFTER_ATTTYPE
          elseif (str_vs(attType)=='ENTITY') then
            ca%attType = ATT_ENTITY
            state = ST_AFTER_ATTTYPE
          elseif (str_vs(attType)=='ENTITIES') then
            ca%attType = ATT_ENTITIES
            state = ST_AFTER_ATTTYPE
          elseif (str_vs(attType)=='NMTOKEN') then
            ca%attType = ATT_NMTOKEN
            state = ST_AFTER_ATTTYPE
          elseif (str_vs(attType)=='NMTOKENS') then
            ca%attType = ATT_NMTOKENS
            state = ST_AFTER_ATTTYPE
          elseif (str_vs(attType)=='NOTATION') then
            ca%attType = ATT_NOTATION
            state = ST_AFTER_NOTATION
          else
            call add_error(stack, &
              'Unknown AttType')
          endif
          deallocate(attType)
        else
          call add_error(stack, &
            'Unexpected character in AttType')
        endif

      elseif (state==ST_AFTER_NOTATION) then
        !write(*,*)'ST_AFTER_NOTATION'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (c=='(') then
          state = ST_NOTATION_LIST
        else
          call add_error(stack, &
            'Unexpected character after Notation')
        endif

      elseif (state==ST_NOTATION_LIST) then
        !write(*,*)'ST_NOTATION_LIST'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (isInitialNameChar(c, xv)) then
          value => vs_str_alloc(c)
          state = ST_ENUM_NAME
        else
          call add_error(stack, &
            'Unexpected character in Notation list')
        endif

      elseif (state==ST_ENUMERATION) then
        !write(*,*)'ST_ENUMERATION'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (isNameChar(c, xv)) then
          temp => vs_str_alloc(str_vs(value)//c)
          deallocate(value)
          value => temp
          state = ST_ENUM_NAME
        elseif (c=='|') then
          call add_error(stack, &
            "Missing token in Enumeration")
        elseif (c==')') then
          call add_error(stack, &
            "Missing tokens in Enumeration")
        else
          call add_error(stack, &
            'Unexpected character in attlist enumeration')
        endif

      elseif (state==ST_ENUM_NAME) then
        !write(*,*)'ST_ENUM_NAME'
        if (isNameChar(c, xv)) then
          temp => vs_str_alloc(str_vs(value)//c)
          deallocate(value)
          value => temp
        elseif (verify(c, XML_WHITESPACE)==0) then
          if (validCheck.and.registered_string(ca%enumerations, str_vs(value))) then
            call add_error(stack, &
              "Duplicate enumeration value in ATTLIST")
          elseif (namespaces.and.ca%attType==ATT_NOTATION &
            .and..not.checkNCName(str_vs(value), xv)) then
            call add_error(stack, &
              "Notation name must be NCName")
          else
            call add_string(ca%enumerations, str_vs(value))
          endif
          deallocate(value)
          state = ST_SEPARATOR
        elseif (c=='|') then
          if (validCheck.and.registered_string(ca%enumerations, str_vs(value))) then
            call add_error(stack, &
              "Duplicate enumeration value in ATTLIST")
          elseif (namespaces.and.ca%attType==ATT_NOTATION &
            .and..not.checkNCName(str_vs(value), xv)) then
            call add_error(stack, &
              "Notation name must be NCName")
          else
            call add_string(ca%enumerations, str_vs(value))
          endif
          deallocate(value)
          if (ca%attType==ATT_NOTATION) then
            state = ST_NOTATION_LIST
          else
            allocate(value(0))
            state = ST_ENUMERATION
          endif
        elseif (c==')') then
          if (size(value)==0) then
            call add_error(stack, &
              'Missing token in Enumeration list')
          endif
          if (validCheck.and.registered_string(ca%enumerations, str_vs(value))) then
            call add_error(stack, &
              "Duplicate enumeration value in ATTLIST")
          elseif (namespaces.and.ca%attType==ATT_NOTATION &
            .and..not.checkNCName(str_vs(value), xv)) then
            call add_error(stack, &
              "Notation name must be NCName")
          else
            call add_string(ca%enumerations, str_vs(value))
          endif
          deallocate(value)
          state = ST_AFTER_ATTTYPE_SPACE
        else
          call add_error(stack, &
            'Unexpected character in attlist enumeration')
        endif

      elseif (state==ST_SEPARATOR) then
        !write(*,*)'ST_SEPARATOR'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (c=='|') then
          if (ca%attType==ATT_NOTATION) then
            state = ST_NOTATION_LIST
          else
            allocate(value(0))
            state = ST_ENUMERATION
          endif
        elseif (c==')') then
          state = ST_AFTER_ATTTYPE_SPACE
        else
          call add_error(stack, &
            'Unexpected character in attlist enumeration')
        endif

      elseif (state==ST_AFTER_ATTTYPE_SPACE) then
        if (verify(c, XML_WHITESPACE)/=0) then
          call add_error(stack, &
            'Missing whitespace in attlist enumeration')
        endif
        state = ST_AFTER_ATTTYPE

      elseif (state==ST_AFTER_ATTTYPE) then
        !write(*,*)'ST_AFTER_ATTTYPE'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (c=='#') then
          allocate(default(0))
          state = ST_DEFAULT_DECL
        elseif (c=='"'.or.c=="'") then
          if (validCheck) then
            ! Validity Constraint: ID Attribute Default
            if (ca%attType==ATT_ID) &
              call add_error(stack, &
              "Attribute of type ID may not have default value")
          endif
          ca%attDefault = ATT_DEFAULT
          q = c
          allocate(value(0))
          state = ST_DEFAULTVALUE
        else
          call add_error(stack, &
            'Unexpected character after AttType')
        endif

      elseif (state==ST_DEFAULT_DECL) then
        !write(*,*)'ST_DEFAULT_DECL'
        if (verify(c, upperCase)==0) then
          temp => vs_str_alloc(str_vs(default)//c)
          deallocate(default)
          default => temp
        elseif (verify(c, XML_WHITESPACE)==0) then
          if (str_vs(default)=='REQUIRED') then
            ca%attdefault = ATT_REQUIRED
            deallocate(default)
            state = ST_START
          elseif (str_vs(default)=='IMPLIED') then
            ca%attdefault = ATT_IMPLIED
            deallocate(default)
            state = ST_START
          elseif (str_vs(default)=='FIXED') then
            if (validCheck) then
              ! Validity Constraint: ID Attribute Default
              if (ca%attType==ATT_ID) &
                call add_error(stack, &
                "Attribute of type ID may not have FIXED value")
            endif
            ca%attdefault = ATT_FIXED
            deallocate(default)
            state = ST_AFTERDEFAULTDECL
          else
            call add_error(stack, &
              'Unknown Default declaration')
          endif
        else
          call add_error(stack, &
            'Unexpected character in Default declaration')
        endif

      elseif (state==ST_AFTERDEFAULTDECL) then
        !write(*,*)'ST_AFTERDEFAULTDECL'
        if (verify(c, XML_WHITESPACE)==0) cycle
        if (c=='"') then
          q = c
          allocate(value(0))
          state = ST_DEFAULTVALUE
        elseif (c=="'") then
          q = c
          allocate(value(0))
          state = ST_DEFAULTVALUE
        else
          call add_error(stack, &
            'Unexpected character after Default declaration')
        endif

      elseif (state==ST_DEFAULTVALUE) then
        !write(*,*)'ST_DEFAULTVALUE'
        if (c==q) then
          if (ca%attType/=ATT_CDATA) then
            temp => vs_str_alloc(att_value_normalize(str_vs(value)))
            deallocate(value)
            value => temp
          endif
          if (validCheck) then
            select case(ca%attType)
              ! Can't have ID with defaults
            case (ATT_IDREF)
              ! VC: IDREF
              if (namespaces) then
                if (.not.checkNCName(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type IDREF must have a value which is an XML NCName")
              else
                if (.not.checkName(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type IDREF must have a value which is an XML Name")
              endif
            case (ATT_IDREFS)
              ! VC: IDREF
              if (namespaces) then
                if (.not.checkNCNames(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type IDREFS must have a value which contains only XML NCNames")
              else
                if (.not.checkNames(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type IDREFS must have a value which contains only XML Names")
              endif
            case (ATT_ENTITY)
              ! VC: Entity Name
              if (namespaces) then
                if (.not.checkNCName(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type ENTITY must have a value which is an XML NCName")
              else
                if (.not.checkName(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type ENTITY must have a value which is an XML Name")
              endif
            case (ATT_ENTITIES)
              ! VC: Entity Name
              if (namespaces) then
                if (.not.checkNames(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type ENTITIES must have a value which contains only XML NCNames")
              else
                if (.not.checkNames(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type ENTITIES must have a value which contains only XML Names")
              endif
            case (ATT_NMTOKEN)
              ! VC Name Token
              if (.not.checkNmtoken(str_vs(value), xv)) &
                call add_error(stack, &
                "Attributes of type NMTOKEN must have a value which is a NMTOKEN")
            case (ATT_NMTOKENS)
              ! VC: Name Token
              if (.not.checkNmtokens(str_vs(value), xv)) &
                call add_error(stack, &
                "Attributes of type NMTOKENS must have a value which contain only NMTOKENs")
            case (ATT_NOTATION)
              ! VC: Notation Attributes
              if (namespaces) then
                if (.not.checkNCName(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type NOTATION must have a value which is an XMLNCName")
              else
                if (.not.checkName(str_vs(value), xv)) &
                  call add_error(stack, &
                  "Attributes of type NOTATION must have a value which is an XML Name")
              endif
            case (ATT_ENUM)
              ! VC: Enumeration
              if (.not.checkNmtoken(str_vs(value), xv)) &
                call add_error(stack, &
                "Attributes of type ENUM must have a value which is an NMTOKENs")
              if (.not.registered_string(ca%enumerations, str_vs(value))) &
                call add_error(stack, &
                "Default value of ENUM does not match permitted values")
            end select
          endif
          if (.not.in_error(stack)) then
            if (ca%attType==ATT_ENTITIES) then
              call destroy_string_list(ca%enumerations)
              ca%enumerations = tokenize_to_string_list(str_vs(value))
            endif
            ca%default => value
            value => null()
            state = ST_START
          endif
        else
          temp => vs_str_alloc(str_vs(value)//c)
          deallocate(value)
          value => temp
        endif

      endif

    enddo

    if (associated(ignore_att)) call destroy_attribute_t(ignore_att)

    if (.not.in_error(stack)) then
      if (state==ST_START) then
        return
      else
        call add_error(stack, &
          'Incomplete Attlist declaration')
      endif
    endif
    
    if (associated(name)) deallocate(name)
    if (associated(attType)) deallocate(attType)
    if (associated(default)) deallocate(default)
    if (associated(value)) deallocate(value)

  end subroutine parse_dtd_attlist

  subroutine report_declarations(elem, attributeDecl_handler)
    type(element_t), intent(in) :: elem
    interface
      subroutine attributeDecl_handler(eName, aName, type, mode, value)
        character(len=*), intent(in) :: eName
        character(len=*), intent(in) :: aName
        character(len=*), intent(in) :: type
        character(len=*), intent(in), optional :: mode
        character(len=*), intent(in), optional :: value
      end subroutine attributeDecl_handler
    end interface

    integer :: i
    character(len=8) :: type
    character(len=8) :: mode
    type(attribute_t), pointer :: a

    do i = 1, size(elem%attlist%list)
      a => elem%attlist%list(i)
      type = ATT_TYPES(a%attType)
      select case (a%attDefault)
      case (ATT_REQUIRED)
        mode = "REQUIRED"
      case (ATT_IMPLIED)
        mode = "IMPLIED"
      case (ATT_FIXED)
        mode = "FIXED"
      end select

      if (a%attType==ATT_NOTATION) then
        if (a%attDefault==ATT_DEFAULT) then
          if (associated(a%default)) then
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              'NOTATION '//make_token_group(a%enumerations), value=str_vs(a%default))
          else
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              'NOTATION '//make_token_group(a%enumerations))
          endif
        else
          if (associated(a%default)) then
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              'NOTATION '//make_token_group(a%enumerations), mode=trim(mode), &
              value=str_vs(a%default))
          else
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              'NOTATION '//make_token_group(a%enumerations), mode=trim(mode))
          endif
        endif
      elseif (a%attType==ATT_ENUM) then
        if (a%attDefault==ATT_DEFAULT) then
          if (associated(a%default)) then
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              make_token_group(a%enumerations), value=str_vs(a%default))
          else
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              make_token_group(a%enumerations))
          endif
        else
          if (associated(a%default)) then
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              make_token_group(a%enumerations), mode=trim(mode), &
              value=str_vs(a%default))
          else
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              make_token_group(a%enumerations), mode=trim(mode))
          endif
        endif
      else
        if (a%attDefault==ATT_DEFAULT) then
          if (associated(a%default)) then
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              trim(type), value=str_vs(a%default))
          else
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              trim(type))
          endif
        else
          if (associated(a%default)) then
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              trim(type), mode=trim(mode), value=str_vs(a%default))
          else
            call attributeDecl_handler(str_vs(elem%name), str_vs(a%name), &
              trim(type), mode=trim(mode))
          endif
        endif
      endif
    enddo


  end subroutine report_declarations

  pure function make_token_group_len(s_list) result(n)
    type(string_list), intent(in) :: s_list
    integer :: n

    integer :: i
    n = size(s_list%list) + 1
    do i = 1, size(s_list%list)
      n = n + size(s_list%list(i)%s)
    enddo
  end function make_token_group_len

  function make_token_group(s_list) result(s)
    type(string_list), intent(in) :: s_list
    character(len=make_token_group_len(s_list)) :: s
    
    integer :: i, m, n
    s(1:1) = '('
    n = 2
    do i = 1, size(s_list%list)-1
      m = size(s_list%list(i)%s)
      s(n:n+m) = str_vs(s_list%list(i)%s)//'|'
      n = n + m + 1
    enddo
    s(n:) = str_vs(s_list%list(i)%s)//')'
  end function make_token_group

  function attribute_has_default(att) result(p)
    type(attribute_t), pointer :: att
    logical :: p

    if (associated(att)) then
      p = att%attDefault==ATT_DEFAULT.or.att%attDefault==ATT_FIXED
    else
      p = .false.
    endif
  end function attribute_has_default

  function get_attlist_size(elem) result(n)
    type(element_t), pointer :: elem
    integer :: n

    if (associated(elem)) then
      n = size(elem%attlist%list)
    else
      n = 0
    endif
  end function get_attlist_size

  function get_attdecl_by_index(elem, n) result(att)
    type(element_t), pointer :: elem
    integer, intent(in) :: n
    type(attribute_t), pointer :: att

    att => null()
    if (associated(elem)) then
      if (n>0.and.n<=size(elem%attlist%list)) then
        att => elem%attlist%list(n)
      endif
    endif
  end function get_attdecl_by_index

  function get_attdecl_by_name(elem, name) result(att)
    type(element_t), pointer :: elem
    character(len=*), intent(in) :: name
    type(attribute_t), pointer :: att

    integer :: i
    att => null()
    if (associated(elem)) then
      do i = 1, size(elem%attlist%list)
        if (str_vs(elem%attlist%list(i)%name)==name) then
          att => elem%attlist%list(i)
          return
        endif
      enddo
    endif
  end function get_attdecl_by_name

  pure function express_att_decl_len(a) result(n)
    type(attribute_t), intent(in) :: a
    integer :: n

    if (a%attType==ATT_ENUM) then
      n = size(a%name)
    else
      n = size(a%name)+1+len_trim(ATT_TYPES(a%attType))
    endif

    if (a%attType==ATT_NOTATION &
      .or.a%attType==ATT_ENUM) &
      n = n + 1 + make_token_group_len(a%enumerations)

    select case(a%attDefault)
    case (ATT_REQUIRED)
      n = n + len(" #REQUIRED")
    case (ATT_IMPLIED)
      n = n + len(" #IMPLIED")
    case (ATT_DEFAULT)
      n = n + len(" ")
    case (ATT_FIXED)
      n = n + len(" #FIXED")
    end select
    
    if (associated(a%default)) &
      n = n + 3 + size(a%default)
  end function express_att_decl_len

  function express_attribute_declaration(a) result(s)
    type(attribute_t), intent(in) :: a
    character(len=express_att_decl_len(a)) :: s

    if (a%attType==ATT_ENUM) then
      s = str_vs(a%name)
    else
      s = str_vs(a%name)//" "//ATT_TYPES(a%attType)
    endif
    if (a%attType==ATT_NOTATION &
      .or.a%attType==ATT_ENUM) &
      s = trim(s)//" "//make_token_group(a%enumerations)

    select case(a%attDefault)
    case (ATT_REQUIRED)
      s = trim(s)//" #REQUIRED"
    case (ATT_IMPLIED)
      s = trim(s)//" #IMPLIED"
    case (ATT_DEFAULT)
      s = trim(s)//" "
    case (ATT_FIXED)
      s = trim(s)//" #FIXED"
    end select
    
    if (associated(a%default)) &
      s = trim(s)//" """//str_vs(a%default)//""""
  end function express_attribute_declaration

  function get_att_type_enum(s) result(n)
    character(len=*), intent(in) :: s
    integer :: n

    select case(s)
    case ('CDATA')
      n = ATT_CDATA
    case ('ID')
      n = ATT_ID
    case ('IDREF')
      n = ATT_IDREF
    case ('IDREFS')
      n = ATT_IDREFS
    case ('NMTOKEN')
      n = ATT_NMTOKEN
    case ('NMTOKENS')
      n = ATT_NMTOKENS
    case ('ENTITY')
      n = ATT_ENTITY
    case ('ENTITIES')
      n = ATT_ENTITIES
    case ('NOTATION')
      n = ATT_NOTATION
    case ('CDANO')
      n= ATT_CDANO
    case ('CDAMB')
      n = ATT_CDAMB
    end select
  end function get_att_type_enum

  pure function att_value_normalize_len(s1) result(n)
    character(len=*), intent(in) :: s1
    integer :: n

    integer :: i
    logical :: w

    n = 0
    w = .true.
    do i = 1, len(s1)
      if (w.and.(verify(s1(i:i),XML_WHITESPACE)==0)) cycle
      w = .false.
      n = n + 1
      if (verify(s1(i:i),XML_WHITESPACE)==0) w = .true.
    enddo
    if (w) n = n - 1 ! Discard final space
    
  end function att_value_normalize_len

  function att_value_normalize(s1) result(s2)
    character(len=*), intent(in) :: s1
    character(len=att_value_normalize_len(s1)) :: s2
    
    integer :: i, i2
    logical :: w
    
    i = 0
    i2 = 1
    w = .true.
    do while (i2<=len(s2))
      i = i + 1
      if (w.and.(verify(s1(i:i),XML_WHITESPACE)==0)) cycle
      w = .false.
      s2(i2:i2) = s1(i:i)
      i2 = i2 + 1
      if (verify(s1(i:i),XML_WHITESPACE)==0) w = .true.
    enddo

  end function att_value_normalize

#endif
end module m_common_element
