LCOV - code coverage report
Current view: top level - src/io - hsd_formatter.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 94.2 % 173 163
Test Date: 2026-02-04 13:26:36 Functions: 100.0 % 11 11

            Line data    Source code
       1              : !> HSD Formatter/Serializer
       2              : !>
       3              : !> This module provides functionality to write HSD data structures back to
       4              : !> text format.
       5              : module hsd_formatter
       6              :   use hsd_constants, only: dp, sp, CHAR_NEWLINE, CHAR_DQUOTE, CHAR_SQUOTE, CHAR_BACKSLASH
       7              :   use hsd_types, only: hsd_node, hsd_table, hsd_value, hsd_iterator, &
       8              :     VALUE_TYPE_NONE, VALUE_TYPE_ARRAY, VALUE_TYPE_STRING, &
       9              :     VALUE_TYPE_INTEGER, VALUE_TYPE_REAL, VALUE_TYPE_LOGICAL, VALUE_TYPE_COMPLEX
      10              :   use hsd_error, only: hsd_error_t, HSD_STAT_OK, HSD_STAT_IO_ERROR, make_error
      11              :   implicit none (type, external)
      12              :   private
      13              : 
      14              :   public :: hsd_dump, hsd_dump_to_string
      15              : 
      16              :   !> Indentation string (2 spaces)
      17              :   character(len=*), parameter :: INDENT_STR = "  "
      18              : 
      19              :   !> Characters that require quoting
      20              :   character(len=*), parameter :: QUOTE_TRIGGER_CHARS = "{}[]= " // char(9)
      21              : 
      22              : contains
      23              : 
      24              :   !> Write HSD table to a file
      25           27 :   subroutine hsd_dump(root, filename, error)
      26              :     type(hsd_table), intent(in) :: root
      27              :     character(len=*), intent(in) :: filename
      28              :     type(hsd_error_t), allocatable, intent(out), optional :: error
      29              : 
      30           27 :     integer :: unit_num, io_stat
      31              :     character(len=256) :: io_msg
      32              : 
      33              :     open(newunit=unit_num, file=filename, status='replace', action='write', &
      34           27 :          iostat=io_stat, iomsg=io_msg)
      35           27 :     if (io_stat /= 0) then
      36            1 :       if (present(error)) then
      37              :         call make_error(error, HSD_STAT_IO_ERROR, &
      38            1 :           "Cannot open file for writing: " // trim(io_msg), filename)
      39              :       end if
      40            1 :       return
      41              :     end if
      42              : 
      43           26 :     call write_table_content(unit_num, root, 0)
      44              : 
      45           26 :     close(unit_num)
      46              : 
      47           27 :   end subroutine hsd_dump
      48              : 
      49              :   !> Write HSD table to a string (dynamically allocated)
      50         1061 :   subroutine hsd_dump_to_string(root, output)
      51              :     type(hsd_table), intent(in) :: root
      52              :     character(len=:), allocatable, intent(out) :: output
      53              : 
      54              :     ! Start with empty string and build up dynamically
      55         1061 :     output = ""
      56              : 
      57         1061 :     call write_table_to_string(root, 0, output)
      58              : 
      59           27 :   end subroutine hsd_dump_to_string
      60              : 
      61              :   !> Write table contents to unit
      62           30 :   recursive subroutine write_table_content(unit_num, table, indent_level)
      63              :     integer, intent(in) :: unit_num
      64              :     type(hsd_table), intent(in) :: table
      65              :     integer, intent(in) :: indent_level
      66              : 
      67           30 :     integer :: i
      68              :     class(hsd_node), pointer :: child
      69           30 :     character(len=:), allocatable :: indent
      70              : 
      71           33 :     indent = repeat(INDENT_STR, indent_level)
      72              : 
      73           71 :     do i = 1, table%num_children
      74           41 :       call table%get_child(i, child)
      75           41 :       if (.not. associated(child)) cycle
      76              : 
      77           30 :       select type (child)
      78              :       type is (hsd_table)
      79           11 :         call write_table_node(unit_num, child, indent_level)
      80              :       type is (hsd_value)
      81           30 :         call write_value_node(unit_num, child, indent_level)
      82              :       end select
      83              :     end do
      84              : 
      85         1091 :   end subroutine write_table_content
      86              : 
      87              :   !> Write a table node
      88           11 :   recursive subroutine write_table_node(unit_num, table, indent_level)
      89              :     integer, intent(in) :: unit_num
      90              :     type(hsd_table), intent(in) :: table
      91              :     integer, intent(in) :: indent_level
      92              : 
      93           11 :     character(len=:), allocatable :: indent, attrib_str
      94              : 
      95           12 :     indent = repeat(INDENT_STR, indent_level)
      96              : 
      97              :     ! Build attribute string
      98           11 :     if (table%has_attrib()) then
      99            1 :       attrib_str = " [" // table%get_attrib() // "]"
     100              :     else
     101           10 :       attrib_str = ""
     102              :     end if
     103              : 
     104              :     ! Check if table has single child (for = syntax)
     105           11 :     if (table%num_children == 1) then
     106              :       block
     107              :         class(hsd_node), pointer :: single_child
     108            9 :         call table%get_child(1, single_child)
     109              : 
     110              :         select type (single_child)
     111              :         type is (hsd_table)
     112              :           ! Tag = ChildTag { ... }
     113            2 :           if (allocated(table%name) .and. len_trim(table%name) > 0) then
     114              :             write(unit_num, '(A)') indent // trim(table%name) // attrib_str // &
     115            1 :               " = " // trim(single_child%name) // " {"
     116            1 :             call write_table_content(unit_num, single_child, indent_level + 1)
     117            1 :             write(unit_num, '(A)') indent // "}"
     118              :           else
     119              :             ! Unnamed table, just write children
     120            1 :             call write_table_content(unit_num, table, indent_level)
     121              :           end if
     122           11 :           return
     123              : 
     124              :         type is (hsd_value)
     125              :           ! Tag = value
     126            7 :           if (allocated(table%name) .and. len_trim(table%name) > 0) then
     127            7 :             call write_tag_value(unit_num, table%name, attrib_str, &
     128           14 :                                  single_child, indent_level)
     129              :           else
     130            0 :             call write_value_node(unit_num, single_child, indent_level)
     131              :           end if
     132           14 :           return
     133              :         end select
     134              :       end block
     135              :     end if
     136              : 
     137              :     ! Regular block: Tag { ... }
     138            2 :     if (allocated(table%name) .and. len_trim(table%name) > 0) then
     139            2 :       write(unit_num, '(A)') indent // trim(table%name) // attrib_str // " {"
     140            2 :       call write_table_content(unit_num, table, indent_level + 1)
     141            2 :       write(unit_num, '(A)') indent // "}"
     142              :     else
     143              :       ! Root or unnamed table - just write content
     144            0 :       call write_table_content(unit_num, table, indent_level)
     145              :     end if
     146              : 
     147           11 :   end subroutine write_table_node
     148              : 
     149              :   !> Write a value node
     150           30 :   subroutine write_value_node(unit_num, val, indent_level)
     151              :     integer, intent(in) :: unit_num
     152              :     type(hsd_value), intent(in) :: val
     153              :     integer, intent(in) :: indent_level
     154              : 
     155           30 :     character(len=:), allocatable :: indent, attrib_str, value_str
     156              : 
     157           35 :     indent = repeat(INDENT_STR, indent_level)
     158              : 
     159              :     ! Build attribute string
     160           30 :     if (val%has_attrib()) then
     161            2 :       attrib_str = " [" // val%get_attrib() // "]"
     162              :     else
     163           28 :       attrib_str = ""
     164              :     end if
     165              : 
     166              :     ! Get value string
     167           30 :     value_str = format_value(val)
     168              : 
     169              :     ! Write
     170           30 :     if (allocated(val%name) .and. len_trim(val%name) > 0) then
     171           30 :       if (index(value_str, CHAR_NEWLINE) > 0) then
     172              :         ! Multi-line value
     173            6 :         write(unit_num, '(A)') indent // trim(val%name) // attrib_str // " {"
     174            6 :         call write_multiline(unit_num, value_str, indent_level + 1)
     175            6 :         write(unit_num, '(A)') indent // "}"
     176              :       else
     177              :         ! Single-line value
     178           24 :         write(unit_num, '(A)') indent // trim(val%name) // attrib_str // " = " // value_str
     179              :       end if
     180              :     else
     181              :       ! Anonymous value (data content)
     182            0 :       if (index(value_str, CHAR_NEWLINE) > 0) then
     183            0 :         call write_multiline(unit_num, value_str, indent_level)
     184              :       else
     185            0 :         write(unit_num, '(A)') indent // value_str
     186              :       end if
     187              :     end if
     188              : 
     189           30 :   end subroutine write_value_node
     190              : 
     191              :   !> Write tag = value
     192            7 :   subroutine write_tag_value(unit_num, name, attrib_str, val, indent_level)
     193              :     integer, intent(in) :: unit_num
     194              :     character(len=*), intent(in) :: name
     195              :     character(len=*), intent(in) :: attrib_str
     196              :     type(hsd_value), intent(in) :: val
     197              :     integer, intent(in) :: indent_level
     198              : 
     199            7 :     character(len=:), allocatable :: indent, value_str, val_attrib
     200              : 
     201            8 :     indent = repeat(INDENT_STR, indent_level)
     202            7 :     value_str = format_value(val)
     203              : 
     204              :     ! Combine attributes
     205            7 :     if (val%has_attrib()) then
     206            0 :       val_attrib = " [" // val%get_attrib() // "]"
     207              :     else
     208            7 :       val_attrib = attrib_str
     209              :     end if
     210              : 
     211            7 :     if (index(value_str, CHAR_NEWLINE) > 0) then
     212              :       ! Multi-line value
     213            1 :       write(unit_num, '(A)') indent // trim(name) // val_attrib // " {"
     214            1 :       call write_multiline(unit_num, value_str, indent_level + 1)
     215            1 :       write(unit_num, '(A)') indent // "}"
     216              :     else
     217            6 :       write(unit_num, '(A)') indent // trim(name) // val_attrib // " = " // value_str
     218              :     end if
     219              : 
     220           37 :   end subroutine write_tag_value
     221              : 
     222              :   !> Write multi-line content
     223            7 :   subroutine write_multiline(unit_num, text, indent_level)
     224              :     integer, intent(in) :: unit_num
     225              :     character(len=*), intent(in) :: text
     226              :     integer, intent(in) :: indent_level
     227              : 
     228            7 :     character(len=:), allocatable :: indent
     229            7 :     integer :: pos, next_pos, text_len
     230              : 
     231           14 :     indent = repeat(INDENT_STR, indent_level)
     232            7 :     text_len = len(text)
     233            7 :     pos = 1
     234              : 
     235           21 :     do while (pos <= text_len)
     236           21 :       next_pos = index(text(pos:), CHAR_NEWLINE)
     237           21 :       if (next_pos > 0) then
     238           14 :         next_pos = pos + next_pos - 1
     239           14 :         if (next_pos > pos) then
     240           11 :           write(unit_num, '(A)') indent // text(pos:next_pos-1)
     241              :         else
     242            3 :           write(unit_num, '(A)') ""
     243              :         end if
     244           14 :         pos = next_pos + 1
     245              :       else
     246            7 :         write(unit_num, '(A)') indent // text(pos:)
     247            7 :         exit
     248              :       end if
     249              :     end do
     250              : 
     251           14 :   end subroutine write_multiline
     252              : 
     253              :   !> Format a value for output
     254        12117 :   function format_value(val) result(str)
     255              :     type(hsd_value), intent(in) :: val
     256              :     character(len=:), allocatable :: str
     257              : 
     258              :     character(len=64) :: buffer
     259              : 
     260        12121 :     select case (val%value_type)
     261              :     case (VALUE_TYPE_LOGICAL)
     262            8 :       if (val%logical_value) then
     263            2 :         str = "Yes"
     264              :       else
     265            2 :         str = "No"
     266              :       end if
     267              : 
     268              :     case (VALUE_TYPE_INTEGER)
     269         1016 :       write(buffer, '(I0)') val%int_value
     270         1016 :       str = trim(adjustl(buffer))
     271              : 
     272              :     case (VALUE_TYPE_REAL)
     273           12 :       write(buffer, '(G0)') val%real_value
     274           12 :       str = trim(adjustl(buffer))
     275              :       ! Ensure we have a decimal point for whole numbers
     276           12 :       if (index(str, ".") == 0 .and. index(str, "E") == 0 .and. index(str, "e") == 0) then
     277            0 :         str = str // ".0"
     278              :       end if
     279              : 
     280              :     case (VALUE_TYPE_STRING)
     281        22160 :       if (allocated(val%string_value)) then
     282        11079 :         str = quote_if_needed(val%string_value)
     283            1 :       else if (allocated(val%raw_text)) then
     284            1 :         str = val%raw_text
     285              :       else
     286            0 :         str = ""
     287              :       end if
     288              : 
     289              :     case default
     290            5 :       if (allocated(val%string_value)) then
     291            0 :         str = quote_if_needed(val%string_value)
     292            5 :       else if (allocated(val%raw_text)) then
     293            0 :         str = val%raw_text
     294              :       else
     295            5 :         str = ""
     296              :       end if
     297              :     end select
     298              : 
     299            7 :   end function format_value
     300              : 
     301              :   !> Quote a string if it contains special characters
     302        11079 :   function quote_if_needed(str) result(quoted)
     303              :     character(len=*), intent(in) :: str
     304              :     character(len=:), allocatable :: quoted
     305              : 
     306        11079 :     logical :: needs_quote, has_dquote, has_squote
     307        11079 :     integer :: i
     308              : 
     309        11079 :     needs_quote = .false.
     310        11079 :     has_dquote = .false.
     311        11079 :     has_squote = .false.
     312              : 
     313              :     ! Check for special characters
     314        56700 :     do i = 1, len(str)
     315        45621 :       if (index(QUOTE_TRIGGER_CHARS, str(i:i)) > 0) then
     316         2049 :         needs_quote = .true.
     317              :       end if
     318        45621 :       if (str(i:i) == CHAR_DQUOTE) has_dquote = .true.
     319        56700 :       if (str(i:i) == CHAR_SQUOTE) has_squote = .true.
     320              :     end do
     321              : 
     322        11079 :     if (.not. needs_quote) then
     323        10054 :       quoted = str
     324         1025 :     else if (.not. has_dquote) then
     325         1022 :       quoted = CHAR_DQUOTE // str // CHAR_DQUOTE
     326            3 :     else if (.not. has_squote) then
     327            2 :       quoted = CHAR_SQUOTE // str // CHAR_SQUOTE
     328              :     else
     329              :       ! Both quote types present - escape double quotes
     330            1 :       quoted = CHAR_DQUOTE // escape_quotes(str) // CHAR_DQUOTE
     331              :     end if
     332              : 
     333        12117 :   end function quote_if_needed
     334              : 
     335              :   !> Escape double quotes in a string
     336            1 :   function escape_quotes(str) result(escaped)
     337              :     character(len=*), intent(in) :: str
     338              :     character(len=:), allocatable :: escaped
     339              : 
     340            1 :     integer :: i
     341              : 
     342            1 :     escaped = ""
     343           20 :     do i = 1, len(str)
     344           20 :       if (str(i:i) == CHAR_DQUOTE) then
     345            2 :         escaped = escaped // CHAR_BACKSLASH // CHAR_DQUOTE
     346              :       else
     347           17 :         escaped = escaped // str(i:i)
     348              :       end if
     349              :     end do
     350              : 
     351        11079 :   end function escape_quotes
     352              : 
     353              :   !> Write table to dynamically allocated string (for string output)
     354       221087 :   recursive subroutine write_table_to_string(table, indent_level, output)
     355              :     type(hsd_table), intent(in) :: table
     356              :     integer, intent(in) :: indent_level
     357              :     character(len=:), allocatable, intent(inout) :: output
     358              : 
     359        10093 :     integer :: i
     360              :     class(hsd_node), pointer :: child
     361        10093 :     character(len=:), allocatable :: indent, attrib_str, line
     362              : 
     363        28150 :     indent = repeat(INDENT_STR, indent_level)
     364              : 
     365        31205 :     do i = 1, table%num_children
     366        21112 :       call table%get_child(i, child)
     367        21112 :       if (.not. associated(child)) cycle
     368              : 
     369        10093 :       select type (child)
     370              :       type is (hsd_table)
     371         9032 :         if (child%has_attrib()) then
     372            1 :           attrib_str = " [" // child%get_attrib() // "]"
     373              :         else
     374         9031 :           attrib_str = ""
     375              :         end if
     376              : 
     377        18065 :         if (allocated(child%name) .and. len_trim(child%name) > 0) then
     378         9031 :           line = indent // trim(child%name) // attrib_str // " {"
     379         9031 :           output = output // line // CHAR_NEWLINE
     380         9031 :           call write_table_to_string(child, indent_level + 1, output)
     381         9031 :           line = indent // "}"
     382         9031 :           output = output // line // CHAR_NEWLINE
     383              :         else
     384            1 :           call write_table_to_string(child, indent_level, output)
     385              :         end if
     386              : 
     387              :       type is (hsd_value)
     388        12080 :         if (child%has_attrib()) then
     389         1004 :           attrib_str = " [" // child%get_attrib() // "]"
     390              :         else
     391        11076 :           attrib_str = ""
     392              :         end if
     393              : 
     394        12080 :         if (allocated(child%name) .and. len_trim(child%name) > 0) then
     395        11073 :           line = indent // trim(child%name) // attrib_str // " = " // format_value(child)
     396              :         else
     397         1007 :           line = indent // format_value(child)
     398              :         end if
     399        24160 :         output = output // line // CHAR_NEWLINE
     400              :       end select
     401              :     end do
     402              : 
     403        10094 :   end subroutine write_table_to_string
     404              : 
     405        51364 : end module hsd_formatter
        

Generated by: LCOV version 2.0-1