LCOV - code coverage report
Current view: top level - src/io - hsd_formatter.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 90.5 % 275 249
Test Date: 2026-03-19 13:06:14 Functions: 100.0 % 17 17

            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, CHAR_NEWLINE, CHAR_DQUOTE, CHAR_SQUOTE, CHAR_BACKSLASH
       7              :   use hsd_types, only: hsd_node_t, hsd_iterator_t, &
       8              :     NODE_TYPE_TABLE, NODE_TYPE_VALUE, &
       9              :     VALUE_TYPE_NONE, VALUE_TYPE_ARRAY, VALUE_TYPE_STRING, &
      10              :     VALUE_TYPE_INTEGER, VALUE_TYPE_REAL, VALUE_TYPE_LOGICAL, VALUE_TYPE_COMPLEX
      11              :   use hsd_error, only: hsd_error_t, HSD_STAT_OK, HSD_STAT_IO_ERROR, make_error
      12              :   use hsd_utils, only: string_buffer_t
      13              :   implicit none (type, external)
      14              :   private
      15              : 
      16              :   public :: hsd_dump, hsd_dump_to_string
      17              : 
      18              :   !> Indentation string (2 spaces)
      19              :   character(len=*), parameter :: INDENT_STR = "  "
      20              : 
      21              :   !> Characters that require quoting
      22              :   character(len=*), parameter :: QUOTE_TRIGGER_CHARS = "{}[]= <>" // char(9)
      23              : 
      24              : contains
      25              : 
      26              :   !> Check whether a node name is anonymous (unset, blank, or "#text")
      27              :   !>
      28              :   !> Uses nested if-blocks to avoid non-short-circuit .and./.or. evaluation
      29              :   !> that can access unallocated allocatable components.
      30         1135 :   pure function is_anon_name_(name) result(res)
      31              :     character(len=:), allocatable, intent(in) :: name
      32              :     logical :: res
      33              : 
      34         1135 :     res = .true.
      35         1135 :     if (allocated(name)) then
      36         1134 :       if (len_trim(name) > 0 .and. name /= "#text") then
      37         1125 :         res = .false.
      38              :       end if
      39              :     end if
      40              : 
      41         1135 :   end function is_anon_name_
      42              : 
      43              :   !> Safely retrieve a node name, returning empty string if unallocated
      44           14 :   pure function safe_name_(name) result(res)
      45              :     character(len=:), allocatable, intent(in) :: name
      46              :     character(len=:), allocatable :: res
      47              : 
      48           14 :     if (allocated(name)) then
      49           14 :       res = name
      50              :     else
      51            0 :       res = ""
      52              :     end if
      53              : 
      54         1135 :   end function safe_name_
      55              : 
      56              :   !> Check whether a node has a non-blank name
      57              :   !>
      58              :   !> Uses nested if to avoid non-short-circuit access to unallocated name.
      59           45 :   pure function has_name_(name) result(res)
      60              :     character(len=:), allocatable, intent(in) :: name
      61              :     logical :: res
      62              : 
      63           45 :     res = .false.
      64           45 :     if (allocated(name)) then
      65           43 :       if (len_trim(name) > 0) res = .true.
      66              :     end if
      67              : 
      68           59 :   end function has_name_
      69              : 
      70              :   !> Write HSD table to a file
      71           23 :   subroutine hsd_dump(root, filename, error)
      72              :     type(hsd_node_t), intent(in) :: root
      73              :     character(len=*), intent(in) :: filename
      74              :     type(hsd_error_t), allocatable, intent(out), optional :: error
      75              : 
      76           23 :     integer :: unit_num, io_stat
      77              :     character(len=256) :: io_msg
      78              : 
      79              :     open(newunit=unit_num, file=filename, status='replace', action='write', &
      80           23 :          iostat=io_stat, iomsg=io_msg)
      81           23 :     if (io_stat /= 0) then
      82            1 :       if (present(error)) then
      83              :         call make_error(error, HSD_STAT_IO_ERROR, &
      84            1 :           "Cannot open file for writing: " // trim(io_msg), filename)
      85              :       end if
      86            1 :       return
      87              :     end if
      88              : 
      89           22 :     call write_table_content(unit_num, root, 0)
      90              : 
      91           22 :     close(unit_num)
      92              : 
      93           68 :   end subroutine hsd_dump
      94              : 
      95              :   !> Write HSD table to a string (dynamically allocated)
      96              :   !>
      97              :   !> Uses string_buffer_t internally to avoid O(n²) concatenation.
      98           43 :   subroutine hsd_dump_to_string(root, output)
      99              :     type(hsd_node_t), intent(in) :: root
     100              :     character(len=:), allocatable, intent(out) :: output
     101              : 
     102           43 :     type(string_buffer_t) :: buf
     103              : 
     104           43 :     call buf%init()
     105           43 :     call write_table_to_string_buf(root, 0, buf)
     106           43 :     output = buf%get_string()
     107              : 
     108           66 :   end subroutine hsd_dump_to_string
     109              : 
     110              :   !> Write table contents to unit
     111           47 :   recursive subroutine write_table_content(unit_num, table, indent_level)
     112              :     integer, intent(in) :: unit_num
     113              :     type(hsd_node_t), intent(in) :: table
     114              :     integer, intent(in) :: indent_level
     115              : 
     116           47 :     integer :: i
     117              :     type(hsd_node_t), pointer :: child
     118           47 :     character(len=:), allocatable :: indent
     119              : 
     120           81 :     indent = repeat(INDENT_STR, indent_level)
     121              : 
     122          133 :     do i = 1, table%num_children
     123           86 :       call table%get_child(i, child)
     124           86 :       if (.not. associated(child)) cycle
     125              : 
     126          162 :       if (child%node_type == NODE_TYPE_TABLE) then
     127           29 :         call write_table_node(unit_num, child, indent_level)
     128          114 :       else if (child%node_type == NODE_TYPE_VALUE) then
     129           57 :         call write_value_node(unit_num, child, indent_level)
     130              :       end if
     131              :     end do
     132              : 
     133           90 :   end subroutine write_table_content
     134              : 
     135              :   !> Write a table node
     136           29 :   recursive subroutine write_table_node(unit_num, table, indent_level)
     137              :     integer, intent(in) :: unit_num
     138              :     type(hsd_node_t), intent(in) :: table
     139              :     integer, intent(in) :: indent_level
     140              : 
     141           29 :     character(len=:), allocatable :: indent, attrib_str
     142              : 
     143           40 :     indent = repeat(INDENT_STR, indent_level)
     144              : 
     145              :     ! Build attribute string
     146           29 :     if (table%has_attrib()) then
     147            1 :       attrib_str = " [" // table%get_attrib() // "]"
     148              :     else
     149           28 :       attrib_str = ""
     150              :     end if
     151              : 
     152              :     ! Check if table has single child (for = syntax)
     153           29 :     if (table%num_children == 1) then
     154              :       block
     155              :         type(hsd_node_t), pointer :: single_child
     156           17 :         call table%get_child(1, single_child)
     157              : 
     158           17 :         if (single_child%node_type == NODE_TYPE_TABLE) then
     159              :           ! Tag = ChildTag { ... }
     160            9 :           if (has_name_(table%name)) then
     161              :             write(unit_num, '(A)') indent // trim(table%name) // attrib_str // &
     162            8 :               " = " // trim(safe_name_(single_child%name)) // " {"
     163            8 :             call write_table_content(unit_num, single_child, indent_level + 1)
     164            8 :             write(unit_num, '(A)') indent // "}"
     165              :           else
     166              :             ! Unnamed table, just write children
     167            1 :             call write_table_content(unit_num, table, indent_level)
     168              :           end if
     169           13 :           return
     170              : 
     171            8 :         else if (single_child%node_type == NODE_TYPE_VALUE) then
     172              :           ! Tag = value (for unnamed/anonymous or #text-named children)
     173            8 :           if (is_anon_name_(single_child%name)) then
     174            8 :             if (has_name_(table%name)) then
     175            3 :               call write_tag_value(unit_num, table%name, attrib_str, &
     176            6 :                                    single_child, indent_level)
     177              :             else
     178            1 :               call write_value_node(unit_num, single_child, indent_level)
     179              :             end if
     180            4 :             return
     181              :           end if
     182              :           ! Named child — fall through to regular block
     183              :         end if
     184              :       end block
     185              :     end if
     186              : 
     187              :     ! Regular block: Tag { ... }
     188           16 :     if (has_name_(table%name)) then
     189           15 :       write(unit_num, '(A)') indent // trim(table%name) // attrib_str // " {"
     190           15 :       call write_table_content(unit_num, table, indent_level + 1)
     191           15 :       write(unit_num, '(A)') indent // "}"
     192              :     else
     193              :       ! Root or unnamed table - just write content
     194            1 :       call write_table_content(unit_num, table, indent_level)
     195              :     end if
     196              : 
     197           29 :   end subroutine write_table_node
     198              : 
     199              :   !> Write a value node
     200           58 :   subroutine write_value_node(unit_num, val, indent_level)
     201              :     integer, intent(in) :: unit_num
     202              :     type(hsd_node_t), intent(in) :: val
     203              :     integer, intent(in) :: indent_level
     204              : 
     205           58 :     character(len=:), allocatable :: indent, attrib_str, value_str, raw_text
     206           58 :     logical :: is_anonymous
     207              : 
     208          118 :     indent = repeat(INDENT_STR, indent_level)
     209              : 
     210              :     ! Build attribute string
     211           58 :     if (val%has_attrib()) then
     212            2 :       attrib_str = " [" // val%get_attrib() // "]"
     213              :     else
     214           56 :       attrib_str = ""
     215              :     end if
     216              : 
     217              :     ! Treat #text-named values as anonymous (inline content)
     218           58 :     is_anonymous = is_anon_name_(val%name)
     219              : 
     220              :     ! For anonymous values with multiline raw text, use unquoted format
     221           58 :     if (is_anonymous) then
     222              :       ! Check for raw text with newlines (block content like GenFormat data)
     223            3 :       raw_text = get_raw_text_(val)
     224            5 :       if (index(raw_text, CHAR_NEWLINE) > 0) then
     225            2 :         call write_multiline(unit_num, raw_text, indent_level)
     226            2 :         return
     227              :       end if
     228              :     end if
     229              : 
     230              :     ! Get value string (may quote/escape)
     231           56 :     value_str = format_value(val)
     232              : 
     233              :     ! Write
     234           56 :     if (.not. is_anonymous) then
     235           55 :       if (index(value_str, CHAR_NEWLINE) > 0) then
     236              :         ! Multi-line value
     237            0 :         write(unit_num, '(A)') indent // trim(val%name) // attrib_str // " {"
     238            0 :         call write_multiline(unit_num, value_str, indent_level + 1)
     239            0 :         write(unit_num, '(A)') indent // "}"
     240              :       else
     241              :         ! Single-line value
     242           55 :         write(unit_num, '(A)') indent // trim(val%name) // attrib_str // " = " // value_str
     243              :       end if
     244              :     else
     245              :       ! Anonymous value (data content)
     246            1 :       if (index(value_str, CHAR_NEWLINE) > 0) then
     247            0 :         call write_multiline(unit_num, value_str, indent_level)
     248              :       else
     249            1 :         write(unit_num, '(A)') indent // value_str
     250              :       end if
     251              :     end if
     252              : 
     253           58 :   end subroutine write_value_node
     254              : 
     255              :   !> Write tag = value
     256            3 :   subroutine write_tag_value(unit_num, name, attrib_str, val, indent_level)
     257              :     integer, intent(in) :: unit_num
     258              :     character(len=*), intent(in) :: name
     259              :     character(len=*), intent(in) :: attrib_str
     260              :     type(hsd_node_t), intent(in) :: val
     261              :     integer, intent(in) :: indent_level
     262              : 
     263            3 :     character(len=:), allocatable :: indent, value_str, val_attrib, raw_text
     264              : 
     265            3 :     indent = repeat(INDENT_STR, indent_level)
     266              : 
     267              :     ! Combine attributes
     268            3 :     if (val%has_attrib()) then
     269            1 :       val_attrib = " [" // val%get_attrib() // "]"
     270              :     else
     271            2 :       val_attrib = attrib_str
     272              :     end if
     273              : 
     274              :     ! Check for multiline raw text first (e.g. GenFormat data) — use unquoted format
     275            3 :     raw_text = get_raw_text_(val)
     276            3 :     if (index(raw_text, CHAR_NEWLINE) > 0) then
     277            1 :       write(unit_num, '(A)') indent // trim(name) // val_attrib // " {"
     278            1 :       call write_multiline(unit_num, raw_text, indent_level + 1)
     279            1 :       write(unit_num, '(A)') indent // "}"
     280            1 :       return
     281              :     end if
     282              : 
     283            2 :     value_str = format_value(val)
     284              : 
     285            2 :     if (index(value_str, CHAR_NEWLINE) > 0) then
     286              :       ! Multi-line value
     287            0 :       write(unit_num, '(A)') indent // trim(name) // val_attrib // " {"
     288            0 :       call write_multiline(unit_num, value_str, indent_level + 1)
     289            0 :       write(unit_num, '(A)') indent // "}"
     290              :     else
     291            2 :       write(unit_num, '(A)') indent // trim(name) // val_attrib // " = " // value_str
     292              :     end if
     293              : 
     294           61 :   end subroutine write_tag_value
     295              : 
     296              :   !> Write multi-line content
     297            3 :   subroutine write_multiline(unit_num, text, indent_level)
     298              :     integer, intent(in) :: unit_num
     299              :     character(len=*), intent(in) :: text
     300              :     integer, intent(in) :: indent_level
     301              : 
     302            3 :     character(len=:), allocatable :: indent
     303            3 :     integer :: pos, next_pos, text_len
     304              : 
     305            6 :     indent = repeat(INDENT_STR, indent_level)
     306            3 :     text_len = len(text)
     307            3 :     pos = 1
     308              : 
     309           13 :     do while (pos <= text_len)
     310           13 :       next_pos = index(text(pos:), CHAR_NEWLINE)
     311           13 :       if (next_pos > 0) then
     312           10 :         next_pos = pos + next_pos - 1
     313           10 :         if (next_pos > pos) then
     314           10 :           write(unit_num, '(A)') indent // text(pos:next_pos-1)
     315              :         else
     316            0 :           write(unit_num, '(A)') ""
     317              :         end if
     318           10 :         pos = next_pos + 1
     319              :       else
     320            3 :         write(unit_num, '(A)') indent // text(pos:)
     321            3 :         exit
     322              :       end if
     323              :     end do
     324              : 
     325            6 :   end subroutine write_multiline
     326              : 
     327              :   !> Get raw text content from a value node (unquoted, with real newlines)
     328            9 :   function get_raw_text_(val) result(text)
     329              :     type(hsd_node_t), intent(in) :: val
     330              :     character(len=:), allocatable :: text
     331              : 
     332            9 :     if (allocated(val%string_value)) then
     333            9 :       text = val%string_value
     334              :     else
     335            0 :       text = ""
     336              :     end if
     337              : 
     338            3 :   end function get_raw_text_
     339              : 
     340              :   !> Format a value for output
     341         1119 :   function format_value(val) result(str)
     342              :     type(hsd_node_t), intent(in) :: val
     343              :     character(len=:), allocatable :: str
     344              : 
     345         1119 :     if (allocated(val%string_value)) then
     346         1118 :       str = quote_if_needed(val%string_value)
     347              :     else
     348            1 :       str = ""
     349              :     end if
     350              : 
     351            9 :   end function format_value
     352              : 
     353              :   !> Quote a string if it contains special characters.
     354              :   !>
     355              :   !> Newlines, backslashes, tabs, and quote characters are escaped so that
     356              :   !> the result is always a single-line quoted token that round-trips cleanly.
     357         1118 :   function quote_if_needed(str) result(quoted)
     358              :     character(len=*), intent(in) :: str
     359              :     character(len=:), allocatable :: quoted
     360              : 
     361         1118 :     logical :: needs_quote
     362         1118 :     integer :: i
     363              :     character(len=1) :: ch
     364              : 
     365         1118 :     needs_quote = .false.
     366         6600 :     do i = 1, len(str)
     367         5509 :       ch = str(i:i)
     368              :       if (index(QUOTE_TRIGGER_CHARS, ch) > 0 &
     369              :           & .or. ch == CHAR_NEWLINE &
     370              :           & .or. ch == CHAR_BACKSLASH &
     371              :           & .or. ch == CHAR_DQUOTE &
     372         6600 :           & .or. ch == CHAR_SQUOTE) then
     373           27 :         needs_quote = .true.
     374           27 :         exit
     375              :       end if
     376              :     end do
     377              : 
     378         1118 :     if (.not. needs_quote) then
     379         1091 :       quoted = str
     380              :     else
     381           27 :       quoted = CHAR_DQUOTE // escape_string(str) // CHAR_DQUOTE
     382              :     end if
     383              : 
     384         1119 :   end function quote_if_needed
     385              : 
     386              :   !> Escape special characters in a string for HSD quoted output.
     387              :   !>
     388              :   !> Handles: backslash, double-quote, newline, tab.
     389              :   !> Uses string_buffer_t to avoid O(n^2) concatenation.
     390           27 :   function escape_string(str) result(escaped)
     391              :     character(len=*), intent(in) :: str
     392              :     character(len=:), allocatable :: escaped
     393              : 
     394           27 :     type(string_buffer_t) :: buf
     395           27 :     integer :: i
     396              :     character(len=1) :: ch
     397              : 
     398           27 :     call buf%init()
     399          417 :     do i = 1, len(str)
     400          390 :       ch = str(i:i)
     401          417 :       if (ch == CHAR_BACKSLASH) then
     402            1 :         call buf%append_str(CHAR_BACKSLASH // CHAR_BACKSLASH)
     403          389 :       else if (ch == CHAR_DQUOTE) then
     404            6 :         call buf%append_str(CHAR_BACKSLASH // CHAR_DQUOTE)
     405          383 :       else if (ch == CHAR_NEWLINE) then
     406            9 :         call buf%append_str(CHAR_BACKSLASH // "n")
     407          374 :       else if (ch == char(9)) then
     408            1 :         call buf%append_str(CHAR_BACKSLASH // "t")
     409              :       else
     410          373 :         call buf%append_char(ch)
     411              :       end if
     412              :     end do
     413           27 :     escaped = buf%get_string()
     414              : 
     415         1145 :   end function escape_string
     416              : 
     417              :   !> Write table to string_buffer_t (for string output, avoids O(n²) concatenation)
     418              :   !>
     419              :   !> Mirrors the file-output path including the single-child `= ChildTag { }`
     420              :   !> and `Tag = value` shorthand syntax.
     421           58 :   recursive subroutine write_table_to_string_buf(table, indent_level, buf)
     422              :     type(hsd_node_t), intent(in) :: table
     423              :     integer, intent(in) :: indent_level
     424              :     type(string_buffer_t), intent(inout) :: buf
     425              : 
     426           58 :     integer :: i
     427              :     type(hsd_node_t), pointer :: child
     428           58 :     character(len=:), allocatable :: indent, attrib_str, line, value_str
     429              : 
     430           76 :     indent = repeat(INDENT_STR, indent_level)
     431              : 
     432         1135 :     do i = 1, table%num_children
     433         1077 :       call table%get_child(i, child)
     434         1077 :       if (.not. associated(child)) cycle
     435              : 
     436         1151 :       if (child%node_type == NODE_TYPE_TABLE) then
     437           16 :         call write_table_node_to_buf(child, indent_level, buf)
     438              : 
     439         1061 :       else if (child%node_type == NODE_TYPE_VALUE) then
     440         1061 :         block
     441         1061 :           logical :: is_anon
     442         1061 :           character(len=:), allocatable :: raw_text
     443              : 
     444         1061 :           if (child%has_attrib()) then
     445            4 :             attrib_str = " [" // child%get_attrib() // "]"
     446              :           else
     447         1057 :             attrib_str = ""
     448              :           end if
     449              : 
     450              :           ! Treat #text-named values as anonymous
     451         1061 :           is_anon = is_anon_name_(child%name)
     452              : 
     453         2124 :           if (is_anon) then
     454              :             ! Anonymous/inline content — check for multiline raw text first
     455            2 :             raw_text = get_raw_text_(child)
     456            2 :             if (index(raw_text, CHAR_NEWLINE) > 0) then
     457            0 :               call write_multiline_to_buf(raw_text, indent_level, buf)
     458              :             else
     459            2 :               value_str = format_value(child)
     460            2 :               if (index(value_str, CHAR_NEWLINE) > 0) then
     461            0 :                 call write_multiline_to_buf(value_str, indent_level, buf)
     462              :               else
     463            2 :                 line = indent // value_str
     464            2 :                 call buf%append_str(line)
     465            2 :                 call buf%append_str(CHAR_NEWLINE)
     466              :               end if
     467              :             end if
     468              :           else
     469         1059 :             value_str = format_value(child)
     470         1059 :             if (index(value_str, CHAR_NEWLINE) > 0) then
     471            0 :               call buf%append_str(indent // trim(child%name) // attrib_str // " {")
     472            0 :               call buf%append_str(CHAR_NEWLINE)
     473            0 :               call write_multiline_to_buf(value_str, indent_level + 1, buf)
     474            0 :               call buf%append_str(indent // "}")
     475            0 :               call buf%append_str(CHAR_NEWLINE)
     476              :             else
     477         1059 :               line = indent // trim(child%name) // attrib_str // " = " // value_str
     478         1059 :               call buf%append_str(line)
     479         1059 :               call buf%append_str(CHAR_NEWLINE)
     480              :             end if
     481              :           end if
     482              :         end block
     483              :       end if
     484              :     end do
     485              : 
     486           85 :   end subroutine write_table_to_string_buf
     487              : 
     488              :   !> Write a table node to string buffer with single-child shorthand
     489           16 :   recursive subroutine write_table_node_to_buf(table, indent_level, buf)
     490              :     type(hsd_node_t), intent(in) :: table
     491              :     integer, intent(in) :: indent_level
     492              :     type(string_buffer_t), intent(inout) :: buf
     493              : 
     494           16 :     character(len=:), allocatable :: indent, attrib_str, value_str, val_attrib
     495              : 
     496           20 :     indent = repeat(INDENT_STR, indent_level)
     497              : 
     498              :     ! Build attribute string
     499           16 :     if (table%has_attrib()) then
     500            0 :       attrib_str = " [" // table%get_attrib() // "]"
     501              :     else
     502           16 :       attrib_str = ""
     503              :     end if
     504              : 
     505              :     ! Check for single-child shorthand (= syntax)
     506           16 :     if (table%num_children == 1) then
     507              :       block
     508              :         type(hsd_node_t), pointer :: single_child
     509           14 :         call table%get_child(1, single_child)
     510              : 
     511           14 :         if (single_child%node_type == NODE_TYPE_TABLE) then
     512              :           ! Tag = ChildTag { ... }
     513            6 :           if (has_name_(table%name)) then
     514              :             call buf%append_str(indent // trim(table%name) // attrib_str // &
     515            6 :               " = " // trim(safe_name_(single_child%name)) // " {")
     516            6 :             call buf%append_str(CHAR_NEWLINE)
     517            6 :             call write_table_to_string_buf(single_child, indent_level + 1, buf)
     518            6 :             call buf%append_str(indent // "}")
     519            6 :             call buf%append_str(CHAR_NEWLINE)
     520              :           else
     521            0 :             call write_table_to_string_buf(table, indent_level, buf)
     522              :           end if
     523            7 :           return
     524              : 
     525            8 :         else if (single_child%node_type == NODE_TYPE_VALUE) then
     526              :           ! Tag = value (for unnamed/anonymous or #text-named children)
     527            8 :           if (is_anon_name_(single_child%name)) then
     528            1 :             if (has_name_(table%name)) then
     529            1 :               block
     530            1 :                 character(len=:), allocatable :: raw_text
     531              :                 ! Check for multiline raw text (e.g. GenFormat data)
     532            1 :                 raw_text = get_raw_text_(single_child)
     533            1 :                 if (single_child%has_attrib()) then
     534            0 :                   val_attrib = " [" // single_child%get_attrib() // "]"
     535              :                 else
     536            1 :                   val_attrib = attrib_str
     537              :                 end if
     538            3 :                 if (index(raw_text, CHAR_NEWLINE) > 0) then
     539              :                   ! Multiline content — write as block
     540            1 :                   call buf%append_str(indent // trim(table%name) // val_attrib // " {")
     541            1 :                   call buf%append_str(CHAR_NEWLINE)
     542            1 :                   call write_multiline_to_buf(raw_text, indent_level + 1, buf)
     543            1 :                   call buf%append_str(indent // "}")
     544            1 :                   call buf%append_str(CHAR_NEWLINE)
     545              :                 else
     546            0 :                   value_str = format_value(single_child)
     547              :                   call buf%append_str(indent // trim(table%name) // val_attrib // &
     548            0 :                     " = " // value_str)
     549            0 :                   call buf%append_str(CHAR_NEWLINE)
     550              :                 end if
     551              :               end block
     552              :             else
     553            0 :               value_str = format_value(single_child)
     554            0 :               call buf%append_str(indent // value_str)
     555            0 :               call buf%append_str(CHAR_NEWLINE)
     556              :             end if
     557            1 :             return
     558              :           end if
     559              :           ! Named child — fall through to regular block
     560              :         end if
     561              :       end block
     562              :     end if
     563              : 
     564              :     ! Regular block: Tag { ... }
     565            9 :     if (has_name_(table%name)) then
     566            8 :       call buf%append_str(indent // trim(table%name) // attrib_str // " {")
     567            8 :       call buf%append_str(CHAR_NEWLINE)
     568            8 :       call write_table_to_string_buf(table, indent_level + 1, buf)
     569            8 :       call buf%append_str(indent // "}")
     570            8 :       call buf%append_str(CHAR_NEWLINE)
     571              :     else
     572            1 :       call write_table_to_string_buf(table, indent_level, buf)
     573              :     end if
     574              : 
     575           16 :   end subroutine write_table_node_to_buf
     576              : 
     577              :   !> Write multi-line content to string buffer
     578            1 :   subroutine write_multiline_to_buf(text, indent_level, buf)
     579              :     character(len=*), intent(in) :: text
     580              :     integer, intent(in) :: indent_level
     581              :     type(string_buffer_t), intent(inout) :: buf
     582              : 
     583            1 :     character(len=:), allocatable :: indent
     584            1 :     integer :: pos, next_pos, text_len
     585              : 
     586            2 :     indent = repeat(INDENT_STR, indent_level)
     587            1 :     text_len = len(text)
     588            1 :     pos = 1
     589              : 
     590            3 :     do while (pos <= text_len)
     591            3 :       next_pos = index(text(pos:), CHAR_NEWLINE)
     592            3 :       if (next_pos > 0) then
     593            2 :         next_pos = pos + next_pos - 1
     594            2 :         if (next_pos > pos) then
     595            2 :           call buf%append_str(indent // text(pos:next_pos-1))
     596              :         end if
     597            2 :         call buf%append_str(CHAR_NEWLINE)
     598            2 :         pos = next_pos + 1
     599              :       else
     600            1 :         call buf%append_str(indent // text(pos:))
     601            1 :         call buf%append_str(CHAR_NEWLINE)
     602            1 :         exit
     603              :       end if
     604              :     end do
     605              : 
     606            2 :   end subroutine write_multiline_to_buf
     607              : 
     608              : end module hsd_formatter
        

Generated by: LCOV version 2.0-1