1.1.2Updated a month ago
// deno-lint-ignore-file react-no-danger

import { FunctionComponent } from "preact/src/index.d.ts";

import { Package } from "$classes/package.ts";
import { join } from "$std/path/mod.ts";
import { existsSync } from "$std/fs/mod.ts";

import { bundledLanguagesInfo, codeToHtml } from "npm:shiki@^3.9.2";
import { Sort } from "$utils/sort_utils.ts";

export function guessLangFromPath(p: string): string {
  const ext = p.split(".").pop()?.toLowerCase();
  if(!ext) return "txt";

  if(ext == 'lock') return 'json';

  const bundled = bundledLanguagesInfo.find(l => l.aliases?.includes(ext) || l.id == ext || l.name?.toLowerCase() == ext);
  if(bundled) return bundled.aliases?.[0] || bundled.id;

  return "txt";
}

export interface PackageFileCodeProps {
  file: {
    path: string[];
    disk_path: string,
    html: string;
    highlight_line?: number;
    highlight_column?: number;
  }
  package: Package
};

export const FileCode: FunctionComponent<PackageFileCodeProps> = ({ package: _package, file: { path, html, highlight_line, highlight_column }}) => {
  const path_urls: string[] = [];
  const path_temp = [...path];
  let current_path_element = path_temp.shift();
  let current_url = '';
  do {
    current_url = `${current_url}/${current_path_element}`;
    if(current_url == _package.versioned_url) {
      path_urls.push(current_url + '/?tab=browse');
    } else {
      path_urls.push(current_url);
    }
    current_path_element = path_temp.shift();
  } while(current_path_element);

  return (
    <div class="mockup-code bg-base-300 text-base-content grow relative">
      <div class="absolute top-[0.7em] right-0 px-3 py-2 text-xs opacity-70">
        { path.map((part, i) => {
          return <span key={`file_path_part_${i}`}>
            { i > 0 &&
              <span class="cursor-default"> / </span>
            }
            {
              path_urls[i]
              ? <a href={`${path_urls[i]}`} class="link-hover">{ part }</a>
              : <span class="link-hover cursor-default">{ part }</span>
            }
          </span>
        }
      )}
      </div>

      <div class="max-h-128 overflow-y-auto overflow-x-auto relative">
        <div class="shiki-wrap"   dangerouslySetInnerHTML={{ __html: html }} />
        {highlight_line && highlight_column ? (
          <span class="absolute pointer-events-none bg-neutral-content/60 shiki-column-highlight"
            style={columnMarkerStyle(highlight_line, highlight_column)} />
        ) : null}
      </div>
    </div>
  );
}

function columnMarkerStyle(line: number, col: number): preact.JSX.CSSProperties {
  return {
    top: `calc(1.45em * ${line})`,
    left: `calc(3.25rem + ${Math.max(0, col - 1)}ch)`,
    width: "1ch",
    height: "1.45em",
    borderRadius: "2px",
  };
}

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

  const [file_name, line_number, column_number] = file_path_complex.join('/').split(':');

  if(!file_name) return null;

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

  const full_file_path = `${_package.namespace}/${_package.name}@${_package.versions.current}/${file_name}`;

  let highlight_line: number | undefined = Number(line_number);
  let highlight_column: number | undefined = Number(column_number);

  if(!Number.isSafeInteger(highlight_line)) highlight_line = undefined;
  if(!Number.isSafeInteger(highlight_column)) highlight_column = undefined;

  return {
    name: file_name,
    disk_path,
    url_path: full_file_path,
    highlight: {
      line: highlight_line,
      column: highlight_column
    }
  }
}

export const ExtractPackageFileCodeProps = async (pathname: string, _package: Package): Promise<PackageFileCodeProps['file'] | null> => {
  const file = ExtractFile(pathname, _package);
  if(!file) return null;

  const {
    name,
    disk_path,
    url_path,
    highlight,
  } = file;

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

  const file_contents = Deno.readTextFileSync(disk_path);

  const lang = guessLangFromPath(name);

  const html = await codeToHtml(file_contents, {
    lang,
    themes: { light: "solarized-light", dark: "material-theme-darker" },
    defaultColor: false,
    transformers: highlight.line ? [{
      line(node, line) {
        if(line == highlight.line) {
          this.addClassToHast(node, "selected-line");
          node.properties ??= {};
          node.properties.id = `code_highlighted`;
        }
      }
    }] : []
  })

  return {
    path: url_path.split('/'),
    disk_path,
    html,
    highlight_line: highlight.line,
    highlight_column: highlight.column
  }
}

export const ExtractFileVersionHistory = (pathname: string, _package: Package): Viapak.Package.Event[] | null => {
  const file = ExtractFile(pathname, _package);
  if(!file) return [];

  const {
    disk_path,
  } = file;

  const events: Viapak.Package.Event[] = []

  for(const version of _package.versions.all) {
    const versioned_file_path = disk_path.replace(/\/(?:\d+\.){1,3}\d+/, `/${version}`);

    if(existsSync(versioned_file_path)) {
      events.push(_package.events.filter(e => e.version == version).sort(Sort.Event.Descending)[0]);
    }
  }

  return events;
}