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_INDEX_PATH, INDEX_START_MARKER, &
                                      INDEX_END_MARKER, FALLBACK_COUNT, &
                                      FALLBACK_EXAMPLES
    use fortplot_doc_utils, only: build_file_path, check_file_exists, &
                                  get_file_extension, 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_directory_listing, only: list_directory_entries
    use fortplot_logging, only: log_warning
    use fortplot_doc_output, only: write_output_section, scan_directory_for_media
    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()
        character(len=LINE_MAX_LEN) :: line
        character(len=32) :: slug
        logical :: in_section, has_slug
        integer :: unit, ios

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

        open(newunit=unit, file=EXAMPLES_INDEX_PATH, status='old', action='read', iostat=ios)
        if (ios /= 0) then
            call load_fallback_manifest()
            return
        end if

        in_section = .false.
        do
            read(unit, '(A)', iostat=ios) line
            if (ios /= 0) exit

            if (index(line, INDEX_START_MARKER) > 0) then
                in_section = .true.
                cycle
            end if

            if (index(line, INDEX_END_MARKER) > 0) exit
            if (.not. in_section) cycle

            call parse_example_line(line, slug, has_slug)
            if (has_slug) call add_manifest_entry(slug)
        end do

        close(unit)

        if (manifest_count == 0) then
            call load_fallback_manifest()
        end if
    end subroutine load_example_manifest

    subroutine load_fallback_manifest()
        integer :: n

        manifest_names = ''
        n = min(FALLBACK_COUNT, MAX_EXAMPLES)
        manifest_names(1:n) = FALLBACK_EXAMPLES(1:n)
        manifest_count = n
    end subroutine load_fallback_manifest

    subroutine parse_example_line(line, slug, has_slug)
        character(len=*), intent(in) :: line
        character(len=32), intent(out) :: slug
        logical, intent(out) :: has_slug

        character(len=:), allocatable :: trimmed_line, link
        integer :: link_start, link_end

        slug = ''
        has_slug = .false.

        trimmed_line = adjustl(line)
        if (len_trim(trimmed_line) < 4) return
        if (trimmed_line(1:2) /= '- ') return

        link_start = index(trimmed_line, '](')
        if (link_start <= 0) return

        link_end = index(trimmed_line(link_start+2:), ')')
        if (link_end <= 0) return

        link = trimmed_line(link_start+2:link_start+1+link_end-1)
        call extract_slug_from_link(link, slug, has_slug)
    end subroutine parse_example_line

    subroutine extract_slug_from_link(link, slug, has_slug)
        character(len=*), intent(in) :: link
        character(len=32), intent(out) :: slug
        logical, intent(out) :: has_slug

        character(len=:), allocatable :: work
        integer :: pos

        slug = ''
        has_slug = .false.

        work = trim(link)
        if (len_trim(work) == 0) return

        if (len(work) >= 2 .and. work(1:2) == './') then
            work = work(3:)
        end if

        pos = index(work, '/example/fortran/')
        if (pos > 0) then
            work = work(pos + len('/example/fortran/'):)
        end if

        pos = index(work, '/', back=.true.)
        if (pos > 0) then
            if (pos < len_trim(work)) then
                work = work(pos+1:)
            else
                work = work(:pos-1)
            end if
        end if

        pos = index(work, '.html', back=.true.)
        if (pos > 0) work = work(:pos-1)

        if (len_trim(work) == 0) return

        slug = adjustl(work)
        slug = slug(:len(slug))
        slug = trim(slug)
        if (len_trim(slug) == 0) return

        has_slug = .true.
    end subroutine extract_slug_from_link

    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

        summary_lines = ''
        summary_count = 0

        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)
                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)
        character(len=*), intent(in) :: line
        character(len=LINE_MAX_LEN), intent(inout) :: summary_lines(:)
        integer, intent(inout) :: summary_count

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

        if (summary_count >= size(summary_lines)) return

        trimmed = trim(line)
        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
        if (len_trim(trimmed) >= 3) then
            if (trimmed(1:3) == '```') return
        end if

        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