fortplot_doc_processing.f90 Source File


Source Code

module fortplot_doc_processing
    !! Example processing logic for documentation generation.
    !!
    !! Handles example manifest loading, line parsing, and per-example
    !! markdown document creation.

    use fortplot_doc_constants, only: PATH_MAX_LEN, FILENAME_MAX_LEN, &
                                      LINE_MAX_LEN, MAX_EXAMPLES, &
                                      MAX_MEDIA_FILES, OUTPUT_BASE_DIR, &
                                      EXAMPLES_ROOT
    use fortplot_doc_utils, only: lowercase_string, &
                                  replace_extension, &
                                  title_case, &
                                  build_readme_path, &
                                  build_output_path, &
                                  build_fortran_url, &
                                  build_local_fortran_path, &
                                  get_output_title, &
                                  get_fortran_filename, &
                                  get_example_run_target
    use fortplot_doc_output, only: write_output_section, scan_directory_for_media
    use fortplot_directory_listing, only: list_directory_entries
    implicit none
    private

    public :: get_example_count, get_example_dir, get_example_name
    public :: process_example

    ! Manifest state
    logical :: manifest_loaded = .false.
    integer :: manifest_count = 0
    character(len=32) :: manifest_names(MAX_EXAMPLES)

contains

    function get_example_count() result(count)
        integer :: count

        call ensure_example_manifest()
        count = manifest_count
    end function get_example_count

    subroutine get_example_dir(index, dir)
        integer, intent(in) :: index
        character(len=PATH_MAX_LEN), intent(out) :: dir

        call ensure_example_manifest()

        if (index > 0 .and. index <= manifest_count) then
            dir = 'example/fortran/' // trim(manifest_names(index))
        else
            dir = ''
        end if
    end subroutine get_example_dir

    subroutine get_example_name(index, name)
        integer, intent(in) :: index
        character(len=PATH_MAX_LEN), intent(out) :: name

        call ensure_example_manifest()

        if (index > 0 .and. index <= manifest_count) then
            name = trim(manifest_names(index))
        else
            name = ''
        end if
    end subroutine get_example_name

    subroutine ensure_example_manifest()
        if (.not. manifest_loaded) then
            call load_example_manifest()
        end if
    end subroutine ensure_example_manifest

    subroutine load_example_manifest()
        !! Discover examples from the source tree.
        !!
        !! This used to scrape the generated gallery page for '- [Name](...)'
        !! bullets, which coupled the generator to the presentation of a file it
        !! also generates: changing the gallery to render cards silently matched
        !! nothing, and the empty result fell through to a hardcoded fallback
        !! list naming examples deleted long ago, which were then published.
        !! example/fortran/ is the actual source of truth.
        character(len=FILENAME_MAX_LEN), allocatable :: entries(:)
        character(len=PATH_MAX_LEN) :: source_path
        integer :: entry_count, status, i
        logical :: exists

        manifest_names = ''
        manifest_count = 0
        manifest_loaded = .true.

        allocate(entries(MAX_EXAMPLES))
        entries = ''
        call list_directory_entries(EXAMPLES_ROOT, entries, entry_count, status)
        if (status /= 0) return

        call sort_names(entries, min(entry_count, MAX_EXAMPLES))

        do i = 1, min(entry_count, MAX_EXAMPLES)
            if (len_trim(entries(i)) == 0) cycle
            ! A directory holding any Fortran source is an example; a plain file
            ! in example/fortran (README.md, say) is not. The source is not
            ! required to share the directory's name: animation/ holds
            ! save_animation_demo.f90 and ascii_heatmap/ holds
            ! ascii_heatmap_demo.f90.
            if (.not. holds_fortran_source(trim(entries(i)))) cycle
            call add_manifest_entry(entries(i))
        end do

    end subroutine load_example_manifest

    logical function holds_fortran_source(name) result(is_example)
        !! True when example/fortran/<name>/ contains at least one .f90 file.
        character(len=*), intent(in) :: name

        character(len=FILENAME_MAX_LEN), allocatable :: files(:)
        character(len=:), allocatable :: candidate
        integer :: count, status, i

        is_example = .false.
        allocate(files(MAX_MEDIA_FILES))
        files = ''
        call list_directory_entries(EXAMPLES_ROOT//'/'//name, files, count, status)
        if (status /= 0) return

        do i = 1, min(count, MAX_MEDIA_FILES)
            candidate = trim(files(i))
            if (len(candidate) < 5) cycle
            if (candidate(len(candidate) - 3:) == '.f90') then
                is_example = .true.
                return
            end if
        end do
    end function holds_fortran_source

    subroutine sort_names(values, n)
        !! Alphabetical order, so generated pages appear in a stable sequence
        !! regardless of the order the filesystem hands entries back.
        character(len=*), intent(inout) :: values(:)
        integer, intent(in) :: n
        character(len=len(values(1))) :: temp
        integer :: i, j

        if (n <= 1) return
        do i = 2, n
            temp = values(i)
            j = i - 1
            do while (j >= 1)
                if (trim(values(j)) <= trim(temp)) exit
                values(j + 1) = values(j)
                j = j - 1
            end do
            values(j + 1) = temp
        end do
    end subroutine sort_names




    subroutine add_manifest_entry(name)
        character(len=*), intent(in) :: name
        character(len=32) :: candidate
        integer :: i

        candidate = trim(adjustl(name))
        if (len_trim(candidate) == 0) return

        do i = 1, manifest_count
            if (trim(manifest_names(i)) == trim(candidate)) return
        end do

        if (manifest_count >= MAX_EXAMPLES) return

        manifest_count = manifest_count + 1
        manifest_names(manifest_count) = candidate
    end subroutine add_manifest_entry

    subroutine process_example(example_dir, example_name)
        character(len=*), intent(in) :: example_dir, example_name
        character(len=PATH_MAX_LEN) :: readme_file, output_file
        character(len=PATH_MAX_LEN) :: fortran_file, fortran_url
        character(len=PATH_MAX_LEN) :: run_target
        character(len=PATH_MAX_LEN) :: output_dir
        character(len=LINE_MAX_LEN) :: line
        character(len=LINE_MAX_LEN) :: summary_lines(200)
        character(len=FILENAME_MAX_LEN) :: media_files(MAX_MEDIA_FILES)
        integer :: unit_in, unit_out, ios
        integer :: summary_count, n_media
        logical :: readme_exists, in_code_block

        summary_lines = ''
        summary_count = 0
        in_code_block = .false.

        readme_file = trim(example_dir) // '/README.md'
        output_file = 'doc/examples/' // trim(example_name) // '.md'
        output_dir = OUTPUT_BASE_DIR // trim(example_name)

        call get_fortran_filename(example_name, fortran_file)
        call build_fortran_url(example_name, fortran_url)
        call get_example_run_target(example_name, run_target)

        inquire(file=readme_file, exist=readme_exists)
        if (readme_exists) then
            open(newunit=unit_in, file=readme_file, status='old', action='read', iostat=ios)
            if (ios == 0) then
                do
                    read(unit_in, '(A)', iostat=ios) line
                    if (ios /= 0) exit
                    call append_summary_line(line, summary_lines, summary_count, in_code_block)
                end do
                close(unit_in)
            end if
        end if

        call trim_summary(summary_lines, summary_count)
        call scan_directory_for_media(output_dir, media_files, n_media)

        print '(A,A)', '  Processing example: ', trim(example_name)

        open(newunit=unit_out, file=output_file, status='replace', action='write', iostat=ios)
        if (ios /= 0) return

        call write_example_header(unit_out, example_name, fortran_file, fortran_url)
        call write_summary_section(unit_out, summary_lines, summary_count)
        call write_files_section(unit_out, example_name, fortran_file, n_media)
        call write_running_section(unit_out, trim(run_target))
        call write_output_section(unit_out, example_name, media_files, n_media)

        close(unit_out)
    end subroutine process_example

    subroutine append_summary_line(line, summary_lines, summary_count, in_code_block)
        character(len=*), intent(in) :: line
        character(len=LINE_MAX_LEN), intent(inout) :: summary_lines(:)
        integer, intent(inout) :: summary_count
        logical, intent(inout) :: in_code_block

        character(len=:), allocatable :: trimmed, lower

        if (summary_count >= size(summary_lines)) return

        trimmed = trim(line)
        if (len_trim(trimmed) >= 3) then
            if (trimmed(1:3) == '```') then
                in_code_block = .not. in_code_block
                summary_count = summary_count + 1
                if (summary_count <= size(summary_lines)) then
                    summary_lines(summary_count) = trimmed
                end if
                return
            end if
        end if

        if (in_code_block) then
            summary_count = summary_count + 1
            if (summary_count <= size(summary_lines)) then
                summary_lines(summary_count) = trimmed
            end if
            return
        end if

        if (len_trim(trimmed) == 0) then
            if (summary_count > 0) then
                if (summary_lines(summary_count) /= '') then
                    summary_count = summary_count + 1
                    if (summary_count <= size(summary_lines)) then
                        summary_lines(summary_count) = ''
                    end if
                end if
            end if
            return
        end if

        if (trimmed(1:1) == '#') return

        lower = lowercase_string(trimmed)
        if (len(lower) >= 6) then
            if (lower(1:6) == 'title:') return
        end if
        if (len(lower) >= 12) then
            if (lower(1:12) == 'make example') return
        end if
        if (trimmed == '---') return

        summary_count = summary_count + 1
        if (summary_count <= size(summary_lines)) then
            summary_lines(summary_count) = trimmed
        end if
    end subroutine append_summary_line

    subroutine trim_summary(summary_lines, summary_count)
        character(len=LINE_MAX_LEN), intent(inout) :: summary_lines(:)
        integer, intent(inout) :: summary_count

        do while (summary_count > 0)
            if (len_trim(summary_lines(summary_count)) == 0) then
                summary_lines(summary_count) = ''
                summary_count = summary_count - 1
            else
                exit
            end if
        end do
    end subroutine trim_summary

    subroutine write_example_header(unit_out, example_name, fortran_file, fortran_url)
        integer, intent(in) :: unit_out
        character(len=*), intent(in) :: example_name, fortran_file, fortran_url

        write(unit_out, '(A)') 'title: ' // title_case(example_name)
        write(unit_out, '(A)') '---'
        write(unit_out, '(A)') ''
        write(unit_out, '(A)') '# ' // title_case(example_name)
        write(unit_out, '(A)') ''
        write(unit_out, '(A)') 'Source: [' // trim(fortran_file) // '](' // trim(fortran_url) // ')'
        write(unit_out, '(A)') ''
    end subroutine write_example_header

    subroutine write_summary_section(unit_out, summary_lines, summary_count)
        integer, intent(in) :: unit_out
        character(len=LINE_MAX_LEN), intent(in) :: summary_lines(:)
        integer, intent(in) :: summary_count
        integer :: i

        if (summary_count == 0) then
            write(unit_out, '(A)') 'See source and outputs below.'
            write(unit_out, '(A)') ''
        else
            do i = 1, summary_count
                write(unit_out, '(A)') trim(summary_lines(i))
            end do
            write(unit_out, '(A)') ''
        end if
    end subroutine write_summary_section

    subroutine write_files_section(unit_out, example_name, fortran_file, n_media)
        integer, intent(in) :: unit_out
        character(len=*), intent(in) :: example_name, fortran_file
        integer, intent(in) :: n_media

        write(unit_out, '(A)') '## Files'
        write(unit_out, '(A)') ''
        write(unit_out, '(A,A,A)') '- `', trim(fortran_file), '` - Source code'
        if (n_media > 0) then
            write(unit_out, '(A,A,A)') '- Generated media in `output/example/fortran/', &
                trim(example_name), '/`'
        else
            write(unit_out, '(A,A,A)') '- Run the example to populate `output/example/fortran/', &
                trim(example_name), '/`'
        end if
        write(unit_out, '(A)') ''
    end subroutine write_files_section

    subroutine write_running_section(unit_out, run_target)
        integer, intent(in) :: unit_out
        character(len=*), intent(in) :: run_target

        write(unit_out, '(A)') '## Running'
        write(unit_out, '(A)') ''
        write(unit_out, '(A)') '```bash'
        write(unit_out, '(A,A,A)') 'make example ARGS="', trim(run_target), '"'
        write(unit_out, '(A)') '```'
        write(unit_out, '(A)') ''
    end subroutine write_running_section

end module fortplot_doc_processing