/*
 * Copyright 2020 The Kubernetes Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import React from 'react';
import { v4 as uuid } from 'uuid';
import { hackMarkdownSource, inlineSnippets, rehypeCodeIndexer, rehypeTabbed, rehypeTip, rehypeWizard } from 'madwizard/dist/parser';
import { newChoiceState } from 'madwizard/dist/choices';
import { TextContent } from '@patternfly/react-core/dist/esm/components/Text/TextContent';
import { inElectron } from '@kui-shell/core/mdist/api/Capabilities';
import ReactMarkdown from 'react-markdown';
// GitHub Flavored Markdown plugin; see https://github.com/IBM/kui/issues/6563
import gfm from 'remark-gfm';
// ==foo== -> <mark>foo</mark>
import hackMarks from './remark-mark.js';
// ++ctrl+alt+delete++== -> <kbd>ctrl</kbd>+<kbd>alt</kbd>+<kbd>delete</kbd>
import hackKeys from './remark-keys.js';
// parses out ::import{filepath} as node.type === 'leafDirective', but
// does not create any DOM elements
import remarkDirective from 'remark-directive';
import emojis from 'remark-emoji';
import frontmatter from 'remark-frontmatter';
import components from './components';
// react-markdown v6+ now require use of these to support html
import rehypeRaw from 'rehype-raw';
import rehypeSlug from 'rehype-slug';
import icons from './rehype-icons';
import { kuiFrontmatter, tryFrontmatter } from './frontmatter';
const rehypePlugins = (uuid, choices, filepath) => [
    rehypeWizard,
    [rehypeTabbed, uuid, choices, { optimize: { aprioris: inElectron() } }],
    rehypeTip,
    [rehypeCodeIndexer, uuid, filepath, {}, undefined, true],
    icons,
    rehypeRaw,
    rehypeSlug
];
const remarkPlugins = (tab) => [
    gfm,
    remarkDirective,
    [frontmatter, ['yaml', 'toml']],
    [kuiFrontmatter, { tab }],
    emojis // [emojis, { emoticon: true }]
];
export default class Markdown extends React.PureComponent {
    constructor(props) {
        super(props);
        this.mounted = false;
        /** Handlers to be called on unmount */
        this.cleaners = [];
        this.uuid = uuid();
        this.choices = this.props.choices || newChoiceState('default');
        /** This will be the `components` argument to `<ReactMarkdown/>` */
        this._components = components({
            mdprops: this.props,
            repl: this.repl,
            uuid: this.uuid,
            choices: this.choices,
            codeBlockResponses: this.codeBlockResponseWithReplayedBit.bind(this)
        });
        this._remarkPlugins = remarkPlugins(this.props.tab);
        this._rehypePlugins = rehypePlugins(this.uuid, this.choices, this.props.filepath);
        this.state = {
            source: '',
            hasError: false
        };
    }
    /** We are going away. Invoke cleaners */
    componentWillUnmount() {
        this.mounted = false;
        this.cleaners.forEach(_ => _());
    }
    /** We are coming to life */
    componentDidMount() {
        this.mounted = true;
        this.prepareSource();
    }
    /** New props, so re-fetch inlined source if needed */
    componentDidUpdate(prevProps) {
        if (prevProps.source !== this.props.source) {
            this.prepareSource();
        }
    }
    /**
     * Base path for snippet inlining. Prefer `this.props`, which comes
     * from user/command line. Otherwise, look in the top matter of the
     * document.  Later, in `inlineSnippets` we will also look in each
     * include line in the document, which may also specify a
     * line-specific base path.
     */
    get snippetBasePath() {
        if (this.props.snippetBasePath) {
            return this.props.snippetBasePath;
        }
        else {
            const fm = tryFrontmatter(this.props.source);
            if (fm.attributes && fm.attributes.snippets && fm.attributes.snippets.basePath) {
                return fm.attributes.snippets.basePath;
            }
        }
    }
    static getDerivedStateFromError() {
        return { hasError: true };
    }
    componentDidCatch(error, errorInfo) {
        console.error(error, errorInfo);
    }
    /**
     * To support snippet inlining (which may require an async file
     * fetch), we need to prepare the source from props into state. Yay
     * react.
     */
    prepareSource() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.mounted) {
                return;
            }
            const sourcePriorToInlining = (yield this.source(this.props)).trim();
            if (this.props.tab && this.props.tab.REPL) {
                setTimeout(() => __awaiter(this, void 0, void 0, function* () {
                    if (this.mounted) {
                        const args = { REPL: this.repl };
                        const { loadNotebook } = yield import('@kui-shell/plugin-client-common/notebook');
                        const fetcher = (filepath) => loadNotebook(filepath, args).then(_ => _.toString());
                        const source = yield inlineSnippets({
                            fetcher,
                            madwizardOptions: {},
                            snippetBasePath: this.snippetBasePath
                        })(sourcePriorToInlining, this.props.filepath);
                        this.setState({ source: Markdown.hackSource(source) });
                    }
                }));
            }
            else {
                this.setState({ source: Markdown.hackSource(sourcePriorToInlining) });
            }
        });
    }
    codeBlockResponseWithReplayedBit(codeBlockIdx) {
        const fromProps = this.props.codeBlockResponses ? this.props.codeBlockResponses[codeBlockIdx] : undefined;
        // replayed if we have a response in props, because the commentary
        // controller has fetched this from a json from that stores
        // pre-recorded output
        const replayed = !!fromProps;
        // the current response to be displayed favors state, as these
        // come from re-executions in this session
        const response = fromProps || undefined;
        return Object.assign({ replayed }, response);
    }
    /** `exec` controller */
    get repl() {
        return this.props.tab ? this.props.tab.REPL : undefined;
    }
    /** @return markdown source, as string in application/markdown format */
    source(props) {
        return __awaiter(this, void 0, void 0, function* () {
            if (props.contentType === 'text/html') {
                const [{ default: TurndownService }, { gfm }] = yield Promise.all([
                    import('turndown'),
                    import('turndown-plugin-gfm')
                ]);
                const td = new TurndownService();
                td.use(gfm);
                return td.turndown(props.source);
            }
            else {
                return props.source;
            }
        });
    }
    /**
     * Deal with pymdown indentation syntax, and other not-worth-parsing
     * syntax from pymdown (such as target=_blank for links).
     */
    static hackSource(source) {
        return hackKeys(hackMarks(hackMarkdownSource(source)))
            .trim()
            .replace(/\){target=[^}]+}/g, ')')
            .replace(/{draggable=(false|true)}/g, '');
    }
    get className() {
        return (this.props.className ||
            'padding-content marked-content page-content ' +
                (this.props.extraClassName || '') +
                (!this.props.nested ? ' scrollable scrollable-x scrollable-auto' : ''));
    }
    render() {
        if (this.props.onRender) {
            this.props.onRender();
        }
        if (this.state.hasError) {
            return React.createElement(TextContent, null, "Internal Error");
        }
        const { source } = this.state;
        if (source.length === 0) {
            return React.createElement(React.Fragment, null);
        }
        return (React.createElement(React.Suspense, { fallback: React.createElement("div", null) },
            React.createElement(TextContent, null,
                React.createElement(ReactMarkdown, { className: this.className, components: this._components(), remarkPlugins: this._remarkPlugins, rehypePlugins: this._rehypePlugins, "data-is-nested": this.props.nested || undefined }, source))));
    }
}
//# sourceMappingURL=index.js.map