/**
 * @bobfrankston/rmf-tiny — TinyMCE adapter for rmfmail.
 *
 * MIT-licensed adapter glue. Imports TinyMCE at runtime; ships no
 * TinyMCE bytes. The user is responsible for making TinyMCE reachable
 * to their environment:
 *
 *   - Desktop / Node-side install:  `npm install tinymce`
 *     (rmfmail's createEditor("tinymce") branch dynamically imports
 *     this adapter, which dynamically imports tinymce from
 *     node_modules.)
 *
 *   - Android / browser-only WebView: pass `cdnUrl` in the options to
 *     have the adapter inject a <script src=…> tag. The bytes are
 *     fetched directly from Tiny's CDN (or whatever URL the user
 *     configured); the host APK / page never carries them.
 *
 * The adapter implements the same `MailxEditor` shape as the Quill /
 * tiptap factories in rmfmail's editor.ts, so the host code path is
 * identical regardless of which editor is in use.
 */
/** Load tinymce. Tries native module import first (desktop / npm-installed);
 *  falls back to script-tag injection from cdnUrl. Resolves to whatever's
 *  on `window.tinymce`. */
async function loadTinymce(opts) {
    // 1. Try the npm-installed path. Wrapped in try because in browser-only
    //    environments the module specifier won't resolve and we want to
    //    fall through to CDN injection.
    try {
        const mod = await import(/* @vite-ignore */ "tinymce");
        const tinymce = mod.default || mod;
        // Pull in the plugins TinyMCE's paste-from-Word handler needs. These
        // are side-effect imports — they register themselves on the global.
        await Promise.all([
            import("tinymce/themes/silver").catch(() => { }),
            import("tinymce/icons/default").catch(() => { }),
            import("tinymce/models/dom").catch(() => { }),
            import("tinymce/plugins/paste").catch(() => { }),
            import("tinymce/plugins/lists").catch(() => { }),
            import("tinymce/plugins/link").catch(() => { }),
            import("tinymce/plugins/table").catch(() => { }),
            import("tinymce/plugins/code").catch(() => { }),
            import("tinymce/plugins/image").catch(() => { }),
        ]);
        return tinymce;
    }
    catch {
        // Fall through.
    }
    // 2. Already loaded by a prior call.
    const w = window;
    if (w.tinymce)
        return w.tinymce;
    // 3. Inject from CDN if provided.
    if (!opts.cdnUrl) {
        throw new Error("rmf-tiny: tinymce not installed (npm install tinymce) and no cdnUrl supplied. See README for Android setup.");
    }
    const url = opts.apiKey && !opts.cdnUrl.includes("api-key")
        ? `${opts.cdnUrl}${opts.cdnUrl.includes("?") ? "&" : "?"}apiKey=${encodeURIComponent(opts.apiKey)}`
        : opts.cdnUrl;
    await new Promise((resolve, reject) => {
        const s = document.createElement("script");
        s.src = url;
        s.referrerPolicy = "origin";
        s.onload = () => resolve();
        s.onerror = () => reject(new Error(`rmf-tiny: failed to load TinyMCE from ${url}`));
        document.head.appendChild(s);
    });
    if (!w.tinymce)
        throw new Error("rmf-tiny: TinyMCE script loaded but window.tinymce is missing");
    return w.tinymce;
}
/** Create a TinyMCE-backed MailxEditor in `container`. */
export async function createTinyMceEditor(container, opts = {}) {
    const tinymce = await loadTinymce(opts);
    // TinyMCE attaches to a target element by id selector; create one
    // inside the host container so multiple editors on a page don't
    // collide and so we don't pollute the caller's element with TinyMCE
    // attributes directly.
    const target = document.createElement("div");
    target.id = `rmf-tiny-${Math.random().toString(36).slice(2, 10)}`;
    target.style.cssText = "width:100%;height:100%;";
    container.appendChild(target);
    const editor = await new Promise((resolve) => {
        tinymce.init({
            target,
            // Word-paste fidelity is the entire point of using TinyMCE here.
            // `paste_as_text: false` keeps formatting; powerpaste isn't in
            // OSS but the standard paste plugin handles Word reasonably well.
            // All free / OSS plugins bundled in the jsDelivr tinymce@6 script.
            // No premium plugins listed — listing one without an API key
            // triggers TinyMCE's upsell dialog on every load.
            plugins: "paste lists advlist link table code codesample image searchreplace autolink wordcount emoticons charmap insertdatetime quickbars nonbreaking directionality help",
            toolbar: [
                "undo redo | bold italic underline strikethrough | forecolor backcolor",
                "bullist numlist outdent indent | link table image code codesample | emoticons charmap | help",
            ].join(" | "),
            // Include "tools" so wordcount and searchreplace are reachable.
            menubar: "file edit view insert format tools",
            // View menu — append zoom commands. fullscreen omitted: this editor
            // lives inside a compose iframe overlay that already fills its host
            // window; TinyMCE's fullscreen plugin re-positions the container in
            // ways that produce a blank body, so it's not in the plugin list.
            menu: {
                view: { title: "View", items: "code | visualaid visualchars visualblocks | preview | zoomIn zoomOut zoomReset" },
            },
            // Disable the "Get all features" upsell badge that TinyMCE 6+
            // injects automatically when no premium plugins are listed.
            promotion: false,
            // Floating toolbar that appears on text selection — keeps the
            // main toolbar uncluttered while exposing common formatters
            // where the cursor already is. quickbars_insert_toolbar:false
            // suppresses the second (insert-on-blank-line) popup which
            // clutters more than it helps in an email composer.
            quickbars_selection_toolbar: "bold italic underline | forecolor | quicklink blockquote",
            quickbars_insert_toolbar: false,
            quickbars_image_toolbar: "alignleft aligncenter alignright",
            // Code-sample dropdown languages. TinyMCE's default list omits
            // "Text" / plain — every option triggers syntax highlighting
            // which mangles unrelated paste content (Bob 2026-05-24).
            // Adding Text first so it's the default; rest are the modern
            // languages we actually paste.
            codesample_languages: [
                { text: "Text", value: "text" },
                { text: "HTML/XML", value: "markup" },
                { text: "JavaScript", value: "javascript" },
                { text: "TypeScript", value: "typescript" },
                { text: "CSS", value: "css" },
                { text: "JSON", value: "json" },
                { text: "Python", value: "python" },
                { text: "Java", value: "java" },
                { text: "C", value: "c" },
                { text: "C++", value: "cpp" },
                { text: "C#", value: "csharp" },
                { text: "Go", value: "go" },
                { text: "Rust", value: "rust" },
                { text: "Ruby", value: "ruby" },
                { text: "PHP", value: "php" },
                { text: "Shell", value: "bash" },
                { text: "SQL", value: "sql" },
            ],
            // WebView's native spell-check (red underlines, right-click
            // "Add to dictionary"). Free; same UX as Quill's spellcheck=true.
            // Premium tinymcespellchecker plugin would replace this with a
            // custom backend via spellchecker_rpc_url, but requires a
            // Tiny Cloud subscription.
            browser_spellcheck: true,
            // Right-click context menu DISABLED so WebView2's native menu
            // fires instead. We need the native menu because that's where
            // the browser shows spelling suggestions (TinyMCE's contextmenu
            // intercepts the right-click and replaces the menu — red
            // underlines appear but suggestions are unreachable). Trade-off:
            // lose TinyMCE's link / image / table quick actions. Standard
            // text formatting is still on the toolbar.
            contextmenu: false,
            statusbar: false,
            branding: false,
            license_key: "gpl",
            paste_data_images: true,
            // Permissive valid_elements — preserve as much of the source
            // formatting as possible. The default is more aggressive about
            // stripping. Empty string for `valid_elements` means accept
            // everything that the schema allows.
            paste_word_valid_elements: "@[style|class],-strong/b,-em/i,-u,-s,-sub,-sup,-strike,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,-blockquote,-table[border|cellpadding|cellspacing|width|height|class|style],-tr,-td[colspan|rowspan|width|height|class|style|valign|align|background|bgcolor],-th,-thead,-tbody,-tfoot,-pre,-br,-a[href|target|title],-img[src|alt|width|height|style|class]",
            paste_retain_style_properties: "color background background-color font-family font-size font-weight font-style text-decoration text-align padding padding-top padding-bottom padding-left padding-right margin margin-top margin-bottom margin-left margin-right border border-top border-bottom border-left border-right",
            // Auto-link bare URLs in pasted content. TinyMCE's `autolink`
            // plugin only fires on TYPED space/enter; URLs that arrive via
            // clipboard (browser address bar, terminal copy) come in as
            // plain text and stay un-linked. paste_preprocess runs on the
            // HTML the paste plugin produced.
            //
            // CRITICAL: skip auto-link when the content already contains
            // anchors. Naive regex over the whole content would wrap
            // `<a href="X">X</a>` in ANOTHER anchor (nested anchors are
            // invalid HTML and browsers split them, producing visible
            // junk). For HTML pastes that already have linked URLs (the
            // common case from a browser address bar or another mail
            // client), TinyMCE preserves them — no auto-link pass needed.
            // For plain-text pastes (no anchors present), wrap any bare
            // http(s)://… runs. Trailing sentence punctuation is excluded
            // from the URL.
            paste_preprocess: (_plugin, args) => {
                if (/<a[\s>]/i.test(args.content))
                    return;
                args.content = args.content.replace(/(^|[\s(\[])((?:https?|ftp):\/\/[^\s<>"']+[^\s<>"'.,;:!?)\]])/gi, (_m, lead, url) => `${lead}<a href="${url}">${url}</a>`);
            },
            // Body font + quoted-reply styling. Without explicit rules for
            // <blockquote> and div.reply the editor iframe renders the
            // quoted block with no visual distinction from the user's own
            // text — pasted reply quotes vanish into a wall of unformatted
            // paragraphs (Bob 2026-05-12: "the body losing all formatting").
            // The styles below match Thunderbird/Outlook conventions: a
            // muted left-bar + indent on blockquotes, slight color shift on
            // the "On … wrote:" attribution, untouched typography for
            // everything else so genuine HTML formatting (bold / italic /
            // tables / inline color) still comes through verbatim.
            content_style: [
                // Dark-blue native caret bar. caret-shape:block produced a
                // column wider than the gap between letters AND caused the
                // caret to "scoot back to its old position" after an arrow
                // press (Chromium block-caret quirk with the arrow-key
                // selection adjustment, Bob 2026-05-24). Plain bar is
                // browser-default and behaves correctly with arrow keys;
                // just darken the color so it stays visible.
                "body { font-family: system-ui, sans-serif; font-size: 14px; caret-color: #0a2647; }",
                "blockquote { border-left: 3px solid #c0c8d0; margin: 0 0 0 4px; padding: 2px 0 2px 10px; color: #555; }",
                "div.reply { margin-top: 0.5em; }",
                "div.reply > p:first-child { color: #666; font-size: 0.95em; margin: 0 0 4px 0; }",
                "pre, code { font-family: ui-monospace, Consolas, Menlo, monospace; }",
            ].join(" "),
            init_instance_callback: (ed) => resolve(ed),
            setup: (ed) => {
                if (opts.initialHtml)
                    ed.on("init", () => ed.setContent(opts.initialHtml));
                // Zoom — content font-size scales between ZOOM_MIN and ZOOM_MAX.
                // Reachable via View → Zoom in/out/reset, Ctrl+wheel inside the
                // editor iframe, and Ctrl+= / Ctrl+- / Ctrl+0 shortcuts.
                const ZOOM_DEFAULT = 14;
                const ZOOM_STEP = 2;
                const ZOOM_MIN = 8;
                const ZOOM_MAX = 32;
                let zoomPx = ZOOM_DEFAULT;
                const applyZoom = () => {
                    try {
                        const body = ed.getBody();
                        if (body)
                            body.style.fontSize = `${zoomPx}px`;
                    }
                    catch { /* */ }
                };
                const bumpZoom = (delta) => {
                    zoomPx = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoomPx + delta));
                    applyZoom();
                };
                ed.ui.registry.addMenuItem("zoomIn", {
                    text: "Zoom in", shortcut: "Ctrl+=",
                    onAction: () => bumpZoom(ZOOM_STEP),
                });
                ed.ui.registry.addMenuItem("zoomOut", {
                    text: "Zoom out", shortcut: "Ctrl+-",
                    onAction: () => bumpZoom(-ZOOM_STEP),
                });
                ed.ui.registry.addMenuItem("zoomReset", {
                    text: "Reset zoom", shortcut: "Ctrl+0",
                    onAction: () => { zoomPx = ZOOM_DEFAULT; applyZoom(); },
                });
                // Keyboard shortcuts go on the iframe doc directly. TinyMCE's
                // `addShortcut` is too late — WebView2 intercepts Ctrl+= and
                // Ctrl+- for its built-in page zoom, so we have to call
                // preventDefault in the capture phase to win the race.
                // Bound below in the "init" handler once the iframe doc exists.
                // Typing right after a link must not extend the link. When
                // any text insertion is about to happen with the caret
                // collapsed at the very end of an <a>, hop the caret to just
                // after the <a> first — otherwise contenteditable keeps
                // appending into the link and the whole sentence turns into
                // link text (Bob 2026-05-18: "I typed a URL but it then
                // included everything else I typed in the URL text"; Bob
                // 2026-05-25 reconfirmed). Covers autolink, pasted URLs,
                // hand-made links, IME composition, and clipboard paste.
                //
                // Uses `beforeinput` (modern, fires for ALL text insertion
                // kinds — keypress is deprecated and silently skips IME /
                // paste / autocomplete on Chromium). Done as a raw DOM
                // listener so it runs BEFORE TinyMCE's own beforeinput
                // handling, which is what reaches into the <a> and extends
                // it. Re-bound on every `init` (the iframe doc is recreated
                // on setContent, fullscreen, etc.).
                const installLinkEscape = () => {
                    const doc = ed.getDoc();
                    if (!doc || doc.__rmfLinkEscapeInstalled)
                        return;
                    doc.__rmfLinkEscapeInstalled = true;
                    doc.addEventListener("beforeinput", (e) => {
                        // Only intercept text-insertion kinds. Deletes,
                        // formatting, history nav don't extend links.
                        const kind = e.inputType || "";
                        const isInsert = kind === "insertText"
                            || kind === "insertCompositionText"
                            || kind === "insertFromPaste"
                            || kind === "insertFromDrop"
                            || kind === "insertFromComposition";
                        if (!isInsert)
                            return;
                        const sel = doc.getSelection();
                        if (!sel || sel.rangeCount === 0)
                            return;
                        const rng = sel.getRangeAt(0);
                        if (!rng.collapsed)
                            return;
                        const node = rng.startContainer;
                        const a = (node.nodeType === Node.ELEMENT_NODE ? node : node.parentNode);
                        if (!a)
                            return;
                        const link = a.closest?.("a");
                        if (!link)
                            return;
                        // Range from caret to end-of-link: empty ⇒ caret is
                        // at the link's trailing edge. Anything else means
                        // mid-link edit, which we want to leave alone.
                        let tail;
                        try {
                            tail = rng.cloneRange();
                            tail.setEndAfter(link);
                        }
                        catch {
                            return;
                        }
                        if (tail.toString().length > 0)
                            return;
                        const after = doc.createRange();
                        after.setStartAfter(link);
                        after.collapse(true);
                        sel.removeAllRanges();
                        sel.addRange(after);
                    }, true);
                };
                ed.on("init", installLinkEscape);
                ed.on("SetContent", installLinkEscape);
                ed.on("init", () => {
                    // Engage WebView2's native spellcheck. TinyMCE's own
                    // `browser_spellcheck: true` is a no-op — its code path
                    // only DISABLES spellcheck on false; on true it leaves
                    // the iframe body without an explicit attribute, and
                    // WebView2 doesn't engage red underlines on a fresh
                    // contenteditable iframe without `spellcheck="true"` set.
                    try {
                        const body = ed.getBody();
                        if (body) {
                            body.setAttribute("spellcheck", "true");
                            body.setAttribute("lang", "en");
                        }
                        const doc = ed.getDoc();
                        if (doc?.documentElement)
                            doc.documentElement.setAttribute("lang", "en");
                    }
                    catch { /* */ }
                    // Ctrl+wheel zoom inside the editor iframe.
                    try {
                        const doc = ed.getDoc();
                        doc.addEventListener("wheel", (e) => {
                            if (!e.ctrlKey)
                                return;
                            e.preventDefault();
                            bumpZoom(e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP);
                        }, { passive: false });
                        // Capture-phase keydown so we beat WebView2's built-in
                        // page-zoom binding. `e.key` is "=" (or "+" with shift),
                        // "-", and "0". MetaKey covers Mac Cmd; ctrlKey covers
                        // Windows/Linux Ctrl. Numpad +/- arrive as "+" / "-"
                        // already so a single check covers both.
                        doc.addEventListener("keydown", (e) => {
                            if (!(e.ctrlKey || e.metaKey))
                                return;
                            if (e.key === "=" || e.key === "+") {
                                e.preventDefault();
                                e.stopPropagation();
                                bumpZoom(ZOOM_STEP);
                            }
                            else if (e.key === "-") {
                                e.preventDefault();
                                e.stopPropagation();
                                bumpZoom(-ZOOM_STEP);
                            }
                            else if (e.key === "0") {
                                e.preventDefault();
                                e.stopPropagation();
                                zoomPx = ZOOM_DEFAULT;
                                applyZoom();
                            }
                        }, true);
                    }
                    catch { /* */ }
                });
            },
        });
    });
    return {
        setHtml(html) { editor.setContent(html); },
        getHtml() { return editor.getContent(); },
        getText() { return editor.getContent({ format: "text" }); },
        focus() { editor.focus(); },
        setCursor(pos) {
            // TinyMCE works in ranges, not character indices. Map the
            // common rmfmail usages: pos === 0 → cursor at TOP of body
            // (reply / forward — user types above the quoted block);
            // anything else → cursor at END (legacy "put cursor at end"
            // semantics).
            const place = () => {
                const body = editor.getBody();
                if (pos === 0) {
                    // Put the caret INSIDE the first block element. Collapsing
                    // to raw body-start lands it outside any block (before /
                    // between bare nodes) where contentEditable insertion is
                    // unpredictable — Bob 2026-05-21: "typing goes in the
                    // wrong place until you try again". rmfmail's reply body
                    // now leads with a real <p>; drop the caret into it.
                    const first = body.firstChild;
                    if (first && first.nodeType === 1 /* element */) {
                        editor.selection.setCursorLocation(first, 0);
                    }
                    else {
                        editor.selection.select(body, true);
                        editor.selection.collapse(true);
                    }
                    editor.focus();
                    // Viewport to the top so the user looks at the empty
                    // reply line, not scrolled down into the quote.
                    editor.getWin()?.scrollTo(0, 0);
                }
                else {
                    editor.selection.select(body, true);
                    editor.selection.collapse(false);
                    editor.focus();
                    editor.selection.scrollIntoView();
                }
            };
            try {
                place();
                // Re-apply on the next frame. Cross-iframe focus (compose is
                // an iframe; TinyMCE's edit area is a nested iframe) lets the
                // first selection set get clobbered by a late layout / focus
                // event — the "have to click in and try again" symptom. A
                // second pass after the frame settles makes it stick.
                editor.getWin()?.requestAnimationFrame?.(() => {
                    try {
                        place();
                    }
                    catch { /* */ }
                });
            }
            catch { /* */ }
        },
        get root() { return editor.getContainer(); },
        on(event, handler) { editor.on(event, handler); },
        off(event, handler) { editor.off(event, handler); },
        nativeEditor: editor,
    };
}
//# sourceMappingURL=adapter.js.map