'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { createContext, useContext, useSyncExternalStore, } from 'react';
import { useDeepCompareMemo } from '@react-hookz/web';
import escapeStringRegexp from 'escape-string-regexp';
import flattenChildren from 'react-keyed-flatten-children';
import { createHighlighter, } from 'shiki';
import { EyeClosedIcon, EyeIcon, FileCodeIcon, FileCssIcon, FileHtmlIcon, FileJsIcon, FileJsxIcon, FileMdIcon, FilePyIcon, FileRsIcon, FileSqlIcon, FileTsIcon, FileTsxIcon, FileVueIcon, TerminalIcon, } from '@graphprotocol/gds-react/icons';
import { useControlled, useCSSPropsPolyfill, useGDS } from "../hooks/index.js";
import { cn, getCSSPropsAttributes } from "../utils/index.js";
import { renderAddon } from "./base/Addon.js";
import { Button } from "./Button.js";
import { ButtonGroup } from "./ButtonGroup.js";
import { CodeBlockMeta } from "./CodeBlock.meta.js";
import { CopyButton } from "./CopyButton.js";
import { TabSet, TabSetContext } from "./TabSet.js";
// Can't import `@shikijs/vscode-textmate`'s `FontStyle` enum due to `verbatimModuleSyntax`
const FontStyle = {
    NotSet: -1,
    None: 0,
    Italic: 1,
    Bold: 2,
    Underline: 4,
};
const THEMES = {
    dark: 'night-owl',
    light: 'min-light',
};
const SHIKI_LANGUAGES = [
    'shellscript',
    'shell',
    'bash',
    'sh',
    'zsh',
    'html',
    'markdown',
    'md',
    'css',
    'javascript',
    'js',
    'jsx',
    'typescript',
    'ts',
    'tsx',
    'vue',
    'json',
    'yaml',
    'yml',
    'toml',
    'graphql',
    'gql',
    'sql',
    'php',
    'java',
    'ruby',
    'rust',
    'rs',
    'python',
    'py',
    'go',
    'swift',
    'solidity',
    'mermaid',
];
export const LANGUAGES = ['text', ...SHIKI_LANGUAGES];
function CodeBlockWrapper({ className, children, ...props }) {
    return (_jsxs("figure", { className: cn('root-flex flex-col bg-canvas', className), ...props, children: [children, _jsx("span", { className: "pointer-events-none absolute inset-0 rounded-inherit border border-subtle" })] }));
}
const CodeBlockHeaderContext = createContext(false);
function CodeBlockHeader({ buttons, className, children, ...props }) {
    return (_jsx(CodeBlockHeaderContext.Provider, { value: true, children: _jsxs("figcaption", { className: cn('root-flex h-10 shrink-0 gap-4 rounded-t-inherit bg-muted ps-2 pe-1', className), ...props, children: [_jsx("div", { className: "min-w-em grow", children: children }), buttons ? (_jsx(ButtonGroup, { variant: "naked", size: "medium", 
                    /**
                     * The `not-has-button:hidden` class prevents an empty `ButtonGroup` from taking space
                     * in the layout, which can happen when the stuff that `CodeBlockTabs` passes as
                     * `buttons` ends up rendering nothing (which we can't know in advance).
                     */
                    className: "me-2 shrink-0 self-center not-has-button:hidden", children: buttons })) : null, _jsx("div", { className: "absolute inset-x-0 bottom-0 border-b border-subtle" })] }) }));
}
export function CodeBlockRoot({ language = 'text', icon: passedIcon, label, highlight: passedHighlight, secret: passedSecret, masked: controlledMasked, onMaskedChange, copy: passedCopy, onCopy: passedOnCopy, wrap: passedWrap, diff: passedDiff, lineNumbers: passedLineNumbers, rows: passedRows, className, style, children, ...props }) {
    const { activeTheme } = useGDS();
    const tabsContext = useContext(CodeBlockTabsContext);
    const hasAncestorTabs = tabsContext !== null;
    const hasAncestorHeader = useContext(CodeBlockHeaderContext) !== false;
    const icon = passedIcon !== undefined ? passedIcon : getDefaultIcon(language);
    const tabsProps = hasAncestorTabs ? tabsContext.props : null;
    const highlights = [passedHighlight ?? tabsProps?.highlight]
        .flat()
        .filter((value) => Boolean(value));
    const secrets = [passedSecret ?? tabsProps?.secret]
        .flat()
        .filter((value) => Boolean(value));
    const [masked, setMasked] = useControlled(controlledMasked ?? tabsProps?.masked, true, (masked) => {
        onMaskedChange?.(masked);
        tabsProps?.onMaskedChange?.(masked);
    });
    const copy = passedCopy ?? tabsProps?.copy ?? true;
    const onCopy = passedOnCopy ?? tabsProps?.onCopy;
    const wrap = passedWrap ?? tabsProps?.wrap;
    const diff = passedDiff ?? tabsProps?.diff ?? false;
    const lineNumbers = passedLineNumbers ?? tabsProps?.lineNumbers;
    const rows = passedRows ?? tabsProps?.rows;
    const [cssPropsPolyfillPassedRef, cssPropsPolyfillAttributes] = useCSSPropsPolyfill(CodeBlockMeta, { wrap, lineNumbers }, {});
    const highlighter = useShikiHighlighter();
    const childrenString = Array.isArray(children) ? children.join('') : String(children);
    let code = childrenString;
    let codeToCopy = childrenString;
    let lineStyles = [];
    if (diff) {
        const linesWithDiffMarkers = code.split(/\r?\n/);
        const linesWithoutDiffMarkers = linesWithDiffMarkers.map((line) => line[0] && ['+', '-', ' '].includes(line[0]) ? line.slice(1) : line);
        lineStyles = linesWithDiffMarkers.map((line) => {
            const diffMarker = line[0];
            if (diffMarker === '+')
                return 'added';
            if (diffMarker === '-')
                return 'deleted';
            return null;
        });
        code = linesWithoutDiffMarkers.join('\n');
        codeToCopy = linesWithoutDiffMarkers
            .filter((_line, lineIndex) => lineStyles[lineIndex] !== 'deleted')
            .join('\n');
    }
    const lines = useDeepCompareMemo(() => {
        if (hasAncestorHeader)
            return [];
        const { tokens } = highlighter.codeToTokens(code, {
            theme: THEMES[activeTheme],
            lang: LANGUAGES.includes(language) ? language : 'text',
        });
        return tokens.map((lineTokens, lineIndex) => {
            const lineContent = lineTokens.map((token) => token.content).join('');
            const lineStyle = lineStyles[lineIndex] ?? null;
            let chunks = [];
            if (!lineContent) {
                return { chunks, style: lineStyle };
            }
            if (highlights.length === 0 && secrets.length === 0) {
                chunks = lineTokens.map((token) => ({
                    ...token,
                    highlighted: false,
                    secret: false,
                }));
            }
            else {
                const highlightRanges = highlights.length > 0 ? findRanges(lineContent, highlights) : [];
                const secretRanges = secrets.length > 0 ? findRanges(lineContent, secrets) : [];
                let currentTokenIndex = 0;
                let currentToken = lineTokens[0];
                let currentChunk = {
                    ...currentToken,
                    content: '',
                    highlighted: isInRange(0, highlightRanges),
                    secret: isInRange(0, secretRanges),
                };
                let currentOffset = currentChunk.offset;
                for (let characterIndex = 0; characterIndex < lineContent.length; characterIndex++) {
                    const character = lineContent[characterIndex];
                    const highlighted = isInRange(characterIndex, highlightRanges);
                    const secret = isInRange(characterIndex, secretRanges);
                    const moveToNextToken = currentOffset >= currentToken.offset + currentToken.content.length;
                    if (moveToNextToken ||
                        highlighted !== currentChunk.highlighted ||
                        secret !== currentChunk.secret) {
                        chunks.push(currentChunk);
                        if (moveToNextToken) {
                            currentTokenIndex++;
                            currentToken = lineTokens[currentTokenIndex];
                        }
                        currentChunk = { ...currentToken, content: character, highlighted, secret };
                    }
                    else {
                        currentChunk.content += character;
                    }
                    currentOffset++;
                }
                chunks.push(currentChunk);
            }
            // Split chunks by whitespace boundaries so each chunk is either pure whitespace or pure content
            chunks = chunks.flatMap((chunk) => {
                const parts = chunk.content.split(/(\s+)/).filter(Boolean);
                if (parts.length <= 1)
                    return [chunk];
                return parts.map((part) => ({ ...chunk, content: part }));
            });
            return { chunks, style: lineStyle };
        });
    }, [highlighter, childrenString, activeTheme, language, highlights, secrets, diff]);
    const buttonElements = [
        secrets.length > 0 && (_jsx(Button, { onClick: () => setMasked(!masked), children: masked ? (_jsx(EyeIcon, { alt: `Reveal secret${secrets.length > 1 ? 's' : ''}` })) : (_jsx(EyeClosedIcon, { alt: `Hide secret${secrets.length > 1 ? 's' : ''}` })) }, "reveal")),
        copy && (_jsx(CopyButton, { value: typeof copy === 'string' ? copy : codeToCopy, onCopy: onCopy }, "copy")),
    ].filter(Boolean);
    const buttons = buttonElements.length > 0 ? _jsx(_Fragment, { children: buttonElements }) : null;
    if (hasAncestorHeader) {
        return buttons;
    }
    const Wrapper = hasAncestorTabs ? 'div' : CodeBlockWrapper;
    const headerContent = label || passedIcon ? (_jsxs("div", { className: "flex h-full items-center gap-2 ps-2", children: [icon ? (_jsx("span", { className: "flex size-4 shrink-0 items-center justify-center **:icon:default-size-3.5", children: renderAddon(icon) })) : null, label ? _jsx("span", { className: "truncate text-14 text-muted", children: label }) : null] })) : null;
    return (_jsxs(Wrapper, { ref: cssPropsPolyfillPassedRef, "data-has-header": Boolean(headerContent) || undefined, className: cn(`gds-code-block group/code-block root-flex flex-col
        u:not-in-code-block-tabs:rounded-8
        u:not-in-code-block-tabs:not-data-has-header:min-h-13
        u:not-in-code-block-tabs:data-has-header:min-h-24`, className), ...getCSSPropsAttributes(CodeBlockMeta, { wrap, lineNumbers, rows }, style), ...cssPropsPolyfillAttributes, ...props, children: [headerContent ? (hasAncestorTabs ? (_jsxs("div", { className: "flex h-10 shrink-0 px-2", children: [_jsx("div", { className: "absolute inset-0 border-b border-subtle" }), headerContent] })) : (_jsx(CodeBlockHeader, { buttons: buttons, children: headerContent }))) : !hasAncestorTabs && buttons ? (_jsx("div", { className: `
            absolute end-3 top-3 z-10 opacity-0 transition
            group-not-hover/code-block:pointer-events-none
            group-hover/code-block:opacity-100
            has-focus-visible:opacity-100
          `, children: _jsx(ButtonGroup, { size: "xsmall", children: buttons }) })) : null, _jsx("pre", { dir: "ltr", 
                /**
                 * The Firefox-specific styles are to work around `max-h-max` not working as expected (see
                 * https://bugzilla.mozilla.org/show_bug.cgi?id=1672559). This means that a `CodeBlock` that
                 * has a height (either fixed or determined by the flex algorithm) that is taller than
                 * specified by the `rows` prop will be buggy in Firefox, but `rows` should not be set in
                 * that case, so it should be fine.
                 */
                className: `
          isolate scrollbar-thin h-(--max-height) max-h-max grow overflow-auto font-mono text-14
          [--max-height:calc(9999px-min(9999px,var(--gds-code-block-rows)*9999px)+var(--gds-code-block-rows)*1lh+2*var(--padding))]
          [--padding:--spacing(4)]
          firefox:h-auto
          firefox:max-h-(--max-height)
        `, children: _jsx("code", { 
                    /**
                     * The `min-w-max` class ensures that the right padding is preserved when there is
                     * horizontal scrolling, while `auto-rows-max` works around a Safari bug where wrapped
                     * lines have the wrong height.
                     */
                    className: `
            grid min-w-max auto-rows-max gap-x-2 py-(--padding)
            @prop-wrap-true/code-block:min-w-0
            @prop-line-numbers-true/code-block:grid-cols-[min-content_1fr]
          `, children: lines.map((line, lineIndex) => (_jsxs("span", { "aria-label": line.style === 'added'
                            ? 'Added line'
                            : line.style === 'deleted'
                                ? 'Deleted line'
                                : undefined, "data-style": line.style || undefined, className: cn('col-span-full grid min-h-lh grid-cols-subgrid items-baseline px-(--padding)', line.style
                            ? `data-[style=added]:bg-code-added
                    data-[style=added]:text-code-added
                    data-[style=deleted]:bg-code-deleted
                    data-[style=deleted]:text-code-deleted`
                            : null), children: [_jsx("span", { "aria-hidden": true, className: `
                  min-w-4 text-center text-12 text-subtle select-none
                  @prop-line-numbers-false/code-block:hidden
                `, children: lineIndex + 1 }), _jsxs("span", { children: [diff ? (_jsxs("span", { "aria-hidden": true, children: [line.style === 'added' ? '+' : null, line.style === 'deleted' ? '-' : null, line.style === null ? ' ' : null] })) : null, line.chunks.length === 0 ? (
                                    // Ensure line numbers are correctly aligned even when the line is empty
                                    _jsx("span", { className: "select-none", children: " " })) : (_jsx("span", { className: `
                      @prop-wrap-true/code-block:wrap-anywhere
                      @prop-wrap-true/code-block:whitespace-pre-wrap
                    `, children: line.chunks.map((chunk, chunkIndex) => {
                                            let chunkContent = chunk.content;
                                            if (chunk.secret && masked) {
                                                chunkContent = '•'.repeat(chunkContent.length);
                                            }
                                            return (_jsx("span", { "data-whitespace": chunk.content.trim() === '' || undefined, "data-highlighted": chunk.highlighted || undefined, className: cn(chunk.fontStyle === FontStyle.Bold ? 'font-bold' : null, chunk.fontStyle === FontStyle.Italic ? 'italic' : null, chunk.fontStyle === FontStyle.Underline ? 'underline' : null, chunk.highlighted
                                                    ? `z-10 bg-code-highlighted py-1 font-bold
                              not-has-[+[data-highlighted]]:-mr-1
                              not-has-[+[data-highlighted]]:rounded-r-4
                              not-has-[+[data-highlighted]]:pr-1
                              not-[[data-highlighted]+*]:-ml-1
                              not-[[data-highlighted]+*]:rounded-l-4
                              not-[[data-highlighted]+*]:pl-1`
                                                    : 'z-20', 
                                                // Prevent wrapping between the indentation and the code
                                                chunkIndex === 1 ? '[[data-whitespace]+*]:break-all' : null), style: {
                                                    color: chunk.color,
                                                    backgroundColor: chunk.bgColor,
                                                }, children: chunkContent }, chunkIndex));
                                        }) }))] })] }, lineIndex))) }) })] }));
}
CodeBlockRoot.displayName = 'CodeBlock';
const CodeBlockTabsContext = createContext(null);
export function CodeBlockTabs({ ref: passedRef, tabs: passedTabs, value, defaultValue, onChange, highlight, secret, masked: controlledMasked, onMaskedChange, copy, onCopy, wrap, diff, lineNumbers, rows, className, children, ...props }) {
    if (passedTabs.length === 0) {
        throw new Error('[CodeBlock.Tabs] At least 1 tab is required');
    }
    const childrenArray = flattenChildren(children);
    if (passedTabs.length !== childrenArray.length) {
        throw new Error(`[CodeBlock.Tabs] The number of tabs (${passedTabs.length}) must match the number of children (${childrenArray.length})`);
    }
    const tabs = passedTabs.map((tab, tabIndex) => {
        const tabConfig = typeof tab === 'string' ? { value: tab, label: tab } : tab;
        return { ...tabConfig, content: childrenArray[tabIndex] };
    });
    const [masked, setMasked] = useControlled(controlledMasked, undefined, onMaskedChange);
    const tabsContext = {
        props: {
            highlight,
            secret,
            masked,
            onMaskedChange: setMasked,
            copy,
            onCopy,
            wrap,
            diff,
            lineNumbers,
            rows,
        },
    };
    return (_jsx(TabSet, { ref: passedRef, value: value, defaultValue: defaultValue ?? tabs[0].value, onChange: onChange, className: cn(`gds-code-block-tabs root-flex flex-col u:min-h-24 u:rounded-8
        u:[--saved-default-line-numbers:var(--gds-default-line-numbers)]
        u:[--saved-default-rows:var(--gds-default-rows)]
        u:[--saved-default-wrap:var(--gds-default-wrap)]
        u:[--saved-prop-line-numbers:var(--gds-prop-line-numbers)]
        u:[--saved-prop-rows:var(--gds-prop-rows)]
        u:[--saved-prop-wrap:var(--gds-prop-wrap)]
        u:**:code-block:default-line-numbers-(--saved-default-line-numbers)
        u:**:code-block:default-rows-(--saved-default-rows)
        u:**:code-block:default-wrap-(--saved-default-wrap)
        u:**:code-block:prop-line-numbers-(--saved-prop-line-numbers)
        u:**:code-block:prop-rows-(--saved-prop-rows)
        u:**:code-block:prop-wrap-(--saved-prop-wrap)`, className), ...props, children: _jsxs(CodeBlockWrapper, { className: "grow rounded-inherit", children: [_jsx(CodeBlockHeader, { buttons: _jsx(CodeBlockTabsContext.Provider, { value: tabsContext, children: _jsx(TabSetContext.Consumer, { children: (tabSetContext) => tabs.map((tab) => (tabSetContext.selectedTab === tab.value ? tab.content : null)) }) }), children: _jsx(TabSet.Tabs, { size: "small", divider: false, className: "z-10 h-full", children: tabs.map((tab) => (_jsx(TabSet.Tab, { value: tab.value, addonBefore: tab.addonBefore, addonAfter: tab.addonAfter, disabled: tab.disabled, children: tab.label ?? tab.value }, tab.value))) }) }), _jsx(TabSet.Panels, { transition: { animateSize: 'clip' }, className: "grow", children: tabs.map((tab) => (_jsx(TabSet.Panel, { value: tab.value, className: "flex flex-col *:grow", children: _jsx(CodeBlockTabsContext.Provider, { value: tabsContext, children: tab.content }) }, tab.value))) })] }) }));
}
CodeBlockTabs.displayName = 'CodeBlock.Tabs';
function getDefaultIcon(language) {
    switch (language) {
        case 'shellscript':
        case 'shell':
        case 'bash':
        case 'sh':
        case 'zsh':
            return _jsx(TerminalIcon, { alt: "Terminal" });
        case 'html':
            return _jsx(FileHtmlIcon, { alt: "HTML" });
        case 'markdown':
        case 'md':
            return _jsx(FileMdIcon, { alt: "Markdown" });
        case 'css':
            return _jsx(FileCssIcon, { alt: "CSS" });
        case 'javascript':
        case 'js':
            return _jsx(FileJsIcon, { alt: "JavaScript" });
        case 'jsx':
            return _jsx(FileJsxIcon, { alt: "JSX" });
        case 'typescript':
        case 'ts':
            return _jsx(FileTsIcon, { alt: "TypeScript" });
        case 'tsx':
            return _jsx(FileTsxIcon, { alt: "TSX" });
        case 'vue':
            return _jsx(FileVueIcon, { alt: "Vue.js" });
        case 'sql':
            return _jsx(FileSqlIcon, { alt: "SQL" });
        case 'rust':
        case 'rs':
            return _jsx(FileRsIcon, { alt: "Rust" });
        case 'python':
        case 'py':
            return _jsx(FilePyIcon, { alt: "Python" });
        default:
            return _jsx(FileCodeIcon, { alt: "" });
    }
}
/**
 * Creates a hook that returns a Shiki highlighter.
 *
 * Shiki's `createHighlighter` is async, so we call it immediately and use `useSyncExternalStore` to
 * re-render components when it resolves. Until then, we return a fallback highlighter that renders
 * code as plain text (no syntax highlighting).
 */
