1.1.2Updated a month ago
import { FunctionComponent } from "preact/src/index.d.ts";

import type { Package } from "$classes/package.ts";
import { I_File } from "$modules/zip.ts";
import { join } from "$std/path/mod.ts";
import { guessLangFromPath } from "./Code.tsx";
import { existsSync } from "$std/fs/mod.ts";
import { Sort } from "$utils/sort_utils.ts";

type I_ComplexFile = {
  name: string
  full_path: string
  relative_path: string
  type: "file" | "folder"
  is_directory: boolean
  children?: I_ComplexFile[]
} & ({
  type: "folder"
  children: I_ComplexFile[]
} | {
  type: "file"
  children: never
})

export interface PackageFolderProps {
  package: Package
  folder: {
    path: string[]
    tree: I_ComplexFile[]
  }
}

const DEBUG_EXTRACT = false;
export const ExtractChildren = (root: string, files: I_File[]): I_ComplexFile[] => {
  if(DEBUG_EXTRACT) console.log(`Scanning for files in "${root}"`);
  const current_items = files.filter(file => {
    if(!file.relative_path.trim()) return false;
    if(DEBUG_EXTRACT) console.log(`    - ${file.relative_path}`);

    const relative = file.relative_path.replace(root, '').replace(/^\//, '');
    if(!relative) return false;

    if(!file.relative_path.startsWith(root)) return false;
    if(DEBUG_EXTRACT) console.log(`      - Checking ${file.relative_path.replace(root, '').replace(/^\//, '')}`);
    if(file.relative_path.replace(root, '').replace(/^\//, '').includes('/')) return false;

    if(DEBUG_EXTRACT) console.log(`      - PICKED`);
    return true;
  })

  files = files.filter(file => !current_items.includes(file) && file.relative_path.trim());

  if(DEBUG_EXTRACT) console.log(`  - found ${current_items.length} items`);
  if(DEBUG_EXTRACT) console.log(`  - reduced input files list to ${files.length} items`);

  if(current_items.length == 0) return [];

  return current_items.map(item => {
    const out = {
      name: item.name,
      full_path: item.full_path,
      relative_path: item.relative_path,
      is_directory: item.is_directory,
    } as Partial<I_ComplexFile>;

    if(item.is_directory) {
      out.type = 'folder';
      out.children = ExtractChildren(`${root ? `${root}/` : ''}${item.name}`, files);
    } else {
      out.type = 'file';
    }

    return out as I_ComplexFile;
  }).sort(Sort.File)
}

export const ExtractFolderProps = (pathname: string, _package: Package): PackageFolderProps['folder'] | null => {
  const folder = ExtractFolder(pathname, _package);
  if(!folder) return null;

  const {
    name,
    url_path,
    disk_path,
  } = folder;

  if(!existsSync(disk_path, { isDirectory: true })) return null;

  const tree = ExtractChildren(name, _package.files_and_folders);

  return {
    path: url_path.split('/'),
    tree
  }
}

export const ExtractFolder = (pathname: string, _package: Package) => {
  const [_namespace, _name, ...folder_path_complex] = pathname.replace(/^\/+/, '').replace(/\/\*\./g, '/.').split('/');

  const folder_name = folder_path_complex.join('/').trim();

  if(folder_name.includes(":")) return null;

  const disk_path = join(_package.path!, folder_name).replace(/\/\*\./g, '/.');

  const full_folder_path = `${_package.namespace}/${_package.name}@${_package.versions.current}/${folder_name}`;

  return {
    name: folder_name,
    disk_path,
    url_path: full_folder_path,
  }
}

const SVG = {
  File: {
    Code: () => (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        stroke-width="1.5"
        stroke="currentColor"
        class="h-4 w-4">
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
      </svg>
    ),
    Plain: () => (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        stroke-width="1.5"
        stroke="currentColor"
        class="h-4 w-4">
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
      </svg>
    )
  },
  Folder: () => (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 24 24"
      stroke-width="1.5"
      stroke="currentColor"
      class="h-4 w-4">
      <path
        stroke-linecap="round"
        stroke-linejoin="round"
        d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
    </svg>
  )
} as const;

const RenderFolderTreeRecursive: FunctionComponent<{tree: I_ComplexFile[], package: Package}> = ({tree, package: _package}) => {
  return <>
    {
      tree.map(item => {
        if(item.type == 'folder') {
          return (
            <li>
              <details open>
                <summary>
                  <SVG.Folder />
                  <a href={`${_package.url}/${item.relative_path}`.replace(/\/\./g, '/*.')} class="link-hover w-fit">
                    { item.name }
                  </a>
                </summary>
                <ul>
                  <RenderFolderTreeRecursive tree={item.children} package={_package} />
                </ul>
              </details>
            </li>
          )
        } else {
          return (
            <li>
              <a href={`${_package.url}/${item.relative_path}`.replace(/\/\./g, '/*.')}>
                { guessLangFromPath(item.name) == 'txt' ?
                  <SVG.File.Plain />
                :
                  <SVG.File.Code />
                }
                { item.name }
              </a>
            </li>
          )
        }
      })
    }
  </>
}

export const PackageFolderBrowser: FunctionComponent<PackageFolderProps> = ({ folder, package: _package }) => {
  const show_back = ![_package.url, _package.versioned_url].includes(`/${folder.path.join('/')}`.replace(/\/$/, ''))
  let back_url = `/${join(folder.path.join('/'), '../')}`.replace(/\/{2,}/, '').replace(/\/$/, '');

  if([_package.url, _package.versioned_url].includes(back_url)) {
    back_url += '?tab=browse';
  }

  return (
    <ul class="menu bg-base-200 rounded-box w-full">
      <li><a class="pointer-events-none">
        <SVG.Folder />
        { folder.path.join('/') }
      </a></li>
      <li />
      { show_back &&
        <li><a href={back_url}>...</a></li>
      }
      <RenderFolderTreeRecursive tree={folder.tree} package={_package} />
    </ul>
  )
}