// 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[];
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(/^\/+/, '').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);
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('/'),
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;
}