function createUseShikiHighlighter(themes, langs) {
    const fallback = {
        codeToTokens: (code) => {
            let offset = 0;
            return {
                tokens: code.split('\n').map((line) => {
                    const lineTokens = [{ offset, content: line }];
                    offset += line.length + 1; // +1 for the newline character
                    return lineTokens;
                }),
            };
        },
    };
    let highlighter = null;
    const listeners = new Set();
    void createHighlighter({ themes, langs }).then((h) => {
        highlighter = h;
        listeners.forEach((listener) => listener());
    });
    return function useShikiHighlighter() {
        return useSyncExternalStore((listener) => {
            listeners.add(listener);
            return () => listeners.delete(listener);
        }, () => highlighter ?? fallback, () => fallback);
    };
}
const useShikiHighlighter = createUseShikiHighlighter(Object.values(THEMES), SHIKI_LANGUAGES);
function findRanges(string, substrings) {
    const ranges = [];
    substrings.forEach((substring) => {
        const regex = new RegExp(escapeStringRegexp(substring), 'gi');
        let match;
        while ((match = regex.exec(string)) !== null) {
            ranges.push({ start: match.index, end: match.index + match[0].length });
            regex.lastIndex = match.index + 1; // Ensure `findRanges('aaa', ['aa'])` returns two ranges, not one
        }
    });
    return ranges;
}
function isInRange(index, ranges) {
    return ranges.some((range) => index >= range.start && index < range.end);
}
//# sourceMappingURL=CodeBlock.parts.js.map