Jump to content
Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
If you make an edit, a temporary account will be created for you.

MediaWiki:Gadget-PreloadEditor.js: Difference between revisions

MediaWiki interface page
ReisuDesign (talk | contribs)
No edit summary
ReisuDesign (talk | contribs)
No edit summary
 
(4 intermediate revisions by the same user not shown)
Line 3: Line 3:


     const PreloadEditor = {
     const PreloadEditor = {
         version: '1.0.0',
         version: '1.2.0',


         state: {
         state: {
Line 33: Line 33:
         editorTools: null,
         editorTools: null,


         draftStorageKey:
         draftStorageKey: 'PetitPlanet.PreloadEditor.drafts.v1',
            'PetitPlanet.PreloadEditor.drafts.v1',


         defaults: {
         defaults: {
             preload:
             preload: '<includeonly>\n<!-- preload here -->\n</includeonly>\n<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n'
                '<includeonly>\n<!-- preload here -->\n</includeonly>\n<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n'
         },
         },


Line 47: Line 45:
         getDraftStorage() {
         getDraftStorage() {
             try {
             try {
                 const raw =
                 const raw = window.localStorage.getItem(this.draftStorageKey);
                    window.localStorage.getItem(
                        this.draftStorageKey
                    );


                 if (!raw) {
                 if (!raw) {
Line 59: Line 54:
                 }
                 }


                 const parsed =
                 const parsed = JSON.parse(raw);
                    JSON.parse(raw);


                 if (
                 if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
                    !parsed ||
                    typeof parsed !== 'object' ||
                    Array.isArray(parsed)
                ) {
                     return {
                     return {
                         active: null,
                         active: null,
Line 73: Line 63:
                 }
                 }


                 if (
                 if (!parsed.drafts || typeof parsed.drafts !== 'object' || Array.isArray(parsed.drafts)) {
                    !parsed.drafts ||
                    typeof parsed.drafts !== 'object' ||
                    Array.isArray(parsed.drafts)
                ) {
                     parsed.drafts = {};
                     parsed.drafts = {};
                 }
                 }
Line 83: Line 69:
                 return parsed;
                 return parsed;
             } catch (error) {
             } catch (error) {
                 console.warn(
                 console.warn('Preload Editor: Could not read local drafts.', error);
                    'Preload Editor: Could not read local drafts.',
                    error
                );


                 return {
                 return {
Line 97: Line 80:
         setDraftStorage(storage) {
         setDraftStorage(storage) {
             try {
             try {
                 window.localStorage.setItem(
                 window.localStorage.setItem(this.draftStorageKey, JSON.stringify(storage));
                    this.draftStorageKey,
                    JSON.stringify(storage)
                );
 
                 return true;
                 return true;
             } catch (error) {
             } catch (error) {
                 console.warn(
                 console.warn('Preload Editor: Could not save local draft.', error);
                    'Preload Editor: Could not save local draft.',
                    error
                );
 
                 return false;
                 return false;
             }
             }
Line 114: Line 89:


         getDraftId(title) {
         getDraftId(title) {
             title =
             title = String(title || '').trim();
                String(title || '').trim();
             return title ? title.toLowerCase() : '__untitled__';
 
             return title
                ? title.toLowerCase()
                : '__untitled__';
         },
         },


         getDraftSnapshot() {
         getDraftSnapshot() {
             let content =
             let content = this.getEditorContent();
                this.getEditorContent();


             if (
             if (!this.editorView && !content) {
                !this.editorView &&
                 content = this.state.preloadContent || '';
                !content
            ) {
                 content =
                    this.state.preloadContent ||
                    '';
             }
             }


Line 138: Line 103:


             try {
             try {
                 config =
                 config = this.state.config
                    this.state.config
                    ? JSON.parse(JSON.stringify(this.state.config))
                        ? JSON.parse(
                    : this.emptyConfig();
                            JSON.stringify(
                                this.state.config
                            )
                        )
                        : this.emptyConfig();
             } catch (error) {
             } catch (error) {
                 console.warn(
                 console.warn('Preload Editor: Could not clone config for local draft.', error);
                    'Preload Editor: Could not clone config for local draft.',
                 config = this.emptyConfig();
                    error
                );
 
                 config =
                    this.emptyConfig();
             }
             }


             return {
             return {
                 version: 3,
                 version: 3,
                preloadTitle: String(this.state.preloadTitle || ''),
                preloadSource: String(this.state.preloadSource || ''),
                preloadContent: String(content || ''),
                config,


                 preloadTitle:
                 variables: Array.isArray(this.state.variables)
                     String(
                     ? this.state.variables.slice()
                        this.state.preloadTitle || ''
                     : [],
                     ),


                 preloadSource:
                 conditions: Array.isArray(this.state.conditions)
                     String(
                     ? this.state.conditions.slice()
                        this.state.preloadSource || ''
                     : [],
                     ),


                 preloadContent:
                 selectedVariable: String(this.state.selectedVariable || 'PAGE'),
                    String(
                        content || ''
                    ),


                 config,
                 deletedVariables: Array.isArray(this.state.deletedVariables)
                    ? this.state.deletedVariables.slice()
                    : [],


                 variables:
                 mode: this.state.mode || 'edit',
                    Array.isArray(
                dirty: !!this.state.dirty,
                        this.state.variables
                dirtyPreload: !!this.state.dirtyPreload,
                    )
                dirtyConfig: !!this.state.dirtyConfig,
                        ? this.state.variables.slice()
                savedAt: Date.now()
                        : [],
            };
        },


                conditions:
        updateLocalDraftStatus(message, type) {
                    Array.isArray(
            if (!this.localDraftStatus) {
                        this.state.conditions
                return;
                    )
            }
                        ? this.state.conditions.slice()
                        : [],


                selectedVariable:
             this.localDraftStatus.removeClass(
                    String(
                        this.state.selectedVariable ||
                        'PAGE'
                    ),
 
                deletedVariables:
                    Array.isArray(
                        this.state.deletedVariables
                    )
                        ? this.state.deletedVariables.slice()
                        : [],
 
                mode:
                    this.state.mode ||
                    'edit',
 
                dirty:
                    !!this.state.dirty,
 
                dirtyPreload:
                    !!this.state.dirtyPreload,
 
                dirtyConfig:
                    !!this.state.dirtyConfig,
 
                savedAt:
                    Date.now()
            };
        },
 
        updateLocalDraftStatus(
            message,
            type
        ) {
            if (!this.localDraftStatus) {
                return;
            }
 
             this.localDraftStatus.removeClass(
                 'is-saved',
                 'is-saved',
                 'is-warning',
                 'is-warning',
Line 237: Line 153:


             if (type) {
             if (type) {
                 this.localDraftStatus.addClass(
                 this.localDraftStatus.addClass('is-' + type);
                    'is-' + type
                );
             }
             }


             this.localDraftStatus.text(
             this.localDraftStatus.text(message || '');
                message || ''
            );
         },
         },


Line 253: Line 165:


             try {
             try {
                 return new Date(
                 return new Date(timestamp).toLocaleTimeString([], {
                    timestamp
                     hour: '2-digit',
                ).toLocaleTimeString(
                    minute: '2-digit',
                    [],
                    second: '2-digit'
                     {
                });
                        hour: '2-digit',
                        minute: '2-digit',
                        second: '2-digit'
                    }
                );
             } catch (error) {
             } catch (error) {
                 return '';
                 return '';
Line 273: Line 180:
             }
             }


             if (
             if (this.state.localDraftSavedAt) {
                this.state.localDraftSavedAt
                 const time = this.formatLocalDraftTime(this.state.localDraftSavedAt);
            ) {
                 const time =
                    this.formatLocalDraftTime(
                        this.state.localDraftSavedAt
                    );


                 this.updateLocalDraftStatus(
                 this.updateLocalDraftStatus(
                     time
                     time ? 'Saved locally at ' + time : 'Saved locally',
                        ? 'Saved locally at ' + time
                        : 'Saved locally',
                     'saved'
                     'saved'
                 );
                 );
Line 291: Line 191:
             }
             }


             this.updateLocalDraftStatus(
             this.updateLocalDraftStatus('No local draft', 'empty');
                'No local draft',
                'empty'
            );
         },
         },


Line 302: Line 199:
             }
             }


             const snapshot =
             const snapshot = this.getDraftSnapshot();
                this.getDraftSnapshot();
             const id = this.getDraftId(snapshot.preloadTitle);
 
             const storage = this.getDraftStorage();
             const id =
                this.getDraftId(
                    snapshot.preloadTitle
                );
 
             const storage =
                this.getDraftStorage();
 
            storage.drafts[id] =
                snapshot;


            storage.drafts[id] = snapshot;
             storage.active = id;
             storage.active = id;


             const saved =
             const saved = this.setDraftStorage(storage);
                this.setDraftStorage(
                    storage
                );


             if (saved) {
             if (saved) {
                 this.state.localDraftSavedAt =
                 this.state.localDraftSavedAt = snapshot.savedAt;
                    snapshot.savedAt;
                 this.state.localDraftAvailable = true;
 
                 this.state.localDraftAvailable =
                    true;
 
                 this.updateLocalDraftIndicator();
                 this.updateLocalDraftIndicator();
             } else {
             } else {
                 this.state.localDraftAvailable =
                 this.state.localDraftAvailable = false;
                    false;
                 this.updateLocalDraftStatus('Could not save local draft', 'error');
 
                 this.updateLocalDraftStatus(
                    'Could not save local draft',
                    'error'
                );
             }
             }


Line 345: Line 221:


         clearDraft(title) {
         clearDraft(title) {
             const storage =
             const storage = this.getDraftStorage();
                this.getDraftStorage();
            const id = this.getDraftId(title);


            const id =
             if (storage.drafts && storage.drafts[id]) {
                this.getDraftId(title);
 
             if (
                storage.drafts &&
                storage.drafts[id]
            ) {
                 delete storage.drafts[id];
                 delete storage.drafts[id];
             }
             }


             if (
             if (storage.active === id) {
                storage.active === id
            ) {
                 storage.active = null;
                 storage.active = null;
             }
             }


             this.setDraftStorage(
             this.setDraftStorage(storage);
                storage
            );


             this.state.localDraftSavedAt =
             this.state.localDraftSavedAt = null;
                null;
             this.state.localDraftAvailable = false;
 
             this.state.localDraftAvailable =
                false;


             this.updateLocalDraftIndicator();
             this.updateLocalDraftIndicator();
Line 378: Line 241:


         getMostRecentDraft() {
         getMostRecentDraft() {
             const storage =
             const storage = this.getDraftStorage();
                this.getDraftStorage();


             if (
             if (!storage.drafts || !Object.keys(storage.drafts).length) {
                !storage.drafts ||
                !Object.keys(
                    storage.drafts
                ).length
            ) {
                 return null;
                 return null;
             }
             }


             if (
             if (storage.active && storage.drafts[storage.active]) {
                storage.active &&
                storage.drafts[
                    storage.active
                ]
            ) {
                 return {
                 return {
                     id: storage.active,
                     id: storage.active,
                     draft:
                     draft: storage.drafts[storage.active]
                        storage.drafts[
                            storage.active
                        ]
                 };
                 };
             }
             }
Line 408: Line 257:
             let latestDraft = null;
             let latestDraft = null;


             Object.entries(
             Object.entries(storage.drafts).forEach(([id, draft]) => {
                storage.drafts
                if (!draft || typeof draft !== 'object') {
            ).forEach(
                    return;
                ([id, draft]) => {
                }
                    if (
                        !draft ||
                        typeof draft !== 'object'
                    ) {
                        return;
                    }


                    if (
                if (
                        !latestDraft ||
                    !latestDraft ||
                        (
                    (Number(draft.savedAt) || 0) > (Number(latestDraft.savedAt) || 0)
                            Number(
                ) {
                                draft.savedAt
                    latestId = id;
                            ) || 0
                    latestDraft = draft;
                        ) >
                        (
                            Number(
                                latestDraft.savedAt
                            ) || 0
                        )
                    ) {
                        latestId = id;
                        latestDraft = draft;
                    }
                 }
                 }
             );
             });


             if (!latestDraft) {
             if (!latestDraft) {
Line 449: Line 282:


         async restoreDraft() {
         async restoreDraft() {
             const result =
             const result = this.getMostRecentDraft();
                this.getMostRecentDraft();


             if (
             if (!result || !result.draft) {
                !result ||
                !result.draft
            ) {
                 return false;
                 return false;
             }
             }


             const draft =
             const draft = result.draft;
                result.draft;
             const title = String(draft.preloadTitle || '').trim();
 
             const displayTitle = title || 'an untitled preload';
             const title =
                String(
                    draft.preloadTitle || ''
                ).trim();
 
             const displayTitle =
                title ||
                'an untitled preload';


             const savedAt =
             const savedAt = draft.savedAt
                draft.savedAt
                ? new Date(draft.savedAt).toLocaleString()
                    ? new Date(
                : '';
                        draft.savedAt
                    ).toLocaleString()
                    : '';


             const message =
             const message =
                 'Preload Editor found unsaved local work for ' +
                 'Preload Editor found unsaved local work for ' +
                 displayTitle +
                 displayTitle +
                 (
                 (savedAt ? '\n\nLast saved locally: ' + savedAt : '') +
                    savedAt
                        ? '\n\nLast saved locally: ' +
                          savedAt
                        : ''
                ) +
                 '\n\nRestore it?';
                 '\n\nRestore it?';


             const restore =
             const restore = await this.createConfirmDialog(
                await this.createConfirmDialog(
                'Restore unsaved work',
                    'Restore unsaved work',
                message,
                    message,
                'Restore',
                    'Restore',
                'Discard'
                    'Discard'
            );
                );


             if (!restore) {
             if (!restore) {
Line 502: Line 314:
             }
             }


             let config =
             let config = this.emptyConfig();
                this.emptyConfig();


             if (
             if (
Line 511: Line 322:
             ) {
             ) {
                 try {
                 try {
                     config =
                     config = this.normalizeConfig(
                        this.normalizeConfig(
                        JSON.parse(JSON.stringify(draft.config))
                            JSON.parse(
                    );
                                JSON.stringify(
                                    draft.config
                                )
                            )
                        );
                 } catch (error) {
                 } catch (error) {
                     console.warn(
                     console.warn(
Line 532: Line 338:


             this.state = {
             this.state = {
                 preloadTitle:
                 preloadTitle: title,
                    title,
                preloadSource: String(draft.preloadSource || ''),
                preloadContent: String(draft.preloadContent || ''),
                config,


                 preloadSource:
                 variables: Array.isArray(draft.variables)
                     String(
                     ? draft.variables.slice()
                        draft.preloadSource || ''
                     : [],
                     ),


                preloadContent:
                 conditions: Array.isArray(draft.conditions)
                    String(
                     ? draft.conditions.slice()
                        draft.preloadContent || ''
                    : [],
                    ),
 
                config,
 
                variables:
                    Array.isArray(
                        draft.variables
                    )
                        ? draft.variables.slice()
                        : [],
 
                 conditions:
                    Array.isArray(
                        draft.conditions
                     )
                        ? draft.conditions.slice()
                        : [],
 
                selectedVariable:
                    String(
                        draft.selectedVariable ||
                        'PAGE'
                    ),


                 deletedVariables:
                 selectedVariable: String(draft.selectedVariable || 'PAGE'),
                    Array.isArray(
                        draft.deletedVariables
                    )
                        ? draft.deletedVariables
                            .map(
                                name =>
                                    String(
                                        name
                                    ).toUpperCase()
                            )
                            .filter(
                                name =>
                                    name !==
                                    'PAGE'
                            )
                        : [],


                 mode:
                 deletedVariables: Array.isArray(draft.deletedVariables)
                     draft.mode ||
                     ? draft.deletedVariables
                    'edit',
                        .map(name => String(name).toUpperCase())
                        .filter(name => name !== 'PAGE')
                    : [],


                 dirty:
                 mode: draft.mode || 'edit',
                    hasDirtyFlags
                        ? !!(
                            draft.dirtyPreload ||
                            draft.dirtyConfig
                        )
                        : true,


                 dirtyPreload:
                 dirty: hasDirtyFlags
                     hasDirtyFlags
                     ? !!(draft.dirtyPreload || draft.dirtyConfig)
                        ? !!draft.dirtyPreload
                    : true,
                        : true,


                 dirtyConfig:
                 dirtyPreload: hasDirtyFlags
                     hasDirtyFlags
                     ? !!draft.dirtyPreload
                        ? !!draft.dirtyConfig
                    : true,
                        : true,


                 localDraftSavedAt:
                 dirtyConfig: hasDirtyFlags
                     Number(
                     ? !!draft.dirtyConfig
                        draft.savedAt
                     : true,
                     ) || null,


                 localDraftAvailable:
                localDraftSavedAt: Number(draft.savedAt) || null,
                    true
                 localDraftAvailable: true
             };
             };


             this.reanalyze();
             this.reanalyze();
             this.renderEditor();
             this.renderEditor();
             this.updateLocalDraftIndicator();
             this.updateLocalDraftIndicator();


Line 626: Line 386:
             );
             );


             mw.notify(
             mw.notify('Unsaved Preload Editor work restored.', {
                'Unsaved Preload Editor work restored.',
                 type: 'success'
                 {
            });
                    type: 'success'
                }
            );


             return true;
             return true;
Line 641: Line 398:


         async getPage(title) {
         async getPage(title) {
             const response =
             const response = await new mw.Api().get({
                await new mw.Api().get({
                action: 'query',
                    action: 'query',
                prop: 'info|revisions',
                    prop: 'info|revisions',
                titles: title,
                    titles: title,
                rvprop: 'content',
                    rvprop: 'content',
                rvslots: 'main',
                    rvslots: 'main',
                formatversion: 2
                    formatversion: 2
            });
                });


             const page =
             const page =
Line 669: Line 425:


             return {
             return {
                 title:
                 title: page.title || title,
                    page.title ||
                 content: typeof content === 'string' ? content : '',
                    title,
                 contentmodel: page.contentmodel || 'wikitext'
 
                 content:
                    typeof content === 'string'
                        ? content
                        : '',
 
                 contentmodel:
                    page.contentmodel ||
                    'wikitext'
             };
             };
         },
         },
Line 689: Line 436:


         async searchPages(query) {
         async searchPages(query) {
             const response =
             const response = await new mw.Api().get({
                await new mw.Api().get({
                action: 'query',
                    action: 'query',
                list: 'search',
                    list: 'search',
                srsearch: query,
                    srsearch: query,
                srlimit: 10,
                    srlimit: 10,
                srnamespace: 10,
                    srnamespace: 10,
                formatversion: 2
                    formatversion: 2
            });
                });


             return (
             return (
Line 714: Line 460:
         * changing, use changeContentModel() first.
         * changing, use changeContentModel() first.
         */
         */
         async savePage(
         async savePage(title, content, summary, createOnly, contentModel) {
            title,
            content,
            summary,
            createOnly,
            contentModel
        ) {
             const params = {
             const params = {
                 action: 'edit',
                 action: 'edit',
                 title,
                 title,
                 text: content,
                 text: content,
                 summary:
                 summary: summary || 'Updated with Preload Editor',
                    summary ||
                    'Updated with Preload Editor',
                 formatversion: 2
                 formatversion: 2
             };
             };
Line 736: Line 474:


             if (contentModel) {
             if (contentModel) {
                 params.contentmodel =
                 params.contentmodel = contentModel;
                    contentModel;
             }
             }


             return new mw.Api().postWithToken(
             return new mw.Api().postWithToken('csrf', params);
                'csrf',
                params
            );
         },
         },


Line 754: Line 488:
         * The page must already exist.
         * The page must already exist.
         */
         */
         async changeContentModel(
         async changeContentModel(title, model, summary) {
            title,
             return new mw.Api().postWithToken('csrf', {
            model,
                 action: 'changecontentmodel',
            summary
                title,
        ) {
                model,
             return new mw.Api().postWithToken(
                reason: summary || 'Changed content model with Preload Editor'
                'csrf',
            });
                 {
                    action:
                        'changecontentmodel',
 
                    title,
 
                    model,
 
                    reason:
                        summary ||
                        'Changed content model with Preload Editor'
                }
            );
         },
         },


Line 787: Line 508:
         *  then save the JSON normally.
         *  then save the JSON normally.
         */
         */
         async saveConfigPage(
         async saveConfigPage(title, content, summary) {
            title,
             const page = await this.getPage(title);
            content,
            summary
        ) {
             const page =
                await this.getPage(title);


             if (!page) {
             if (!page) {
Line 805: Line 521:
             }
             }


             if (
             if (String(page.contentmodel || '').toLowerCase() !== 'json') {
                String(
                    page.contentmodel || ''
                ).toLowerCase() !== 'json'
            ) {
                 await this.changeContentModel(
                 await this.changeContentModel(
                     title,
                     title,
Line 826: Line 538:


         // =========================================================
         // =========================================================
         // Preload parsing
         // Config import helpers
         // =========================================================
         // =========================================================


         extractIncludeonly(source) {
         /*
             const match =
        * Accept either:
                String(source || '').match(
        *
                    /<includeonly>([\s\S]*?)<\/includeonly>/i
        *  Template:Example/Preload
                 );
        *
        * or:
        *
        *  Template:Example/Preload/config.json
        *
        * and always return the config.json title.
        */
        normalizeConfigTitle(title) {
             title = String(title || '').trim();
            title = title.replace(/\/+$/, '');
 
            if (/\/config\.json$/i.test(title)) {
                 return title;
            }


             return match
             return title + '/config.json';
                ? match[1].trim()
                : String(source || '').trim();
         },
         },


         findVariables(content) {
         /*
             const variables = [];
        * Return a safe clone of a config field.
             const pattern =
        *
                 /%%([A-Za-z0-9_-]+)%%/g;
        * This is deliberately JSON-based because config
        * definitions are JSON-compatible objects.
        */
        cloneConfigField(field) {
             try {
                return JSON.parse(JSON.stringify(field));
             } catch (error) {
                 console.warn(
                    'Preload Editor: Could not clone imported field.',
                    error
                );


             let match;
                return null;
             }
        },


             while (
        /*
                 (match = pattern.exec(content)) !== null
        * Normalize imported field names to the same uppercase
        * convention used throughout the editor.
        */
        getImportableFields(sourceConfig) {
            const fields = sourceConfig && sourceConfig.fields;
 
             if (
                 !fields ||
                typeof fields !== 'object' ||
                Array.isArray(fields)
             ) {
             ) {
                 const name =
                 return {};
                    match[1].toUpperCase();
            }


                if (!variables.includes(name)) {
            const result = {};
                    variables.push(name);
                }
            }


             return variables;
             Object.entries(fields).forEach(([name, field]) => {
        },
                const upper = String(name || '').trim().toUpperCase();


        findConditions(content) {
                if (!upper || upper === 'PAGE') {
            const conditions = [];
                    return;
            const pattern =
                 }
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;


            let match;
                if (
                    !field ||
                    typeof field !== 'object' ||
                    Array.isArray(field)
                ) {
                    return;
                }


            while (
                 const clone = this.cloneConfigField(field);
                 (match = pattern.exec(content)) !== null
            ) {
                conditions.push({
                    type:
                        match[1].toLowerCase(),


                     expression:
                if (!clone) {
                        match[2].trim(),
                     return;
                }


                    index:
                 result[upper] = clone;
                        match.index
             });
                 });
             }


             return conditions;
             return result;
         },
         },


         findConditionVariables(content) {
         /*
             const variables = [];
        * Show the variable-selection dialog after a source
        * config has been loaded.
        *
        * Resolves:
        *  null -> cancelled
        *  []  -> confirmed with no selections
        *  [...] -> selected field names
        */
        createImportDefinitionsDialog(sourceTitle, fields) {
             return new Promise(resolve => {
                const names = Object.keys(fields).sort((a, b) => a.localeCompare(b));


            this.findConditions(content).forEach(
                 const overlay = $('<div>')
                 condition => {
                     .addClass(
                     if (
                         'preload-editor-prompt-overlay preload-editor-import-overlay'
                         condition.type !== 'if' &&
                     );
                        condition.type !== 'elseif'
                     ) {
                        return;
                    }


                    const matches =
                const box = $('<div>')
                        condition.expression.match(
                    .addClass(
                            /[A-Za-z0-9_-]+/g
                        'preload-editor-prompt preload-editor-import-dialog'
                        ) || [];
                    );


                     matches.forEach(
                $('<h2>')
                        name => {
                     .text('Import variable definitions')
                            name =
                    .appendTo(box);
                                name.toUpperCase();


                            if (
                $('<p>')
                                !variables.includes(
                    .text(
                                    name
                        'Choose which variable definitions to import from "' +
                                )
                        sourceTitle +
                             ) {
                        '". Existing local definitions can be replaced.'
                                variables.push(
                    )
                                    name
                    .appendTo(box);
                                );
 
                             }
                if (!names.length) {
                    $('<div>')
                        .addClass(
                            'preload-editor-empty preload-editor-import-empty'
                        )
                        .text(
                             'This config.json does not contain any importable variable definitions.'
                        )
                        .appendTo(box);
 
                    const emptyButtons = $('<div>')
                        .addClass('preload-editor-prompt-buttons');
 
                    const close = this.createButton(
                        'Close',
                        'preload-editor-button-primary',
                        () => {
                            overlay.remove();
                             resolve(null);
                         }
                         }
                     );
                     );
                    emptyButtons.append(close);
                    box.append(emptyButtons);
                    overlay.append(box);
                    $(document.body).append(overlay);
                    overlay.attr('tabindex', '-1');
                    overlay.trigger('focus');
                    return;
                 }
                 }
            );


            return variables;
                const search = this.textInput('', 'Search variables...')
        },
                    .addClass('preload-editor-import-search');


        analyzePreload(content) {
                const searchWrap = $('<div>')
            return {
                    .addClass('preload-editor-import-search-wrap')
                variables:
                     .append(search);
                     this.findVariables(content),


                 conditions:
                 box.append(searchWrap);
                    this.findConditions(content),


                 conditionVariables:
                 const controls = $('<div>')
                     this.findConditionVariables(
                     .addClass('preload-editor-import-controls');
                        content
                    )
            };
        },


        // =========================================================
                const selectAll = this.createButton(
        // Config
                    'Select all',
        // =========================================================
                    '',
                    () => {
                        list
                            .find('input[type="checkbox"]:visible')
                            .prop('checked', true);


        getAutomaticPageField() {
                        updateSummary();
            return {
                    }
                 label: 'Page Name',
                 );
                 description: 'Name of the page.',
 
                type: 'page',
                 const deselectAll = this.createButton(
                required: true,
                    'Deselect all',
                namespace: 'Main',
                    '',
                'allow-new': true
                    () => {
            };
                        list
        },
                            .find('input[type="checkbox"]:visible')
                            .prop('checked', false);


        emptyConfig() {
                        updateSummary();
            return {
                     }
                settings: {
                 );
                     incomplete: 'add-stub'
                 },


                 fields: {
                 controls.append(selectAll, deselectAll);
                    PAGE:
                 box.append(controls);
                        this.getAutomaticPageField()
                 }
            };
        },


        normalizeConfig(config) {
                 const list = $('<div>')
            if (
                     .addClass('preload-editor-import-list');
                 !config ||
                typeof config !== 'object' ||
                Array.isArray(config)
            ) {
                config =
                     this.emptyConfig();
            }


            if (
                const localFields =
                !config.settings ||
                    this.state.config && this.state.config.fields
                typeof config.settings !== 'object' ||
                        ? this.state.config.fields
                Array.isArray(config.settings)
                        : {};
            ) {
                config.settings = {};
            }


            if (
                 names.forEach(name => {
                 !config.fields ||
                    const existing = !!localFields[name];
                typeof config.fields !== 'object' ||
                    const field = fields[name];
                Array.isArray(config.fields)
            ) {
                config.fields = {};
            }


            config.fields.PAGE =
                    const type = field && field.type
                this.getAutomaticPageField();
                        ? String(field.type)
                        : 'text';


            return config;
                    const label = field && field.label
        },
                        ? String(field.label)
                        : this.humanizeName(name);


        createDefaultField(
                    const row = $('<label>')
            name,
                        .addClass('preload-editor-import-row')
            analysis
                        .attr('data-variable', name)
        ) {
                        .attr(
            return {
                            'data-search',
                label:
                            (name + ' ' + label + ' ' + type).toLowerCase()
                    this.humanizeName(name),
                        );


                description: '',
                    const checkbox = this.checkbox(false)
                        .attr('data-import-variable', name)
                        .addClass('preload-editor-import-checkbox');


                type:
                     const content = $('<span>')
                     analysis.conditionVariables.includes(
                         .addClass('preload-editor-import-content');
                        name
                    )
                        ? 'boolean'
                         : 'text'
            };
        },


        humanizeName(name) {
                    const header = $('<span>')
            return String(name || '')
                        .addClass('preload-editor-import-header');
                .replace(
                    /[_-]+/g,
                    ' '
                )
                .replace(
                    /([a-z])([A-Z])/g,
                    '$1 $2'
                )
                .replace(
                    /\s+/g,
                    ' '
                )
                .trim()
                .replace(
                    /\b\w/g,
                    char =>
                        char.toUpperCase()
                );
        },


        prepareConfigForVariables(
                    $('<strong>')
            config,
                        .text(name)
            analysis
                        .appendTo(header);
        ) {
            config =
                this.normalizeConfig(config);


            const existing =
                    $('<span>')
                config.fields || {};
                        .addClass('preload-editor-import-type')
                        .text(type)
                        .appendTo(header);


            const deleted =
                    const meta = $('<span>')
                new Set(
                         .addClass('preload-editor-import-meta');
                    Array.isArray(
                        this.state.deletedVariables
                    )
                         ? this.state.deletedVariables
                            .map(
                                name =>
                                    String(
                                        name
                                    ).toUpperCase()
                            )
                        : []
                );


            const fields = {};
                    $('<span>')
                        .text(label)
                        .appendTo(meta);


            fields.PAGE =
                    const status = $('<span>')
                this.getAutomaticPageField();
                        .addClass('preload-editor-import-status')
                        .toggleClass('is-existing', existing)
                        .toggleClass('is-new', !existing)
                        .text(existing ? 'REPLACE EXISTING' : 'NEW');


            analysis.variables.forEach(
                    content.append(header, meta, status);
                name => {
                     row.append(checkbox, content);
                     if (
                     list.append(row);
                        name === 'PAGE' ||
                });
                        deleted.has(name)
                     ) {
                        return;
                    }


                     fields[name] =
                box.append(list);
                         existing[name] ||
 
                         this.createDefaultField(
                const summary = $('<div>')
                             name,
                    .addClass('preload-editor-import-summary');
                             analysis
 
                        );
                const updateSummary = () => {
                }
                     const selected = list.find(
            );
                        'input[type="checkbox"]:checked'
                    ).length;
 
                    const existingSelected = list
                         .find('input[type="checkbox"]:checked')
                         .filter(function () {
                             const name = $(this).attr(
                                'data-import-variable'
                             );
 
                            return !!localFields[name];
                        }).length;


            Object.keys(existing).forEach(
                    let text =
                name => {
                        selected +
                    const upper =
                        ' ' +
                         name.toUpperCase();
                        (selected === 1 ? 'variable' : 'variables') +
                         ' selected.';


                     if (
                     if (existingSelected) {
                         upper !== 'PAGE' &&
                         text +=
                        !fields[upper] &&
                            ' ' +
                        !deleted.has(upper)
                            existingSelected +
                    ) {
                            ' existing ' +
                        fields[upper] =
                            (existingSelected === 1
                             existing[name];
                                ? 'definition'
                                : 'definitions') +
                             ' will be replaced.';
                     }
                     }
                }
            );


            config.fields = fields;
                    summary.text(text);
 
                    importButton.prop('disabled', selected === 0);
                };


            return config;
                const filterRows = () => {
        },
                    const query = String(search.val() || '')
                        .trim()
                        .toLowerCase();


        validateConfig(
                    list.find('.preload-editor-import-row').each(function () {
            config,
                        const row = $(this);
            analysis
                        const haystack = String(
        ) {
                            row.attr('data-search') || ''
            const errors = [];
                        );
            const warnings = [];


            if (
                        row.toggle(!query || haystack.includes(query));
                !config ||
                     });
                typeof config !== 'object' ||
                Array.isArray(config)
            ) {
                return {
                     valid: false,
                    errors: [
                        'Config must be an object.'
                    ],
                    warnings: []
                 };
                 };
            }


            if (
                 search.on(
                 !config.fields ||
                    'input.preloadEditor',
                typeof config.fields !== 'object' ||
                    filterRows
                 Array.isArray(config.fields)
                 );
            ) {
 
                 return {
                 list.on(
                     valid: false,
                     'change.preloadEditor',
                     errors: [
                     'input[type="checkbox"]',
                        '"fields" must be an object.'
                     updateSummary
                    ],
                 );
                     warnings
 
                 };
                const buttons = $('<div>')
            }
                    .addClass('preload-editor-prompt-buttons');


            const configured =
                const cancel = this.createButton(
                Object.keys(
                     'Cancel',
                     config.fields
                    '',
                ).map(
                     () => {
                     name =>
                         overlay.remove();
                         name.toUpperCase()
                        resolve(null);
                    }
                 );
                 );


            analysis.variables.forEach(
                const importButton = this.createButton(
                name => {
                    'Import selected',
                    if (
                    'preload-editor-button-primary',
                         !configured.includes(name)
                    () => {
                    ) {
                        const selected = [];
                        errors.push(
 
                            'Variable "' +
                         list
                             name +
                            .find('input[type="checkbox"]:checked')
                            '" is not configured.'
                            .each(function () {
                         );
                                selected.push(
                                    String(
                                        $(this).attr(
                                            'data-import-variable'
                                        )
                                    )
                                );
                             });
 
                        overlay.remove();
                         resolve(selected);
                     }
                     }
                 }
                 );
            );
 
                buttons.append(cancel, importButton);
                box.append(summary, buttons);
                overlay.append(box);
 
                $(document.body).append(overlay);


            configured.forEach(
                 overlay.attr('tabindex', '-1');
                 name => {
                 overlay.trigger('focus');
                    if (
                        !analysis.variables.includes(
                            name
                        )
                    ) {
                        warnings.push(
                            'Field "' +
                            name +
                            '" is not used by the preload.'
                        );
                    }
                 }
            );


            if (
                 updateSummary();
                 !configured.includes('PAGE')
                 search.trigger('focus');
            ) {
                 errors.push(
                    'PAGE must always be configured.'
                );
            }


            Object.entries(
                 const close = event => {
                 config.fields
                     if (event.key === 'Escape') {
            ).forEach(
                         cancel.trigger('click');
                ([name, field]) => {
                    }
                     if (
                };
                        !field ||
                        typeof field !== 'object' ||
                        Array.isArray(field)
                    ) {
                         errors.push(
                            'Field "' +
                            name +
                            '" must be an object.'
                        );


                        return;
                overlay.on(
                     }
                    'keydown.preloadEditor',
                     close
                );


                     if (!field.type) {
                overlay.on(
                        warnings.push(
                    'click.preloadEditor',
                            'Field "' +
                     event => {
                            name +
                        if (event.target === overlay.get(0)) {
                            '" has no type.'
                            cancel.trigger('click');
                         );
                         }
                     }
                     }
                );
            });
        },


                    if (
        /*
                        (
        * Main import workflow.
                            field.type === 'select' ||
        */
                            field.type === 'multiselect'
        async importVariableDefinitions() {
                        ) &&
            const source = await this.createPromptDialog(
                        !field.source &&
                'Import variable definitions',
                        !Array.isArray(field.options)
                'Enter a preload title or its config.json title.'
                    ) {
                        warnings.push(
                            'Field "' +
                            name +
                            '" is a ' +
                            field.type +
                            ' without a source or manual options.'
                        );
                    }
                }
             );
             );


             return {
             if (!source) {
                 valid:
                 return;
                    !errors.length,
             }
                errors,
                warnings
             };
        },


        // =========================================================
            const configTitle = this.normalizeConfigTitle(source);
        // Transformation helpers
        // =========================================================


        transformationObject(
            this.setStatus(
            transform,
                'Loading source config...',
            prefix,
                'loading'
             suffix,
             );
            from,
            to
        ) {
            if (!transform) {
                return null;
            }


             switch (transform) {
             try {
                 case 'remove-prefix':
                 const page = await this.getPage(configTitle);
                    return prefix
                        ? {
                            transform,
                            prefix
                        }
                        : null;


                 case 'remove-suffix':
                 if (!page) {
                     return suffix
                     throw new Error(
                         ? {
                         'No config.json was found at "' +
                            transform,
                        configTitle +
                            suffix
                         '".'
                         }
                    );
                        : null;
                }


                 case 'replace':
                 let sourceConfig;
                    return from === ''
                        ? null
                        : {
                            transform,
                            from,
                            to: to || ''
                        };


                 default:
                 try {
                     return null;
                     sourceConfig = JSON.parse(page.content);
            }
                } catch (error) {
        },
                    throw new Error(
                        'The source config.json contains invalid JSON: ' +
                        error.message
                    );
                }


        // =========================================================
                if (
        // UI helpers
                    !sourceConfig ||
        // =========================================================
                    typeof sourceConfig !== 'object' ||
                    Array.isArray(sourceConfig)
                ) {
                    throw new Error(
                        'The source config.json must contain a JSON object.'
                    );
                }


        createSection(
                if (
            title,
                    !sourceConfig.fields ||
            className
                    typeof sourceConfig.fields !== 'object' ||
        ) {
                    Array.isArray(sourceConfig.fields)
            const section =
                 ) {
                 $('<section>')
                     throw new Error(
                     .addClass(
                         'The source config.json does not contain a valid "fields" object.'
                         'preload-editor-section'
                     );
                     );
                }


            if (className) {
                 const fields = this.getImportableFields(sourceConfig);
                 section.addClass(className);
            }


            $('<h2>')
                const selected =
                .addClass(
                    await this.createImportDefinitionsDialog(
                    'preload-editor-section-title'
                        configTitle,
                )
                        fields
                .text(title)
                    );
                .appendTo(section);


            return section;
                if (selected === null) {
        },
                    this.setStatus(
                        'Variable import cancelled.',
                        'warning'
                    );


        createButton(
                    return;
            label,
                 }
            className,
            callback
        ) {
            const button =
                 $('<button>')
                    .attr(
                        'type',
                        'button'
                    )
                    .addClass(
                        'preload-editor-button'
                    )
                    .text(label);


            if (className) {
                if (!selected.length) {
                button.addClass(className);
                    this.setStatus(
            }
                        'No variable definitions were imported.',
                        'warning'
                    );


            if (callback) {
                     return;
                button.on(
                 }
                     'click.preloadEditor',
                    callback
                 );
            }


            return button;
                if (!this.state.config) {
        },
                    this.state.config = this.emptyConfig();
                }


        fieldRow(
                if (!this.state.config.fields) {
            label,
                    this.state.config.fields = {};
            input,
                 }
            description
        ) {
            const row =
                 $('<div>')
                    .addClass(
                        'preload-editor-field'
                    );


            $('<label>')
                 const imported = [];
                .addClass(
                 const replaced = [];
                    'preload-editor-label'
                )
                 .text(label)
                 .appendTo(row);


            if (description) {
                 selected.forEach(name => {
                 $('<div>')
                     if (name === 'PAGE') {
                     .addClass(
                        return;
                        'preload-editor-help'
                     }
                    )
                    .text(description)
                     .appendTo(row);
            }


            row.append(input);
                    const field = fields[name];


            return row;
                    if (!field) {
        },
                        return;
                    }


        textInput(
                    if (this.state.config.fields[name]) {
            value,
                        replaced.push(name);
            placeholder
                    } else {
        ) {
                        imported.push(name);
            return $('<input>')
                     }
                .attr({
                    type: 'text',
                     value:
                        value == null
                            ? ''
                            : value,


                     placeholder:
                     const clone = this.cloneConfigField(field);
                        placeholder || ''
                })
                .addClass(
                    'preload-editor-input'
                );
        },


        textarea(value) {
                    if (!clone) {
            return $('<textarea>')
                        return;
                .val(
                     }
                     value == null
                        ? ''
                        : value
                )
                .addClass(
                    'preload-editor-textarea'
                );
        },


        checkbox(checked) {
                    this.state.config.fields[name] = clone;
            return $('<input>')
                .attr(
                    'type',
                    'checkbox'
                )
                .prop(
                    'checked',
                    !!checked
                )
                .addClass(
                    'preload-editor-checkbox'
                );
        },


        select(
                    /*
            options,
                    * If this variable was previously deleted,
            value
                    * importing its definition explicitly should
        ) {
                    * restore it.
            const select =
                    */
                $('<select>')
                    if (Array.isArray(this.state.deletedVariables)) {
                    .addClass(
                        this.state.deletedVariables =
                        'preload-editor-select'
                            this.state.deletedVariables.filter(
                     );
                                deletedName =>
                                    String(deletedName).toUpperCase() !== name
                            );
                     }
                });


            options.forEach(
                 /*
                 option => {
                * Keep PAGE automatic regardless of what the
                    $('<option>')
                * imported config contained.
                        .attr(
                */
                            'value',
                this.state.config.fields.PAGE =
                            option.value
                    this.getAutomaticPageField();
                        )
                        .text(
                            option.label
                        )
                        .prop(
                            'selected',
                            String(
                                option.value
                            ) ===
                            String(
                                value === undefined
                                    ? ''
                                    : value
                            )
                        )
                        .appendTo(select);
                }
            );


            return select;
                this.markConfigDirty();
        },


        // =========================================================
                /*
        // In-wiki dialogs
                * Keep the current selection when possible.
        // =========================================================
                * Otherwise select the first imported definition.
                */
                const selectedStillExists = !!(
                    this.state.config.fields[
                        this.state.selectedVariable
                    ]
                );


        createConfirmDialog(
                if (
            title,
                    !selectedStillExists ||
            message,
                    this.state.selectedVariable === 'PAGE'
            confirmLabel,
                ) {
            cancelLabel
                     this.state.selectedVariable =
        ) {
                         selected[0] ||
            return new Promise(
                        'PAGE';
                resolve => {
                }
                     const overlay =
                         $('<div>')
                            .addClass(
                                'preload-editor-prompt-overlay'
                            );


                    const box =
                this.reanalyze();
                        $('<div>')
                            .addClass(
                                'preload-editor-prompt'
                            );


                    $('<h2>')
                this.renderVariablesBySelector();
                        .text(title)
                this.renderVariableConfigurationBySelector();
                        .appendTo(box);
                this.renderAnalysisBySelector();
                this.updateEditorTools();


                    const messageLines =
                this.saveDraft();
                        String(message || '')
                            .split('\n');


                     messageLines.forEach(
                let status =
                        line => {
                    'Imported ' +
                            if (line === '') {
                     selected.length +
                                $('<div>')
                    ' variable ' +
                                    .addClass(
                    (selected.length === 1
                                        'preload-editor-prompt-spacer'
                        ? 'definition'
                                    )
                        : 'definitions') +
                                    .appendTo(box);
                    '.';


                                return;
                if (replaced.length) {
                            }
                    status +=
                        ' Replaced: ' +
                        replaced.join(', ') +
                        '.';
                }


                            $('<p>')
                this.setStatus(status, 'success');
                                .text(line)
                                .appendTo(box);
                        }
                    );


                     const buttons =
                mw.notify(status, {
                        $('<div>')
                     type: 'success'
                            .addClass(
                });
                                'preload-editor-prompt-buttons'
            } catch (error) {
                            );
                console.error(
                    'Preload Editor variable import:',
                    error
                );


                    const cancel =
                this.setStatus(
                        this.createButton(
                    error.message ||
                            cancelLabel || 'Cancel',
                        'Could not import variable definitions.',
                            '',
                    'error'
                            () => {
                );
                                overlay.remove();
                                resolve(false);
                            }
                        );


                    const confirm =
                mw.notify(
                        this.createButton(
                    error.message ||
                            confirmLabel || 'Confirm',
                        'Could not import variable definitions.',
                            'preload-editor-button-primary',
                    {
                            () => {
                        type: 'error'
                                overlay.remove();
                    }
                                resolve(true);
                );
                            }
            }
                        );
        },


                    buttons.append(
        // =========================================================
                        cancel,
        // Preload parsing
                        confirm
        // =========================================================
                    );


                    box.append(buttons);
        extractIncludeonly(source) {
                    overlay.append(box);
            const match = String(source || '').match(
                /<includeonly>([\s\S]*?)<\/includeonly>/i
            );


                    $(document.body)
            return match
                        .append(overlay);
                ? match[1].trim()
                : String(source || '').trim();
        },


                    const close =
        findVariables(content) {
                        value => {
            const variables = [];
                            if (value === 'Escape') {
            const pattern = /%%([A-Za-z0-9_-]+)%%/g;
                                cancel.trigger('click');
                            }
                        };


                    overlay.on(
            let match;
                        'keydown.preloadEditor',
                        event => {
                            close(event.key);
                        }
                    );


                    overlay.attr(
            while ((match = pattern.exec(content)) !== null) {
                        'tabindex',
                const name = match[1].toUpperCase();
                        '-1'
                    );


                     overlay.trigger('focus');
                if (!variables.includes(name)) {
                     variables.push(name);
                }
            }


                    overlay.on(
             return variables;
                        'click.preloadEditor',
                        event => {
                            if (
                                event.target ===
                                overlay.get(0)
                            ) {
                                cancel.trigger('click');
                            }
                        }
                    );
                }
             );
         },
         },


         createPromptDialog(
         findConditions(content) {
            title,
             const conditions = [];
            message
            const pattern =
        ) {
                /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
             return new Promise(
                resolve => {
                    const overlay =
                        $('<div>')
                            .addClass(
                                'preload-editor-prompt-overlay'
                            );


                    const box =
            let match;
                        $('<div>')
                            .addClass(
                                'preload-editor-prompt'
                            );


                     $('<h2>')
            while ((match = pattern.exec(content)) !== null) {
                        .text(title)
                conditions.push({
                        .appendTo(box);
                     type: match[1].toLowerCase(),
                    expression: match[2].trim(),
                    index: match.index
                });
            }


                    $('<p>')
            return conditions;
                        .text(message)
        },
                        .appendTo(box);


                    const input =
        findConditionVariables(content) {
                        this.textInput();
            const variables = [];


                    const buttons =
            this.findConditions(content).forEach(condition => {
                        $('<div>')
                if (
                            .addClass(
                    condition.type !== 'if' &&
                                'preload-editor-prompt-buttons'
                    condition.type !== 'elseif'
                            );
                ) {
                    return;
                }


                    const cancel =
                const matches =
                        this.createButton(
                    condition.expression.match(
                            'Cancel',
                        /[A-Za-z0-9_-]+/g
                            '',
                    ) || [];
                            () => {
                                overlay.remove();
                                resolve(null);
                            }
                        );


                    const confirm =
                matches.forEach(name => {
                        this.createButton(
                    name = name.toUpperCase();
                            'Load',
                            'preload-editor-button-primary',
                            () => {
                                const value =
                                    String(
                                        input.val() || ''
                                    ).trim();


                                if (!value) {
                    if (!variables.includes(name)) {
                                    return;
                        variables.push(name);
                                }
                    }
                });
            });


                                overlay.remove();
            return variables;
                                resolve(value);
        },
                            }
                        );


                    buttons.append(
        analyzePreload(content) {
                        cancel,
            return {
                        confirm
                variables: this.findVariables(content),
                     );
                conditions: this.findConditions(content),
                conditionVariables:
                     this.findConditionVariables(content)
            };
        },


                    box.append(
        // =========================================================
                        input,
        // Config
                        buttons
        // =========================================================
                    );


                    overlay.append(box);
        getAutomaticPageField() {
            return {
                label: 'Page Name',
                description: 'Name of the page.',
                type: 'page',
                required: true,
                namespace: 'Main',
                'allow-new': true
            };
        },


                    $(document.body)
        emptyConfig() {
                        .append(overlay);
            return {
                settings: {
                    incomplete: 'add-stub'
                },


                    input.trigger('focus');
                fields: {
 
                     PAGE: this.getAutomaticPageField()
                     input.on(
                        'keydown.preloadEditor',
                        event => {
                            if (
                                event.key === 'Enter'
                            ) {
                                confirm.trigger('click');
                            }
 
                            if (
                                event.key === 'Escape'
                            ) {
                                cancel.trigger('click');
                            }
                        }
                    );
                 }
                 }
             );
             };
         },
         },


         createConfigReviewDialog() {
         normalizeConfig(config) {
             return new Promise(
             if (
                 resolve => {
                 !config ||
                    const overlay =
                typeof config !== 'object' ||
                        $('<div>')
                Array.isArray(config)
                            .addClass(
            ) {
                                'preload-editor-prompt-overlay preload-editor-code-review-overlay'
                config = this.emptyConfig();
                            );
            }


                    const box =
            if (
                        $('<div>')
                !config.settings ||
                            .addClass(
                typeof config.settings !== 'object' ||
                                'preload-editor-prompt preload-editor-code-review'
                Array.isArray(config.settings)
                            );
            ) {
 
                config.settings = {};
                    $('<h2>')
            }
                        .text('Review config.json')
                        .appendTo(box);


                    $('<p>')
            if (
                        .text(
                !config.fields ||
                            'This is the config.json that will be saved for the current preload.'
                typeof config.fields !== 'object' ||
                        )
                Array.isArray(config.fields)
                        .appendTo(box);
            ) {
                config.fields = {};
            }


                    const code =
            config.fields.PAGE =
                        $('<pre>')
                this.getAutomaticPageField();
                            .addClass(
                                'preload-editor-config-review'
                            )
                            .text(
                                this.getConfigJSON()
                            );


                    const codeWrap =
            return config;
                        $('<div>')
        },
                            .addClass(
                                'preload-editor-config-review-wrap'
                            )
                            .append(code);


                    box.append(codeWrap);
        createDefaultField(name, analysis) {
            return {
                label: this.humanizeName(name),
                description: '',
                type: analysis.conditionVariables.includes(name)
                    ? 'boolean'
                    : 'text'
            };
        },


                    const buttons =
        humanizeName(name) {
                        $('<div>')
            return String(name || '')
                            .addClass(
                .replace(/[_-]+/g, ' ')
                                'preload-editor-prompt-buttons'
                .replace(/([a-z])([A-Z])/g, '$1 $2')
                            );
                .replace(/\s+/g, ' ')
                .trim()
                .replace(/\b\w/g, char => char.toUpperCase());
        },


                    const close =
        prepareConfigForVariables(config, analysis) {
                        this.createButton(
            config = this.normalizeConfig(config);
                            'Close',
                            'preload-editor-button-primary',
                            () => {
                                overlay.remove();
                                resolve();
                            }
                        );


                    buttons.append(close);
            const existing = config.fields || {};
                    box.append(buttons);
                    overlay.append(box);


                    $(document.body)
            const deleted = new Set(
                         .append(overlay);
                Array.isArray(this.state.deletedVariables)
                    ? this.state.deletedVariables.map(
                         name => String(name).toUpperCase()
                    )
                    : []
            );


                    overlay.attr(
            const fields = {};
                        'tabindex',
                        '-1'
                    );


                    overlay.trigger('focus');
            fields.PAGE =
                this.getAutomaticPageField();


                    overlay.on(
            analysis.variables.forEach(name => {
                        'keydown.preloadEditor',
                if (
                        event => {
                    name === 'PAGE' ||
                            if (
                    deleted.has(name)
                                event.key === 'Escape'
                ) {
                            ) {
                    return;
                                close.trigger('click');
                }
                            }
 
                         }
                fields[name] =
                    existing[name] ||
                    this.createDefaultField(
                        name,
                         analysis
                     );
                     );
            });


                    overlay.on(
            Object.keys(existing).forEach(name => {
                        'click.preloadEditor',
                const upper = name.toUpperCase();
                        event => {
 
                            if (
                if (
                                event.target ===
                    upper !== 'PAGE' &&
                                overlay.get(0)
                    !fields[upper] &&
                            ) {
                    !deleted.has(upper)
                                close.trigger('click');
                ) {
                            }
                    fields[upper] =
                         }
                         existing[name];
                    );
                 }
                 }
             );
             });
        },


        async confirmSave(
             config.fields = fields;
             mode,
            title
        ) {
            const configTitle =
                title.replace(
                    /\/+$/,
                    ''
                ) +
                '/config.json';


             let saveDescription;
             return config;
        },


            if (mode === 'preload') {
        validateConfig(config, analysis) {
                saveDescription =
             const errors = [];
                    'Save the preload page only?';
             const warnings = [];
            } else if (mode === 'config') {
                saveDescription =
                    'Save the config.json page only?';
             } else {
                saveDescription =
                    'Save both the preload and config.json?';
             }
 
            let pages = [];


             if (
             if (
                 mode === 'preload' ||
                 !config ||
                 mode === 'both'
                typeof config !== 'object' ||
                 Array.isArray(config)
             ) {
             ) {
                 pages.push(
                 return {
                     'Preload: ' + title
                    valid: false,
                 );
                    errors: [
                        'Config must be an object.'
                    ],
                     warnings: []
                 };
             }
             }


             if (
             if (
                 mode === 'config' ||
                 !config.fields ||
                 mode === 'both'
                 typeof config.fields !== 'object' ||
                Array.isArray(config.fields)
             ) {
             ) {
                 pages.push(
                 return {
                     'Config: ' + configTitle
                    valid: false,
                    errors: [
                        '"fields" must be an object.'
                    ],
                    warnings
                };
            }
 
            const configured =
                Object.keys(
                     config.fields
                ).map(
                    name =>
                        name.toUpperCase()
                 );
                 );


                 pages.push(
            analysis.variables.forEach(name => {
                     'Config content model: JSON'
                 if (!configured.includes(name)) {
                    errors.push(
                        'Variable "' +
                        name +
                        '" is not configured.'
                    );
                }
            });
 
            configured.forEach(name => {
                if (!analysis.variables.includes(name)) {
                     warnings.push(
                        'Field "' +
                        name +
                        '" is not used by the preload.'
                    );
                }
            });
 
            if (!configured.includes('PAGE')) {
                errors.push(
                    'PAGE must always be configured.'
                 );
                 );
             }
             }


             const message =
             Object.entries(config.fields).forEach(([name, field]) => {
                 saveDescription +
                 if (
                '\n\n' +
                    !field ||
                 pages.join('\n') +
                    typeof field !== 'object' ||
                '\n\nThis will write the changes to the wiki.';
                    Array.isArray(field)
                 ) {
                    errors.push(
                        'Field "' +
                        name +
                        '" must be an object.'
                    );


            return this.createConfirmDialog(
                    return;
                'Confirm save',
                 }
                message,
                'Save',
                 'Cancel'
            );
        },


        // =========================================================
                if (!field.type) {
        // CodeMirror
                    warnings.push(
        // =========================================================
                        'Field "' +
                        name +
                        '" has no type.'
                    );
                }
               
                if (
                    field.type === 'file'
                ) {
                    if (
                        field.accept !== undefined &&
                        !Array.isArray(field.accept)
                    ) {
                        errors.push(
                            'File field "' +
                            name +
                            '" must have "accept" as an array.'
                        );
                    }


        destroyEditor() {
                    if (
            if (!this.editor) {
                        Array.isArray(field.accept) &&
                this.editorView = null;
                        field.accept.some(
                this.editorTools = null;
                            value =>
                return;
                                !String(
            }
                                    value || ''
                                ).trim()
                        )
                    ) {
                        errors.push(
                            'File field "' +
                            name +
                            '" contains an empty accepted MIME type.'
                        );
                    }


            const {
                    if (
                view,
                        field.filename !== undefined &&
                inputHandler
                        typeof field.filename !== 'string'
            } = this.editor;
                    ) {
                        errors.push(
                            'File field "' +
                            name +
                            '" must have "filename" as a string.'
                        );
                    }


            if (
                    if (
                view &&
                        field.filetext !== undefined &&
                inputHandler
                        typeof field.filetext !== 'string'
            ) {
                    ) {
                view.dom.removeEventListener(
                        errors.push(
                    'input',
                            'File field "' +
                    inputHandler
                            name +
                );
                            '" must have "filetext" as a string.'
            }
                        );
                    }


            if (view) {
                    if (
                view.destroy();
                        !field.accept ||
            }
                        !field.accept.length
                    ) {
                        warnings.push(
                            'File field "' +
                            name +
                            '" has no accepted MIME types.'
                        );
                    }


            this.editor = null;
                    if (
            this.editorView = null;
                        !field.filename
            this.editorTools = null;
                    ) {
        },
                        warnings.push(
                            'File field "' +
                            name +
                            '" has no filename template.'
                        );
                    }
                }


        getEditorView() {
                if (
            return this.editorView;
                    (
        },
                        field.type === 'select' ||
                        field.type === 'multiselect'
                    ) &&
                    !field.source &&
                    !Array.isArray(field.options)
                ) {
                    warnings.push(
                        'Field "' +
                        name +
                        '" is a ' +
                        field.type +
                        ' without a source or manual options.'
                    );
                }


        getEditorContent() {
                if (field.type === 'file') {
            if (
                    if (
                this.editorView &&
                        field.filename !== undefined &&
                this.editorView.state
                        typeof field.filename !== 'string'
            ) {
                    ) {
                return this.editorView.state.doc.toString();
                        errors.push(
            }
                            'File field "' +
                            name +
                            '" has an invalid "filename" value.'
                        );
                    }


            return (
                    if (
                this.state.preloadContent || ''
                        field.filetext !== undefined &&
            );
                        typeof field.filetext !== 'string'
        },
                    ) {
                        errors.push(
                            'File field "' +
                            name +
                            '" has an invalid "filetext" value.'
                        );
                    }


        setEditorContent(content) {
                    if (
            content =
                        field.accept !== undefined &&
                String(content || '');
                        (
                            !Array.isArray(field.accept) ||
                            field.accept.some(
                                value =>
                                    typeof value !== 'string' ||
                                    !value.trim()
                            )
                        )
                    ) {
                        errors.push(
                            'File field "' +
                            name +
                            '" must have "accept" as an array of MIME types.'
                        );
                    }
                }
            });


             if (!this.editorView) {
             return {
                 this.state.preloadContent =
                 valid: !errors.length,
                    content;
                errors,
                 return;
                 warnings
            }
            };
        },


            const current =
        // =========================================================
                this.editorView.state.doc.toString();
        // Transformation helpers
        // =========================================================


            if (current === content) {
        transformationObject(
                return;
             transform,
             }
             prefix,
 
            suffix,
             this.editorView.dispatch({
             from,
                changes: {
            to
                    from: 0,
         ) {
                    to:
             if (!transform) {
                        this.editorView.state.doc.length,
                    insert: content
                }
             });
        },
 
         getEditorSelection() {
             if (
                !this.editorView ||
                !this.editorView.state
            ) {
                 return null;
                 return null;
             }
             }


             const selection =
             switch (transform) {
                 this.editorView.state
                 case 'remove-prefix':
                     .selection.main;
                     return prefix
                        ? {
                            transform,
                            prefix
                        }
                        : null;


            return {
                 case 'remove-suffix':
                 from:
                     return suffix
                     selection.from,
                        ? {
                            transform,
                            suffix
                        }
                        : null;


                 to:
                 case 'replace':
                     selection.to,
                     return from === ''
 
                        ? null
                text:
                        : {
                    this.editorView.state.doc.sliceString(
                            transform,
                        selection.from,
                            from,
                        selection.to
                            to: to || ''
                    )
                        };
            };
        },


        focusEditor() {
                default:
            if (this.editorView) {
                    return null;
                this.editorView.focus();
             }
             }
         },
         },


         insertEditorText(
         // =========================================================
            text,
        // UI helpers
            selectionFrom,
        // =========================================================
            selectionTo,
 
             cursorPosition
        createSection(title, className) {
        ) {
             const section = $('<section>')
             if (!this.editorView) {
                .addClass('preload-editor-section');
                 return;
 
             if (className) {
                 section.addClass(className);
             }
             }


             const view =
             $('<h2>')
                 this.editorView;
                 .addClass('preload-editor-section-title')
                .text(title)
                .appendTo(section);


             const selection =
             return section;
                view.state.selection.main;
        },


             const from =
        createButton(label, className, callback) {
                 selectionFrom === undefined
             const button = $('<button>')
                    ? selection.from
                 .attr('type', 'button')
                    : selectionFrom;
                .addClass('preload-editor-button')
                .text(label);


             const to =
             if (className) {
                 selectionTo === undefined
                 button.addClass(className);
                    ? selection.to
            }
                    : selectionTo;


             const cursor =
             if (callback) {
                 cursorPosition === undefined
                 button.on(
                     ? from + text.length
                     'click.preloadEditor',
                     : cursorPosition;
                     callback
                );
            }


             view.dispatch({
             return button;
                changes: {
                    from,
                    to,
                    insert: text
                },
 
                selection: {
                    anchor: cursor
                }
            });
 
            view.focus();
         },
         },


         // =========================================================
         fieldRow(label, input, description) {
        // Editor tools
            const row = $('<div>')
        // =========================================================
                .addClass('preload-editor-field');


        wrapSelectionWithVariable() {
            $('<label>')
            const selected =
                .addClass('preload-editor-label')
                 this.getEditorSelection();
                .text(label)
                 .appendTo(row);


             if (
             if (description) {
                 !selected ||
                 $('<div>')
                selected.from === selected.to
                    .addClass('preload-editor-help')
            ) {
                    .text(description)
                this.focusEditor();
                    .appendTo(row);
                return;
             }
             }


             const replacement =
             row.append(input);
                '%%' +
                selected.text +
                '%%';


             this.insertEditorText(
             return row;
                replacement,
        },
                 selected.from,
 
                selected.to,
        textInput(value, placeholder) {
                 selected.from +
            return $('<input>')
                    replacement.length
                 .attr({
            );
                    type: 'text',
                    value: value == null ? '' : value,
                    placeholder: placeholder || ''
                 })
                .addClass('preload-editor-input');
         },
         },


         insertVariable(name) {
         textarea(value) {
             name =
             return $('<textarea>')
                String(name || '')
                .val(value == null ? '' : value)
                    .trim()
                .addClass('preload-editor-textarea');
                    .toUpperCase();
        },


             if (!name) {
        checkbox(checked) {
                 return;
             return $('<input>')
            }
                .attr('type', 'checkbox')
                .prop('checked', !!checked)
                 .addClass('preload-editor-checkbox');
        },


             const selected =
        select(options, value) {
                 this.getEditorSelection();
             const select = $('<select>')
                 .addClass('preload-editor-select');


             if (!selected) {
             options.forEach(option => {
                return;
                $('<option>')
             }
                    .attr('value', option.value)
                    .text(option.label)
                    .prop(
                        'selected',
                        String(option.value) ===
                        String(value === undefined ? '' : value)
                    )
                    .appendTo(select);
             });


             this.insertEditorText(
             return select;
                '%%' + name + '%%',
                selected.from,
                selected.to
            );
         },
         },


         insertCondition(
         // =========================================================
            type,
        // In-wiki dialogs
            variable
         // =========================================================
         ) {
            type =
                String(type || '')
                    .toLowerCase();


             variable =
        createConfirmDialog(
                 String(variable || '')
            title,
                     .trim()
             message,
                    .toUpperCase();
            confirmLabel,
            cancelLabel
        ) {
            return new Promise(resolve => {
                 const overlay = $('<div>')
                     .addClass('preload-editor-prompt-overlay');


            const selected =
                const box = $('<div>')
                 this.getEditorSelection();
                    .addClass('preload-editor-prompt');
 
                 $('<h2>')
                    .text(title)
                    .appendTo(box);


            if (!selected) {
                const messageLines =
                return;
                    String(message || '').split('\n');
            }


            let text;
                messageLines.forEach(line => {
            let cursorOffset;
                    if (line === '') {
                        $('<div>')
                            .addClass(
                                'preload-editor-prompt-spacer'
                            )
                            .appendTo(box);


            switch (type) {
                case 'if': {
                    if (!variable) {
                         return;
                         return;
                     }
                     }


                     const opening =
                     $('<p>')
                        '<!-- @if ' +
                         .text(line)
                         variable +
                         .appendTo(box);
                         ' -->';
                });


                    const closing =
                const buttons = $('<div>')
                         '<!-- @endif -->';
                    .addClass(
                         'preload-editor-prompt-buttons'
                    );


                    if (
                const cancel = this.createButton(
                        selected.from !==
                    cancelLabel || 'Cancel',
                        selected.to
                    '',
                     ) {
                     () => {
                         text =
                         overlay.remove();
                            opening +
                        resolve(false);
                            '\n' +
                    }
                            selected.text +
                );
                            '\n' +
                            closing;


                        cursorOffset =
                const confirm = this.createButton(
                            text.length;
                     confirmLabel || 'Confirm',
                     } else {
                    'preload-editor-button-primary',
                        text =
                    () => {
                            opening +
                        overlay.remove();
                            '\n\n' +
                         resolve(true);
                            closing;
 
                         cursorOffset =
                            opening.length + 1;
                     }
                     }
                );


                    break;
                buttons.append(cancel, confirm);
                 }
                 box.append(buttons);
                overlay.append(box);


                 case 'elseif':
                 $(document.body).append(overlay);
                    text =
                        '<!-- @elseif -->';
                    cursorOffset =
                        text.length;
                    break;


                 case 'else':
                 const close = value => {
                     text =
                     if (value === 'Escape') {
                         '<!-- @else -->';
                         cancel.trigger('click');
                     cursorOffset =
                     }
                        text.length;
                };
                    break;


                 case 'endif':
                 overlay.on(
                     text =
                     'keydown.preloadEditor',
                        '<!-- @endif -->';
                     event => {
                     cursorOffset =
                         close(event.key);
                         text.length;
                     }
                     break;
                );


                 default:
                 overlay.attr('tabindex', '-1');
                    return;
                overlay.trigger('focus');
            }


            this.insertEditorText(
                overlay.on(
                text,
                    'click.preloadEditor',
                selected.from,
                    event => {
                selected.to,
                        if (event.target === overlay.get(0)) {
                selected.from +
                            cancel.trigger('click');
                     cursorOffset
                        }
             );
                     }
                );
             });
         },
         },


         getToolVariables() {
         createPromptDialog(title, message) {
             const variables =
             return new Promise(resolve => {
                 new Set(['PAGE']);
                const overlay = $('<div>')
                    .addClass(
                        'preload-editor-prompt-overlay'
                    );
 
                 const box = $('<div>')
                    .addClass(
                        'preload-editor-prompt'
                    );
 
                $('<h2>')
                    .text(title)
                    .appendTo(box);


            if (
                $('<p>')
                this.analysis &&
                    .text(message)
                this.analysis.variables
                     .appendTo(box);
            ) {
                this.analysis.variables.forEach(
                     name => {
                        variables.add(
                            String(
                                name
                            ).toUpperCase()
                        );
                    }
                );
            }


            return Array.from(variables);
                const input = this.textInput();
        },


        createToolSelect(
                const buttons = $('<div>')
            options,
            className
        ) {
            const select =
                $('<select>')
                     .addClass(
                     .addClass(
                         'preload-editor-tool-select'
                         'preload-editor-prompt-buttons'
                     );
                     );


            if (className) {
                const cancel = this.createButton(
                select.addClass(className);
                    'Cancel',
            }
                    '',
                    () => {
                        overlay.remove();
                        resolve(null);
                    }
                );


            options.forEach(
                const confirm = this.createButton(
                option => {
                    'Load',
                     $('<option>')
                     'preload-editor-button-primary',
                         .attr(
                    () => {
                            'value',
                         const value =
                             option.value
                            String(
                        )
                                input.val() || ''
                        .text(option.label)
                             ).trim();
                         .appendTo(select);
 
                }
                         if (!value) {
            );
                            return;
                        }


            return select;
                        overlay.remove();
        },
                        resolve(value);
                    }
                );


        updateEditorToolVariables() {
                buttons.append(cancel, confirm);
            if (!this.editorTools) {
                return;
            }


            const variables =
                 box.append(
                 this.getToolVariables();
                    input,
                    buttons
                );


            const current =
                 overlay.append(box);
                 this.editorTools
                    .variableSelect
                    .val();


            const currentCondition =
                 $(document.body).append(overlay);
                 this.editorTools
                    .conditionVariableSelect
                    .val();


            const populate =
                 input.trigger('focus');
                 (
                    select,
                    value
                ) => {
                    select.empty();


                     variables.forEach(
                input.on(
                        name => {
                     'keydown.preloadEditor',
                            $('<option>')
                    event => {
                                .attr(
                        if (event.key === 'Enter') {
                                    'value',
                            confirm.trigger('click');
                                    name
                                )
                                .text(name)
                                .appendTo(select);
                         }
                         }
                    );


                    if (
                        if (event.key === 'Escape') {
                        value &&
                            cancel.trigger('click');
                        variables.includes(value)
                        }
                    ) {
                        select.val(value);
                    } else if (
                        variables.length
                    ) {
                        select.val(
                            variables[0]
                        );
                     }
                     }
                 };
                 );
            });
        },


             populate(
        createConfigReviewDialog() {
                 this.editorTools.variableSelect,
             return new Promise(resolve => {
                current
                 const overlay = $('<div>')
            );
                    .addClass(
                        'preload-editor-prompt-overlay preload-editor-code-review-overlay'
                    );


            populate(
                const box = $('<div>')
                this.editorTools.conditionVariableSelect,
                    .addClass(
                currentCondition
                        'preload-editor-prompt preload-editor-code-review'
            );
                    );
        },


        updateConditionVariableVisibility() {
                $('<h2>')
            if (!this.editorTools) {
                    .text('Review config.json')
                return;
                    .appendTo(box);
            }


            const type =
                $('<p>')
                this.editorTools
                    .text(
                     .conditionTypeSelect
                        'This is the config.json that will be saved for the current preload.'
                     .val();
                     )
                     .appendTo(box);


            this.editorTools
                 const code = $('<pre>')
                 .conditionVariableWrap
                    .addClass(
                .toggle(
                        'preload-editor-config-review'
                    type === 'if'
                    )
                );
                    .text(this.getConfigJSON());
        },


        renderEditorTools() {
                const codeWrap = $('<div>')
            const tools =
                $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-tools'
                         'preload-editor-config-review-wrap'
                     );
                     )
                    .append(code);
 
                box.append(codeWrap);


            const wrapGroup =
                const buttons = $('<div>')
                $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-tool-group preload-editor-tool-group-wrap'
                         'preload-editor-prompt-buttons'
                     );
                     );


            wrapGroup.append(
                 const close = this.createButton(
                 this.createButton(
                     'Close',
                     'Wrap selection',
                     'preload-editor-button-primary',
                     'preload-editor-tool-button',
                     () => {
                     () =>
                         overlay.remove();
                         this.wrapSelectionWithVariable()
                        resolve();
                 )
                    }
            );
                );
 
                buttons.append(close);
                box.append(buttons);
                 overlay.append(box);
 
                $(document.body).append(overlay);


            const variableGroup =
                 overlay.attr('tabindex', '-1');
                 $('<div>')
                overlay.trigger('focus');
                    .addClass(
                        'preload-editor-tool-group preload-editor-tool-group-variable'
                    );


            $('<span>')
                 overlay.on(
                 .addClass(
                     'keydown.preloadEditor',
                     'preload-editor-tool-label'
                    event => {
                )
                        if (event.key === 'Escape') {
                .text(
                            close.trigger('click');
                    'Insert existing variable'
                        }
                )
                    }
                 .appendTo(variableGroup);
                 );


            const variableSelect =
                 overlay.on(
                 this.createToolSelect(
                     'click.preloadEditor',
                     [],
                     event => {
                     'preload-editor-variable-select'
                        if (event.target === overlay.get(0)) {
                            close.trigger('click');
                        }
                    }
                 );
                 );
            });
        },


             variableGroup.append(
        async confirmSave(mode, title) {
                 variableSelect,
             const configTitle =
                title.replace(/\/+$/, '') +
                 '/config.json';


                this.createButton(
             let saveDescription;
                    'Insert',
                    'preload-editor-tool-button',
                    () =>
                        this.insertVariable(
                            variableSelect.val()
                        )
                )
             );


             const conditionGroup =
             if (mode === 'preload') {
                 $('<div>')
                 saveDescription =
                     .addClass(
                    'Save the preload page only?';
                        'preload-editor-tool-group preload-editor-tool-group-condition'
            } else if (mode === 'config') {
                    );
                saveDescription =
                     'Save the config.json page only?';
            } else {
                saveDescription =
                    'Save both the preload and config.json?';
            }


             $('<span>')
             const pages = [];
                .addClass(
                    'preload-editor-tool-label'
                )
                .text('Insert condition')
                .appendTo(conditionGroup);


             const conditionTypeSelect =
             if (mode === 'preload' || mode === 'both') {
                this.createToolSelect(
                pages.push(
                    [
                    'Preload: ' +
                        {
                     title
                            value: 'if',
                            label: '@if'
                        },
                        {
                            value: 'elseif',
                            label: '@elseif'
                        },
                        {
                            value: 'else',
                            label: '@else'
                        },
                        {
                            value: 'endif',
                            label: '@endif'
                        }
                    ],
                     'preload-editor-condition-type-select'
                 );
                 );
            }


             const conditionVariableSelect =
             if (mode === 'config' || mode === 'both') {
                 this.createToolSelect(
                 pages.push(
                     [],
                     'Config: ' +
                     'preload-editor-condition-variable-select'
                     configTitle
                 );
                 );


            const conditionVariableWrap =
                 pages.push(
                 $('<span>')
                     'Config content model: JSON'
                    .addClass(
                        'preload-editor-condition-variable'
                    )
                    .append(
                        conditionVariableSelect
                    );
 
            const insertCondition =
                this.createButton(
                    'Insert',
                     'preload-editor-tool-button',
                    () =>
                        this.insertCondition(
                            conditionTypeSelect.val(),
                            conditionVariableSelect.val()
                        )
                 );
                 );
            }


             conditionTypeSelect.on(
             const message =
                 'change.preloadEditor',
                saveDescription +
                 () =>
                 '\n\n' +
                    this.updateConditionVariableVisibility()
                 pages.join('\n') +
            );
                '\n\nThis will write the changes to the wiki.';


             conditionGroup.append(
             return this.createConfirmDialog(
                 conditionTypeSelect,
                 'Confirm save',
                 conditionVariableWrap,
                 message,
                 insertCondition
                 'Save',
                'Cancel'
             );
             );
        },


             tools.append(
        // =========================================================
                 wrapGroup,
        // CodeMirror
                 variableGroup,
        // =========================================================
                 conditionGroup
                destroyEditor() {
             );
             if (!this.editor) {
                this.editorView = null;
                this.editorTools = null;
                 return;
            }
 
            const {
                 view,
                 inputHandler
             } = this.editor;


             this.editorTools = {
             if (view && inputHandler) {
                 root: tools,
                 view.dom.removeEventListener(
                variableSelect,
                    'input',
                conditionTypeSelect,
                    inputHandler
                 conditionVariableSelect,
                 );
                conditionVariableWrap,
             }
                insertCondition
             };


             this.updateEditorToolVariables();
             if (view) {
            this.updateConditionVariableVisibility();
                view.destroy();
            }


             return tools;
             this.editor = null;
            this.editorView = null;
            this.editorTools = null;
         },
         },


         updateEditorTools() {
         getEditorView() {
             this.updateEditorToolVariables();
             return this.editorView;
            this.updateConditionVariableVisibility();
         },
         },


         // =========================================================
         getEditorContent() {
        // Syntax highlighting
             if (
        // =========================================================
                 this.editorView &&
 
                 this.editorView.state
        getKnownVariables() {
             const known =
                new Set(['PAGE']);
 
            const fields =
                 this.state.config &&
                 this.state.config.fields;
 
            if (
                fields &&
                typeof fields === 'object'
             ) {
             ) {
                 Object.keys(fields).forEach(
                 return this.editorView.state.doc.toString();
                    name => {
                        known.add(
                            String(
                                name
                            ).toUpperCase()
                        );
                    }
                );
             }
             }


             return known;
             return this.state.preloadContent || '';
         },
         },


         createSyntaxDecorations(
         setEditorContent(content) {
            state,
             content = String(content || '');
            Decoration
        ) {
             const decorations = [];
            const source =
                state.doc.toString();


             const knownVariables =
             if (!this.editorView) {
                 this.getKnownVariables();
                 this.state.preloadContent = content;
                return;
            }


             const variablePattern =
             const current =
                 /%%([A-Za-z0-9_-]+)%%/g;
                 this.editorView.state.doc.toString();


             let match;
             if (current === content) {
                return;
            }


             while (
             this.editorView.dispatch({
                 (
                 changes: {
                     match =
                     from: 0,
                        variablePattern.exec(
                    to: this.editorView.state.doc.length,
                            source
                    insert: content
                        )
                }
                 ) !== null
            });
        },
 
        getEditorSelection() {
            if (
                !this.editorView ||
                 !this.editorView.state
             ) {
             ) {
                 const name =
                 return null;
                    match[1].toUpperCase();
            }
 
            const selection =
                this.editorView.state.selection.main;


                 const known =
            return {
                     knownVariables.has(name);
                from: selection.from,
                to: selection.to,
                 text: this.editorView.state.doc.sliceString(
                    selection.from,
                     selection.to
                )
            };
        },


                decorations.push(
        focusEditor() {
                    Decoration.mark({
            if (this.editorView) {
                        class: [
                this.editorView.focus();
                            'preload-editor-syntax-variable',
                            known
                                ? 'preload-editor-syntax-variable-known'
                                : 'preload-editor-syntax-variable-unknown'
                        ].join(' ')
                    }).range(
                        match.index,
                        match.index +
                            match[0].length
                    )
                );
             }
             }
        },


             const conditionPattern =
        insertEditorText(
                /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
            text,
            selectionFrom,
            selectionTo,
             cursorPosition
        ) {
            if (!this.editorView) {
                return;
            }
 
            const view = this.editorView;
            const selection = view.state.selection.main;


             while (
             const from =
                 (
                 selectionFrom === undefined
                    match =
                    ? selection.from
                        conditionPattern.exec(
                     : selectionFrom;
                            source
                        )
                ) !== null
            ) {
                decorations.push(
                     Decoration.mark({
                        class:
                            'preload-editor-syntax-condition'
                    }).range(
                        match.index,
                        match.index +
                            match[0].length
                    )
                );


                 const directiveStart =
            const to =
                     match.index +
                 selectionTo === undefined
                     match[0].indexOf('@');
                     ? selection.to
                     : selectionTo;


                 decorations.push(
            const cursor =
                     Decoration.mark({
                 cursorPosition === undefined
                        class:
                     ? from + text.length
                            'preload-editor-syntax-directive'
                     : cursorPosition;
                    }).range(
                        directiveStart,
                        directiveStart +
                            match[1].length +
                            1
                     )
                );


                const type =
            view.dispatch({
                    match[1].toLowerCase();
                changes: {
 
                    from,
                if (
                     to,
                     type !== 'if' &&
                     insert: text
                     type !== 'elseif'
                },
                 ) {
                 selection: {
                     continue;
                     anchor: cursor
                 }
                 }
            });


                const expression =
            view.focus();
                    match[2];
        },


                const expressionStart =
        // =========================================================
                    match.index +
        // Editor tools
                    match[0].indexOf(
        // =========================================================
                        expression
                    );


                const expressionPattern =
        wrapSelectionWithVariable() {
                    /[A-Za-z0-9_-]+/g;
            const selected =
                this.getEditorSelection();


                 let expressionMatch;
            if (
 
                 !selected ||
                 while (
                 selected.from === selected.to
                    (
            ) {
                        expressionMatch =
                this.focusEditor();
                            expressionPattern.exec(
                return;
                                expression
            }
                            )
                    ) !== null
                ) {
                    const name =
                        expressionMatch[0]
                            .toUpperCase();


                    if (
            const replacement =
                        !knownVariables.has(name)
                '%%' +
                    ) {
                selected.text +
                        continue;
                '%%';
                    }


                    const start =
            this.insertEditorText(
                        expressionStart +
                replacement,
                        expressionMatch.index;
                selected.from,
 
                 selected.to,
                    decorations.push(
                 selected.from + replacement.length
                        Decoration.mark({
                            class:
                                'preload-editor-syntax-condition-variable'
                        }).range(
                            start,
                            start +
                                expressionMatch[0]
                                    .length
                        )
                    );
                 }
            }
 
            return Decoration.set(
                decorations,
                 true
             );
             );
         },
         },


         async installSyntaxHighlighting(view) {
         insertVariable(name) {
             await mw.loader.using(
             name = String(name || '')
                 'ext.CodeMirror.v6.lib'
                .trim()
             );
                .toUpperCase();
 
            if (!name) {
                 return;
            }
 
             const selected =
                this.getEditorSelection();


             const require =
             if (!selected) {
                 mw.loader.require;
                 return;
            }


             const {
             this.insertEditorText(
                 StateField,
                 '%%' + name + '%%',
                 StateEffect,
                 selected.from,
                EditorView,
                 selected.to
                 Decoration
            } = require(
                'ext.CodeMirror.v6.lib'
             );
             );
        },


             const syntaxField =
        insertCondition(
                StateField.define({
            type,
                    create: state =>
            variable
                        this.createSyntaxDecorations(
        ) {
                            state,
             type = String(type || '').toLowerCase();
                            Decoration
            variable = String(variable || '')
                        ),
                .trim()
                .toUpperCase();


                    update: (
            const selected =
                        decorations,
                this.getEditorSelection();
                        transaction
                    ) =>
                        transaction.docChanged
                            ? this.createSyntaxDecorations(
                                transaction.state,
                                Decoration
                            )
                            : decorations,


                    provide: field =>
            if (!selected) {
                        EditorView.decorations.from(
                 return;
                            field
            }
                        )
                 });


             view.dispatch({
             let text;
                effects:
             let cursorOffset;
                    StateEffect.appendConfig.of(
                        syntaxField
                    )
             });


             this.editor.syntaxField =
             switch (type) {
                 syntaxField;
                 case 'if': {
        },
                    if (!variable) {
                        return;
                    }


        async mountCodeMirror(textarea) {
                    const opening =
            return new Promise(
                        '<!-- @if ' +
                (
                        variable +
                    resolve,
                        ' -->';
                    reject
                ) => {
                    let settled = false;


                     const cleanup =
                     const closing =
                         () => {
                         '<!-- @endif -->';
                            mw.hook(
                                'dev.CCM.ready'
                            ).remove(onReady);


                            mw.hook(
                    if (
                                'dev.CCM.load'
                        selected.from !==
                             ).remove(onLoad);
                        selected.to
                         };
                    ) {
 
                        text =
                     const onReady =
                            opening +
                         async cm => {
                            '\n' +
                             if (
                             selected.text +
                                settled ||
                            '\n' +
                                !cm ||
                            closing;
                                cm.textarea !== textarea ||
 
                                !cm.view
                         cursorOffset = text.length;
                             ) {
                     } else {
                                return;
                         text =
                            }
                             opening +
                            '\n\n' +
                             closing;


                            settled = true;
                        cursorOffset =
                             cleanup();
                             opening.length + 1;
                    }


                            const view =
                    break;
                                cm.view;
                }


                            const inputHandler =
                case 'elseif':
                                () => {
                    text =
                                    this.state
                        '<!-- @elseif -->';
                                        .preloadContent =
                                        view.state.doc.toString();


                                    this.markPreloadDirty();
                    cursorOffset =
                        text.length;


                                    this.reanalyze();
                    break;


                                    this.renderVariablesBySelector();
                case 'else':
                                    this.renderVariableConfigurationBySelector();
                    text =
                                    this.renderAnalysisBySelector();
                        '<!-- @else -->';
                                    this.updateEditorTools();


                                    this.saveDraft();
                    cursorOffset =
                                };
                        text.length;


                            view.dom.addEventListener(
                    break;
                                'input',
                                inputHandler
                            );


                            view.dom.classList.add(
                case 'endif':
                                'preload-editor-codemirror'
                    text =
                            );
                        '<!-- @endif -->';


                            const wrapper =
                    cursorOffset =
                                view.dom.closest(
                        text.length;
                                    '.ext-codemirror-wrapper'
                                );


                            if (wrapper) {
                    break;
                                wrapper.classList.add(
                                    'preload-editor-codemirror-wrapper'
                                );
                            }


                            this.editor = {
                default:
                                cm,
                    return;
                                view,
            }
                                textarea,
                                inputHandler
                            };


                            this.editorView =
            this.insertEditorText(
                                view;
                text,
                selected.from,
                selected.to,
                selected.from + cursorOffset
            );
        },


                            this.state.preloadContent =
        getToolVariables() {
                                view.state.doc.toString();
            const variables =
                new Set(['PAGE']);


                            try {
            if (
                                await this.installSyntaxHighlighting(
                this.analysis &&
                                    view
                this.analysis.variables
                                );
            ) {
                this.analysis.variables.forEach(
                    name => {
                        variables.add(
                            String(name).toUpperCase()
                        );
                    }
                );
            }


                                if (
            return Array.from(variables);
                                    this.state.dirty
        },
                                ) {
                                    this.saveDraft();
                                }


                                resolve(view);
        createToolSelect(
                            } catch (error) {
            options,
                                reject(error);
            className
                            }
        ) {
                         };
            const select =
                $('<select>')
                    .addClass(
                         'preload-editor-tool-select'
                    );


                    const onLoad =
            if (className) {
                        loader => {
                select.addClass(className);
                            if (
            }
                                settled ||
                                typeof loader !==
                                    'function'
                            ) {
                                return;
                            }


                            try {
            options.forEach(
                                loader(textarea);
                option => {
                            } catch (error) {
                    $('<option>')
                                settled = true;
                         .attr(
                                cleanup();
                            'value',
                                reject(error);
                            option.value
                            }
                        )
                         };
                        .text(option.label)
 
                         .appendTo(select);
                    mw.hook(
                        'dev.CCM.ready'
                    ).add(onReady);
 
                    mw.hook(
                         'dev.CCM.load'
                    ).add(onLoad);
                 }
                 }
             );
             );
            return select;
         },
         },


         // =========================================================
         updateEditorToolVariables() {
        // Main UI
             if (!this.editorTools) {
        // =========================================================
 
        init() {
            const root =
                $('#mw-content-text');
 
             if (!root.length) {
                 return;
                 return;
             }
             }


             this.root =
             const variables =
                 $('<div>')
                 this.getToolVariables();
                    .attr(
                        'id',
                        'preload-editor'
                    )
                    .addClass(
                        'preload-editor'
                    );


             root.append(this.root);
             const current =
                this.editorTools
                    .variableSelect
                    .val();


             this.renderShell();
             const currentCondition =
                this.editorTools
                    .conditionVariableSelect
                    .val();


             this.restoreDraft();
             const populate = (
                select,
                value
            ) => {
                select.empty();


            window.addEventListener(
                variables.forEach(
                'beforeunload',
                    name => {
                () => {
                        $('<option>')
                    if (this.state.dirty) {
                            .attr(
                        this.saveDraft();
                                'value',
                                name
                            )
                            .text(name)
                            .appendTo(select);
                     }
                     }
                );
                if (
                    value &&
                    variables.includes(value)
                ) {
                    select.val(value);
                } else if (
                    variables.length
                ) {
                    select.val(
                        variables[0]
                    );
                 }
                 }
            };
            populate(
                this.editorTools.variableSelect,
                current
            );
            populate(
                this.editorTools.conditionVariableSelect,
                currentCondition
             );
             );
         },
         },


         renderShell() {
         updateConditionVariableVisibility() {
             this.destroyEditor();
             if (!this.editorTools) {
             this.root.empty();
                return;
             }


             const loader =
             const type =
                 this.createSection('Preload');
                 this.editorTools
                    .conditionTypeSelect
                    .val();


             const titleInput =
             this.editorTools
                this.textInput(
                .conditionVariableWrap
                    this.state.preloadTitle,
                 .toggle(
                    'Template:Example/Preload'
                     type === 'if'
                 ).attr(
                     'id',
                    'preload-editor-title-input'
                 );
                 );
        },


             titleInput.on(
        renderEditorTools() {
                'input.preloadEditor',
             const tools =
                 () => {
                $('<div>')
                     this.state.preloadTitle =
                    .addClass(
                         String(
                        'preload-editor-tools'
                            titleInput.val() || ''
                    );
                        ).trim();
 
            const wrapGroup =
                 $('<div>')
                     .addClass(
                         'preload-editor-tool-group preload-editor-tool-group-wrap'
                    );


                    if (
            wrapGroup.append(
                        this.state.dirty &&
                this.createButton(
                        this.state.mode === 'new'
                    'Wrap selection',
                     ) {
                    'preload-editor-tool-button',
                         this.saveDraft();
                     () =>
                    }
                         this.wrapSelectionWithVariable()
                 }
                 )
             );
             );


             const loadButton =
             const variableGroup =
                 this.createButton(
                 $('<div>')
                    'Load',
                     .addClass(
                     'preload-editor-button-primary',
                        'preload-editor-tool-group preload-editor-tool-group-variable'
                     () =>
                     );
                        this.loadExisting(
                            titleInput.val()
                        )
                );


             const newButton =
             $('<span>')
                 this.createButton(
                 .addClass(
                     'New',
                     'preload-editor-tool-label'
                     '',
                )
                    () => this.startNew()
                .text(
                );
                     'Insert existing variable'
                )
                .appendTo(variableGroup);


             const sampleButton =
             const variableSelect =
                 this.createButton(
                 this.createToolSelect(
                     'Load Sample Page',
                     [],
                     '',
                     'preload-editor-variable-select'
                    () => this.loadSamplePage()
                 );
                 );


             loader.append(
             variableGroup.append(
                 $('<div>')
                 variableSelect,
                     .addClass(
 
                        'preload-editor-toolbar'
                this.createButton(
                     )
                    'Insert',
                    .append(
                     'preload-editor-tool-button',
                        titleInput,
                     () =>
                         loadButton,
                        this.insertVariable(
                        newButton,
                            variableSelect.val()
                        sampleButton
                         )
                    )
                )
             );
             );


             this.root.append(loader);
             const conditionGroup =
 
            this.workspace =
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-workspace'
                         'preload-editor-tool-group preload-editor-tool-group-condition'
                     );
                     );


             this.root.append(
             $('<span>')
                 this.workspace
                .addClass(
            );
                    'preload-editor-tool-label'
                )
                .text('Insert condition')
                 .appendTo(conditionGroup);


             /*
             const conditionTypeSelect =
            * Fixed footer
                this.createToolSelect(
            *
                    [
            * Local draft status, normal status messages,
                        {
            * and save actions live here.
                            value: 'if',
            */
                            label: '@if'
             const footer =
                        },
                 $('<div>')
                        {
                     .addClass(
                            value: 'elseif',
                        'preload-editor-footer'
                            label: '@elseif'
                    );
                        },
                        {
                            value: 'else',
                            label: '@else'
                        },
                        {
                            value: 'endif',
                            label: '@endif'
                        }
                    ],
                    'preload-editor-condition-type-select'
                );
 
             const conditionVariableSelect =
                 this.createToolSelect(
                     [],
                    'preload-editor-condition-variable-select'
                );


             this.localDraftStatus =
             const conditionVariableWrap =
                 $('<div>')
                 $('<span>')
                     .addClass(
                     .addClass(
                         'preload-editor-local-draft-status is-empty'
                         'preload-editor-condition-variable'
                     )
                     )
                     .text(
                     .append(
                         'No local draft'
                         conditionVariableSelect
                     );
                     );


             this.status =
             const insertCondition =
                 $('<div>')
                 this.createButton(
                     .addClass(
                    'Insert',
                        'preload-editor-status'
                     'preload-editor-tool-button',
                     );
                     () =>
                        this.insertCondition(
                            conditionTypeSelect.val(),
                            conditionVariableSelect.val()
                        )
                );


             footer.append(
             conditionTypeSelect.on(
                 this.localDraftStatus,
                 'change.preloadEditor',
                 this.status
                 () =>
                    this.updateConditionVariableVisibility()
             );
             );


             this.root.append(
             conditionGroup.append(
                 footer
                 conditionTypeSelect,
                conditionVariableWrap,
                insertCondition
             );
             );


             this.updateLocalDraftIndicator();
             tools.append(
                wrapGroup,
                variableGroup,
                conditionGroup
            );


             this.renderWelcome();
             this.editorTools = {
        },
                root: tools,
                variableSelect,
                conditionTypeSelect,
                conditionVariableSelect,
                conditionVariableWrap,
                insertCondition
            };


        renderWelcome() {
            this.updateEditorToolVariables();
             this.workspace.empty();
             this.updateConditionVariableVisibility();


             const section =
             return tools;
                this.createSection('Get started');
        },


             $('<p>')
        updateEditorTools() {
                .text(
             this.updateEditorToolVariables();
                    'Load an existing preload, create a new one, or start from a sample wiki page.'
            this.updateConditionVariableVisibility();
                )
        },
                 .appendTo(section);
 
        // =========================================================
        // Syntax highlighting
        // =========================================================
 
        getKnownVariables() {
            const known =
                 new Set(['PAGE']);


             const tips =
             const fields =
                 $('<ul>')
                 this.state.config &&
                    .addClass(
                this.state.config.fields;
                        'preload-editor-tips'
                    );


             [
             if (
                 'Existing preload: loads the preload and its config.json if available.',
                 fields &&
                 'New preload: starts with the standard Preload Template structure.',
                 typeof fields === 'object'
                 'Sample page: loads normal wiki page content so you can turn it into a preload.'
            ) {
            ].forEach(
                 Object.keys(fields).forEach(
                text => {
                    name => {
                    $('<li>')
                        known.add(
                        .text(text)
                            String(name).toUpperCase()
                         .appendTo(tips);
                         );
                }
                    }
            );
                );
            }


             section.append(tips);
             return known;
            this.workspace.append(section);
         },
         },


         setStatus(
         createSyntaxDecorations(
             message,
             state,
             type
             Decoration
         ) {
         ) {
             if (!this.status) {
             const decorations = [];
                return;
            const source =
            }
                state.doc.toString();


             this.status.removeClass(
             const knownVariables =
                'is-error',
                this.getKnownVariables();
                'is-success',
                'is-warning',
                'is-loading'
            );


             if (type) {
             const variablePattern =
                 this.status.addClass(
                 /%%([A-Za-z0-9_-]+)%%/g;
                    'is-' + type
                );
            }


             this.status.text(
             let match;
                message || ''
            );
        },


        // =========================================================
            while (
        // Load / create
                (
        // =========================================================
                    match =
                        variablePattern.exec(source)
                ) !== null
            ) {
                const name =
                    match[1].toUpperCase();


        async loadExisting(title) {
                const known =
            title =
                    knownVariables.has(name);
                String(title || '').trim();


            if (!title) {
                decorations.push(
                mw.notify(
                    Decoration.mark({
                    'Enter a preload page title first.',
                        class: [
                    {
                            'preload-editor-syntax-variable',
                         type: 'warn'
                            known
                     }
                                ? 'preload-editor-syntax-variable-known'
                                : 'preload-editor-syntax-variable-unknown'
                         ].join(' ')
                     }).range(
                        match.index,
                        match.index +
                            match[0].length
                    )
                 );
                 );
                return;
             }
             }


             this.setStatus(
             const conditionPattern =
                 'Loading preload...',
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
                'loading'
            );


             try {
             while (
                 const page =
                (
                     await this.getPage(title);
                    match =
                        conditionPattern.exec(source)
                ) !== null
            ) {
                 decorations.push(
                    Decoration.mark({
                        class:
                            'preload-editor-syntax-condition'
                     }).range(
                        match.index,
                        match.index +
                            match[0].length
                    )
                );


                 if (!page) {
                 const directiveStart =
                     const suggestions =
                     match.index +
                        await this.findSimilarPreloads(
                    match[0].indexOf('@');
                            title
                        );


                     this.renderMissingPage(
                decorations.push(
                         title,
                    Decoration.mark({
                         suggestions
                        class:
                     );
                            'preload-editor-syntax-directive'
                     }).range(
                         directiveStart,
                         directiveStart +
                            match[1].length +
                            1
                     )
                );


                     this.setStatus(
                const type =
                        'Preload not found.',
                     match[1].toLowerCase();
                        'warning'
                    );


                     return;
                if (
                     type !== 'if' &&
                    type !== 'elseif'
                ) {
                    continue;
                 }
                 }


                 await this.loadDefinition(
                 const expression =
                     page.title,
                     match[2];
                    page.content
                );


                 this.setStatus(
                 const expressionStart =
                     'Preload loaded.',
                     match.index +
                     'success'
                     match[0].indexOf(
                );
                        expression
            } catch (error) {
                     );
                console.error(
                    'Preload Editor:',
                     error
                );


                 this.setStatus(
                 const expressionPattern =
                     error.message ||
                     /[A-Za-z0-9_-]+/g;
                        'Could not load preload.',
                    'error'
                );
            }
        },


        async findSimilarPreloads(title) {
                let expressionMatch;
            let query =
                title
                    .replace(
                        /\/Preload$/i,
                        ''
                    )
                    .trim();


            if (!query) {
                while (
                query = title;
                    (
            }
                        expressionMatch =
                            expressionPattern.exec(
                                expression
                            )
                    ) !== null
                ) {
                    const name =
                        expressionMatch[0]
                            .toUpperCase();


            try {
                    if (
                const results =
                        !knownVariables.has(name)
                     await this.searchPages(query);
                     ) {
                        continue;
                    }


                return results.filter(
                    const start =
                    result =>
                        expressionStart +
                         /\/Preload$/i.test(
                        expressionMatch.index;
                             result.title
 
                    decorations.push(
                        Decoration.mark({
                            class:
                                'preload-editor-syntax-condition-variable'
                         }).range(
                             start,
                            start +
                                expressionMatch[0].length
                         )
                         )
                );
                    );
             } catch (error) {
                }
                console.error(
             }
                    'Preload Editor search:',
                    error
                );


                 return [];
            return Decoration.set(
             }
                decorations,
                 true
             );
         },
         },


         renderMissingPage(
         async installSyntaxHighlighting(view) {
            title,
             await mw.loader.using(
            suggestions
                'ext.CodeMirror.v6.lib'
        ) {
            );
             this.workspace.empty();


             const section =
             const require =
                 this.createSection(
                 mw.loader.require;
                    'Preload not found'
                );


             $('<p>')
             const {
                 .text(
                StateField,
                    'No preload exists at "' +
                StateEffect,
                    title +
                 EditorView,
                    '".'
                Decoration
                )
            } = require(
                .appendTo(section);
                'ext.CodeMirror.v6.lib'
            );


             if (suggestions.length) {
             const syntaxField =
                $('<h3>')
                StateField.define({
                    .text('Similar preloads')
                    create: state =>
                    .appendTo(section);
                        this.createSyntaxDecorations(
                            state,
                            Decoration
                        ),


                const list =
                    update: (
                     $('<div>')
                        decorations,
                         .addClass(
                        transaction
                             'preload-editor-suggestions'
                     ) =>
                        );
                         transaction.docChanged
                            ? this.createSyntaxDecorations(
                                transaction.state,
                                Decoration
                             )
                            : decorations,


                suggestions.forEach(
                     provide: field =>
                     result => {
                         EditorView.decorations.from(
                         list.append(
                             field
                            this.createButton(
                         )
                                result.title,
                });
                                'preload-editor-suggestion',
                                () =>
                                    this.loadExisting(
                                        result.title
                                    )
                             )
                         );
                    }
                );


                 section.append(list);
            view.dispatch({
             }
                 effects:
                    StateEffect.appendConfig.of(
                        syntaxField
                    )
             });


             section.append(
             this.editor.syntaxField =
                this.createButton(
                 syntaxField;
                    'Create this preload',
                    'preload-editor-button-primary',
                    () =>
                        this.startNew(title)
                 )
            );
 
            this.workspace.append(section);
         },
         },


         async startNew(title) {
         async mountCodeMirror(textarea) {
             title =
             return new Promise(
                 String(title || '').trim();
                 (
                    resolve,
                    reject
                ) => {
                    let settled = false;


            this.state = {
                    const cleanup = () => {
                preloadTitle: title,
                        mw.hook(
                preloadSource: '',
                            'dev.CCM.ready'
                preloadContent:
                        ).remove(onReady);
                    this.defaults.preload,


                config:
                        mw.hook(
                    this.emptyConfig(),
                            'dev.CCM.load'
                        ).remove(onLoad);
                    };


                variables: ['PAGE'],
                    const onReady =
                conditions: [],
                        async cm => {
                selectedVariable: 'PAGE',
                            if (
                deletedVariables: [],
                                settled ||
                mode: 'new',
                                !cm ||
                dirty: true,
                                cm.textarea !== textarea ||
                dirtyPreload: true,
                                !cm.view
                dirtyConfig: true,
                            ) {
                                return;
                            }
 
                            settled = true;
                            cleanup();


                localDraftSavedAt: null,
                            const view =
                localDraftAvailable: false
                                cm.view;
            };


            this.saveDraft();
                            const inputHandler =
            this.renderEditor();
                                () => {
                                    this.state
                                        .preloadContent =
                                        view.state.doc.toString();


            this.setStatus(
                                    this.markPreloadDirty();
                'Creating a new preload.',
                'success'
            );
        },


        async loadSamplePage() {
                                    this.reanalyze();
            const result =
                await this.createPromptDialog(
                    'Load Sample Page',
                    'Enter the title of a normal wiki page to use as the starting point.'
                );


            if (!result) {
                                    this.renderVariablesBySelector();
                return;
                                    this.renderVariableConfigurationBySelector();
            }
                                    this.renderAnalysisBySelector();
                                    this.updateEditorTools();


            this.setStatus(
                                    this.saveDraft();
                'Loading sample page...',
                                };
                'loading'
            );


            try {
                            view.dom.addEventListener(
                const page =
                                'input',
                    await this.getPage(result);
                                inputHandler
                            );


                if (!page) {
                            view.dom.classList.add(
                    throw new Error(
                                'preload-editor-codemirror'
                        'Sample page not found: ' +
                            );
                        result
                    );
                }


                const sample =
                            const wrapper =
                    this.convertSampleToPreload(
                                view.dom.closest(
                        page.content
                                    '.ext-codemirror-wrapper'
                    );
                                );


                this.state = {
                            if (wrapper) {
                    preloadTitle: '',
                                wrapper.classList.add(
                    preloadSource: '',
                                    'preload-editor-codemirror-wrapper'
                    preloadContent: sample,
                                );
                            }


                    config:
                            this.editor = {
                        this.emptyConfig(),
                                cm,
                                view,
                                textarea,
                                inputHandler
                            };


                    variables: ['PAGE'],
                            this.editorView =
                    conditions: [],
                                view;
                    selectedVariable: 'PAGE',
                    deletedVariables: [],
                    mode: 'new',
                    dirty: true,
                    dirtyPreload: true,
                    dirtyConfig: true,


                    localDraftSavedAt: null,
                            this.state.preloadContent =
                    localDraftAvailable: false
                                view.state.doc.toString();
                };


                this.reanalyze();
                            try {
                this.saveDraft();
                                await this.installSyntaxHighlighting(
                this.renderEditor();
                                    view
                                );


                this.setStatus(
                                if (
                    'Sample page loaded.',
                                    this.state.dirty
                    'success'
                                ) {
                );
                                    this.saveDraft();
            } catch (error) {
                                }
                console.error(
                    'Preload Editor sample:',
                    error
                );


                this.setStatus(
                                resolve(view);
                    error.message ||
                            } catch (error) {
                        'Could not load sample page.',
                                reject(error);
                    'error'
                            }
                );
                        };
            }
        },


        convertSampleToPreload(source) {
                    const onLoad =
            let content =
                        loader => {
                String(source || '').trim();
                            if (
                                settled ||
                                typeof loader !==
                                    'function'
                            ) {
                                return;
                            }


            const include =
                            try {
                content.match(
                                loader(textarea);
                    /<includeonly>([\s\S]*?)<\/includeonly>/i
                            } catch (error) {
                );
                                settled = true;
                                cleanup();
                                reject(error);
                            }
                        };


            if (include) {
                    mw.hook(
                content =
                        'dev.CCM.ready'
                     include[1].trim();
                     ).add(onReady);
            }


            content =
                     mw.hook(
                content
                         'dev.CCM.load'
                     .replace(
                     ).add(onLoad);
                        /<noinclude>[\s\S]*?<\/noinclude>/gi,
                }
                         ''
            );
                     )
        },
                    .trim();


            return (
        // =========================================================
                '<includeonly>\n' +
        // Main UI
                content +
         // =========================================================
                '\n</includeonly>\n' +
                '<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n'
            );
         },


         async loadDefinition(
         init() {
            title,
             const root =
            source
                 $('#mw-content-text');
        ) {
             const content =
                 this.extractIncludeonly(source);


             let config =
             if (!root.length) {
                 this.emptyConfig();
                 return;
            }


             const configTitle =
             this.root =
                 title.replace(
                 $('<div>')
                    /\/+$/,
                    .attr(
                    ''
                        'id',
                ) +
                        'preload-editor'
                '/config.json';
                    )
                    .addClass(
                        'preload-editor'
                    );


             const configPage =
             root.append(this.root);
                await this.getPage(configTitle);


             if (configPage) {
             this.renderShell();
                try {
 
                    config =
            this.restoreDraft();
                        JSON.parse(
 
                            configPage.content
            window.addEventListener(
                        );
                'beforeunload',
                 } catch (error) {
                 () => {
                     throw new Error(
                     if (this.state.dirty) {
                        'The existing config.json contains invalid JSON: ' +
                         this.saveDraft();
                         error.message
                     }
                     );
                 }
                 }
             }
             );
        },
                renderShell() {
            this.destroyEditor();
            this.root.empty();


             const analysis =
             const loader =
                 this.analyzePreload(content);
                 this.createSection('Preload');


             this.state.deletedVariables = [];
             const titleInput =
 
                this.textInput(
            config =
                    this.state.preloadTitle,
                 this.prepareConfigForVariables(
                    'Template:Example/Preload'
                     config,
                 ).attr(
                     analysis
                     'id',
                     'preload-editor-title-input'
                 );
                 );


             this.state = {
             titleInput.on(
                preloadTitle: title,
                'input.preloadEditor',
                preloadSource: source,
                () => {
                preloadContent: source,
                    this.state.preloadTitle =
                config,
                        String(
                            titleInput.val() || ''
                        ).trim();


                variables:
                    if (
                     analysis.variables,
                        this.state.dirty &&
                        this.state.mode === 'new'
                     ) {
                        this.saveDraft();
                    }
                }
            );


                 conditions:
            const loadButton =
                     analysis.conditions,
                 this.createButton(
                    'Load',
                    'preload-editor-button-primary',
                     () =>
                        this.loadExisting(
                            titleInput.val()
                        )
                );


                 selectedVariable:
            const newButton =
                     analysis.variables.includes('PAGE')
                 this.createButton(
                        ? 'PAGE'
                     'New',
                        : (
                    '',
                            analysis.variables[0] ||
                    () => this.startNew()
                            'PAGE'
                );
                        ),


                 deletedVariables: [],
            const sampleButton =
 
                 this.createButton(
                mode: 'edit',
                    'Load Sample Page',
                dirty: false,
                    '',
                dirtyPreload: false,
                    () => this.loadSamplePage()
                 dirtyConfig: false,
                 );


                localDraftSavedAt: null,
             loader.append(
                localDraftAvailable: false
             };
 
            this.clearDraft(title);
            this.renderEditor();
        },
 
        // =========================================================
        // Editor
        // =========================================================
 
        renderEditor() {
            this.destroyEditor();
            this.workspace.empty();
 
            const layout =
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-layout'
                         'preload-editor-toolbar'
                     );
                     )
                    .append(
                        titleInput,
                        loadButton,
                        newButton,
                        sampleButton
                    )
            );


             const left =
             this.root.append(loader);
                $('<div>')
                    .addClass(
                        'preload-editor-left'
                    );


             const right =
             this.workspace =
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-right'
                         'preload-editor-workspace'
                     );
                     );


             const editorSection =
             this.root.append(
                 this.createSection(
                 this.workspace
                    'Preload',
            );
                    'preload-editor-main-section'
                );


             const meta =
            /*
            * Fixed footer
            *
            * Local draft status, normal status messages,
            * and save actions live here.
            */
             const footer =
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-meta'
                         'preload-editor-footer'
                     );
                     );


             $('<strong>')
             this.localDraftStatus =
                .text(
                 $('<div>')
                    this.state.preloadTitle ||
                    'New preload'
                )
                .appendTo(meta);
 
            if (
                this.state.mode === 'new'
            ) {
                 $('<span>')
                     .addClass(
                     .addClass(
                         'preload-editor-badge'
                         'preload-editor-local-draft-status is-empty'
                     )
                     )
                     .text('NEW')
                     .text(
                     .appendTo(meta);
                        'No local draft'
            }
                     );


             editorSection.append(meta);
             this.status =
            editorSection.append(
                $('<div>')
                this.renderEditorTools()
                    .addClass(
            );
                        'preload-editor-status'
                    );


             const textarea =
             footer.append(
                this.textarea(
                this.localDraftStatus,
                    this.state.preloadContent
                 this.status
                 )
            );
                    .attr(
                        'id',
                        'preload-editor-content'
                    )
                    .addClass(
                        'preload-editor-content'
                    );


             const editorMount =
             this.root.append(
                $('<div>')
                footer
                    .addClass(
            );
                        'preload-editor-codemirror-mount'
                    )
                    .append(textarea);


             editorSection.append(editorMount);
             this.updateLocalDraftIndicator();
            left.append(editorSection);


             const analysisSection =
             this.renderWelcome();
                this.createSection('Analysis');
        },


            const analysisContainer =
        renderWelcome() {
                $('<div>')
            this.workspace.empty();
                    .addClass(
                        'preload-editor-analysis'
                    );


             analysisSection.append(
             const section =
                analysisContainer
                this.createSection('Get started');
            );


             const variablesSection =
             $('<p>')
                 this.createSection('Variables');
                 .text(
                    'Load an existing preload, create a new one, or start from a sample wiki page.'
                )
                .appendTo(section);


             const detectVariablesButton =
             const tips =
                this.createButton(
                 $('<ul>')
                    'Detect variables',
                    'preload-editor-button-primary preload-editor-detect-variables',
                    () =>
                        this.detectVariables()
                );
 
            const detectHelp =
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-help preload-editor-detect-variables-help'
                         'preload-editor-tips'
                    )
                    .text(
                        'Refresh the variable list after editing the preload.'
                     );
                     );


             variablesSection.append(
             [
                 $('<div>')
                'Existing preload: loads the preload and its config.json if available.',
                    .addClass(
                'New preload: starts with the standard Preload Template structure.',
                         'preload-editor-variable-detect'
                'Sample page: loads normal wiki page content so you can turn it into a preload.'
                    )
            ].forEach(
                    .append(
                 text => {
                        detectVariablesButton,
                    $('<li>')
                        detectHelp
                        .text(text)
                    )
                         .appendTo(tips);
                }
             );
             );


             const variableContainer =
             section.append(tips);
                $('<div>')
            this.workspace.append(section);
                    .addClass(
        },
                        'preload-editor-variables'
                    );


             variablesSection.append(
        setStatus(
                 variableContainer
            message,
            type
        ) {
             if (!this.status) {
                return;
            }
 
            this.status.removeClass(
                 'is-error',
                'is-success',
                'is-warning',
                'is-loading'
             );
             );


             const configurationSection =
             if (type) {
                 this.createSection(
                 this.status.addClass(
                     'Variable configuration'
                     'is-' + type
                 );
                 );
            }


             const configurationContainer =
             this.status.text(
                $('<div>')
                message || ''
                    .addClass(
            );
                        'preload-editor-variable-configuration'
        },
                    );


            configurationSection.append(
        // =========================================================
                configurationContainer
        // Load / create
            );
        // =========================================================


             const configSection =
        async loadExisting(title) {
                 this.createSection(
             title =
                    'Configuration'
                 String(title || '').trim();
                );


             const configHelp =
             if (!title) {
                $('<div>')
                 mw.notify(
                    .addClass(
                     'Enter a preload page title first.',
                        'preload-editor-help'
                     {
                    )
                        type: 'warn'
                    .text(
                     }
                        'Review the exact config.json that will be uploaded to the wiki.'
                    );
 
            const reviewConfigButton =
                 this.createButton(
                     'Review config.json',
                     '',
                     () =>
                        this.createConfigReviewDialog()
                 );
                 );


            configSection.append(
                 return;
                 configHelp,
             }
                reviewConfigButton
             );


             right.append(
             this.setStatus(
                 analysisSection,
                 'Loading preload...',
                variablesSection,
                 'loading'
                configurationSection,
                 configSection
             );
             );


             layout.append(
             try {
                 left,
                 const page =
                right
                    await this.getPage(title);
            );


            const saveBar =
                if (!page) {
                $('<div>')
                    const suggestions =
                     .addClass(
                        await this.findSimilarPreloads(
                         'preload-editor-save-bar'
                            title
                        );
 
                     this.renderMissingPage(
                         title,
                        suggestions
                     );
                     );


            saveBar.append(
                    this.setStatus(
                this.createButton(
                        'Preload not found.',
                    'Save Preload',
                        'warning'
                    '',
                     );
                     () =>
                        this.savePreloadOnly()
                ),


                this.createButton(
                     return;
                    'Save Config',
                 }
                     '',
                    () =>
                        this.saveConfigOnly()
                 ),


                 this.createButton(
                 await this.loadDefinition(
                     'Save',
                     page.title,
                     'preload-editor-button-primary',
                     page.content
                    () =>
                 );
                        this.saveAll()
                 )
            );


            this.root
                this.setStatus(
                .find('.preload-editor-footer')
                    'Preload loaded.',
                 .append(saveBar);
                    'success'
                );
            } catch (error) {
                 console.error(
                    'Preload Editor:',
                    error
                );


            this.workspace.append(
                this.setStatus(
                 layout
                    error.message ||
             );
                        'Could not load preload.',
                    'error'
                 );
             }
        },


             this.reanalyze();
        async findSimilarPreloads(title) {
             let query =
                title
                    .replace(
                        /\/Preload$/i,
                        ''
                    )
                    .trim();


             this.renderVariables(
             if (!query) {
                 variableContainer
                 query = title;
             );
             }


             this.renderAnalysis(
             try {
                analysisContainer
                const results =
            );
                    await this.searchPages(query);


            this.renderVariableConfiguration(
                return results.filter(
                 configurationContainer
                    result =>
             );
                        /\/Preload$/i.test(
                            result.title
                        )
                 );
             } catch (error) {
                console.error(
                    'Preload Editor search:',
                    error
                );


            this.updateEditorTools();
                return [];
            this.updateLocalDraftIndicator();
             }
 
             this.mountCodeMirror(
                textarea.get(0)
            ).catch(
                error => {
                    console.error(
                        'Preload Editor CodeMirror:',
                        error
                    );
 
                    this.setStatus(
                        error.message ||
                            'Could not initialize CodeMirror.',
                        'error'
                    );
                }
            );
         },
         },


         // =========================================================
         renderMissingPage(
         // Variable detection
            title,
        // =========================================================
            suggestions
         ) {
            this.workspace.empty();


        detectVariables() {
             const section =
             const source =
                 this.createSection(
                 this.getEditorContent();
                    'Preload not found'
                );


             this.state.preloadContent =
             $('<p>')
                 source;
                .text(
                    'No preload exists at "' +
                    title +
                    '".'
                )
                 .appendTo(section);


             const content =
             if (suggestions.length) {
                 this.extractIncludeonly(
                 $('<h3>')
                     source
                    .text('Similar preloads')
                );
                     .appendTo(section);
 
 
            const analysis =
                const list =
                this.analyzePreload(
                    $('<div>')
                    content
                        .addClass(
                );
                            'preload-editor-suggestions'
                        );


            /*
                 suggestions.forEach(
            * If a variable was previously deleted from
                     result => {
            * the configuration, detecting the current
                         list.append(
            * preload again should allow it to come back.
                             this.createButton(
            */
                                 result.title,
            if (
                                'preload-editor-suggestion',
                 Array.isArray(
                                () =>
                     this.state.deletedVariables
                                     this.loadExisting(
                )
                                         result.title
            ) {
                                     )
                const detected =
                    new Set(
                         analysis.variables
                             .map(
                                 name =>
                                     String(
                                         name
                                     ).toUpperCase()
                             )
                             )
                     );
                        );
                     }
                );


                 this.state.deletedVariables =
                 section.append(list);
                    this.state.deletedVariables
                        .filter(
                            name =>
                                !detected.has(
                                    String(
                                        name
                                    ).toUpperCase()
                                )
                        );
            } else {
                this.state.deletedVariables = [];
             }
             }


             this.markDirty();
             section.append(
                this.createButton(
                    'Create this preload',
                    'preload-editor-button-primary',
                    () =>
                        this.startNew(title)
                )
            );


             this.reanalyze();
             this.workspace.append(section);
        },


            this.renderVariablesBySelector();
        async startNew(title) {
             this.renderVariableConfigurationBySelector();
             title =
            this.renderAnalysisBySelector();
                String(title || '').trim();
            this.updateEditorTools();


             this.saveDraft();
             this.state = {
                preloadTitle: title,
                preloadSource: '',
                preloadContent:
                    this.defaults.preload,


            this.setStatus(
                config:
                'Variables detected from the current preload.',
                    this.emptyConfig(),
                'success'
            );
        },


        reanalyze() {
                variables: ['PAGE'],
             const source =
                conditions: [],
                this.getEditorContent();
                selectedVariable: 'PAGE',
                deletedVariables: [],
                mode: 'new',
                dirty: true,
                dirtyPreload: true,
                dirtyConfig: true,
 
                localDraftSavedAt: null,
                localDraftAvailable: false
            };
 
            this.saveDraft();
             this.renderEditor();


             const content =
             this.setStatus(
                 this.extractIncludeonly(source);
                 'Creating a new preload.',
                'success'
            );
        },


             const analysis =
        async loadSamplePage() {
                 this.analyzePreload(
             const result =
                     content
                 await this.createPromptDialog(
                     'Load Sample Page',
                    'Enter the title of a normal wiki page to use as the starting point.'
                 );
                 );


             this.state.preloadContent =
             if (!result) {
                 source;
                 return;
            }


             this.state.variables =
             this.setStatus(
                 analysis.variables;
                'Loading sample page...',
                 'loading'
            );


             this.state.conditions =
             try {
                analysis.conditions;
                const page =
                    await this.getPage(result);


            this.state.config =
                 if (!page) {
                 this.prepareConfigForVariables(
                     throw new Error(
                     this.state.config ||
                        'Sample page not found: ' +
                         this.emptyConfig(),
                         result
                     analysis
                     );
                 );
                 }


            this.analysis =
                const sample =
                analysis;
                    this.convertSampleToPreload(
                        page.content
                    );


            const available =
                this.state = {
                ['PAGE'].concat(
                    preloadTitle: '',
                     analysis.variables.filter(
                     preloadSource: '',
                        name =>
                     preloadContent: sample,
                            name !== 'PAGE'
                     )
                );


            if (
                    config:
                !available.includes(
                        this.emptyConfig(),
                    this.state.selectedVariable
                )
            ) {
                this.state.selectedVariable =
                    available[0] ||
                    'PAGE';
            }
        },


        // =========================================================
                    variables: ['PAGE'],
        // Analysis
                    conditions: [],
        // =========================================================
                    selectedVariable: 'PAGE',
                    deletedVariables: [],
                    mode: 'new',
                    dirty: true,
                    dirtyPreload: true,
                    dirtyConfig: true,
 
                    localDraftSavedAt: null,
                    localDraftAvailable: false
                };


        renderAnalysis(container) {
                this.reanalyze();
            container.empty();
                this.saveDraft();
                this.renderEditor();


            const analysis =
                 this.setStatus(
                 this.analysis ||
                    'Sample page loaded.',
                this.analyzePreload(
                     'success'
                     this.extractIncludeonly(
                );
                        this.getEditorContent()
            } catch (error) {
                     )
                console.error(
                     'Preload Editor sample:',
                    error
                 );
                 );


            const variableCount =
                this.setStatus(
                analysis.variables.length;
                    error.message ||
                        'Could not load sample page.',
                    'error'
                );
            }
        },


             const conditionCount =
        convertSampleToPreload(source) {
                 analysis.conditions.length;
             let content =
                 String(source || '').trim();


             const configuredCount =
             const include =
                 Object.keys(
                 content.match(
                     this.state.config &&
                     /<includeonly>([\s\S]*?)<\/includeonly>/i
                    this.state.config.fields
                 );
                        ? this.state.config.fields
                        : {}
                 ).length;


             const summary =
             if (include) {
                 $('<div>')
                 content =
                     .addClass(
                     include[1].trim();
                        'preload-editor-analysis-summary'
            }
                    );


             const createMetric =
             content =
                 (
                 content
                    value,
                     .replace(
                     label
                         /<noinclude>[\s\S]*?<\/noinclude>/gi,
                ) =>
                        ''
                    $('<div>')
                    )
                        .addClass(
                    .trim();
                            'preload-editor-analysis-metric'
                         )
                        .append(
                            $('<strong>')
                                .addClass(
                                    'preload-editor-analysis-value'
                                )
                                .text(value),


                            $('<span>')
            return (
                                .addClass(
                '<includeonly>\n' +
                                    'preload-editor-analysis-label'
                content +
                                )
                '\n</includeonly>\n' +
                                .text(label)
                '<noinclude>{{Documentation|type=Preload|content={{Preload Template}}}}</noinclude>\n'
                        );
            );
        },


            summary.append(
        async loadDefinition(
                createMetric(
            title,
                    variableCount,
            source
                    variableCount === 1
        ) {
                        ? 'variable'
            const content =
                        : 'variables'
                 this.extractIncludeonly(source);
                 ),


                createMetric(
            let config =
                    conditionCount,
                 this.emptyConfig();
                    conditionCount === 1
                        ? 'condition marker'
                        : 'condition markers'
                 ),


                 createMetric(
            const configTitle =
                     configuredCount,
                 title.replace(
                     configuredCount === 1
                     /\/+$/,
                        ? 'configured field'
                     ''
                        : 'configured fields'
                 ) +
                 )
                '/config.json';
            );


             container.append(summary);
             const configPage =
                await this.getPage(configTitle);


             if (
             if (configPage) {
                analysis.conditionVariables.length
                 try {
            ) {
                     config =
                 container.append(
                         JSON.parse(
                     $('<div>')
                             configPage.content
                         .addClass(
                         );
                             'preload-editor-analysis-note'
                } catch (error) {
                         )
                    throw new Error(
                        .text(
                        'The existing config.json contains invalid JSON: ' +
                            'Condition variables: ' +
                        error.message
                            analysis.conditionVariables.join(', ')
                    );
                        )
                 }
                 );
             }
             }


             const validation =
             const analysis =
                 this.validateConfig(
                 this.analyzePreload(content);
                    this.state.config,
 
            this.state.deletedVariables = [];
 
            config =
                this.prepareConfigForVariables(
                    config,
                     analysis
                     analysis
                 );
                 );


             if (validation.errors.length) {
             this.state = {
                 const errors =
                 preloadTitle: title,
                    $('<ul>')
                preloadSource: source,
                        .addClass(
                preloadContent: source,
                            'preload-editor-validation-errors'
                config,
                        );


                 validation.errors.forEach(
                 variables:
                     error => {
                    analysis.variables,
                         $('<li>')
 
                            .text(error)
                conditions:
                             .appendTo(errors);
                    analysis.conditions,
                    }
 
                );
                selectedVariable:
                     analysis.variables.includes('PAGE')
                         ? 'PAGE'
                        : (
                             analysis.variables[0] ||
                            'PAGE'
                        ),


                 container.append(errors);
                 deletedVariables: [],
            }


            if (validation.warnings.length) {
                 mode: 'edit',
                 const warnings =
                dirty: false,
                    $('<ul>')
                dirtyPreload: false,
                        .addClass(
                dirtyConfig: false,
                            'preload-editor-validation-warnings'
                        );


                 validation.warnings.forEach(
                 localDraftSavedAt: null,
                    warning => {
                localDraftAvailable: false
                        $('<li>')
            };
                            .text(warning)
                            .appendTo(warnings);
                    }
                );


                container.append(warnings);
             this.clearDraft(title);
            }
             this.renderEditor();
 
         },
            if (
                !validation.errors.length &&
                !validation.warnings.length
            ) {
                container.append(
                    $('<div>')
                        .addClass(
                            'preload-editor-validation-success'
                        )
                        .text(
                            'Preload and configuration are in sync.'
                        )
                );
             }
        },
 
        renderAnalysisBySelector() {
            const container =
                this.root &&
                this.root.find(
                    '.preload-editor-analysis'
                );
 
             if (
                container &&
                container.length
            ) {
                this.renderAnalysis(container);
            }
         },


         // =========================================================
         // =========================================================
         // Variables
         // Editor
         // =========================================================
         // =========================================================


         renderVariables(container) {
         renderEditor() {
             container.empty();
             this.destroyEditor();
            this.workspace.empty();


             const analysis =
             const layout =
                 this.analysis ||
                 $('<div>')
                this.analyzePreload(
                     .addClass(
                     this.extractIncludeonly(
                         'preload-editor-layout'
                         this.getEditorContent()
                     );
                     )
                );


             const variables =
             const left =
                 analysis.variables.slice();
                 $('<div>')
                    .addClass(
                        'preload-editor-left'
                    );


             const ordered =
             const right =
                 ['PAGE'];
                 $('<div>')
                    .addClass(
                        'preload-editor-right'
                    );


             variables.forEach(
             const editorSection =
                name => {
                this.createSection(
                     if (
                     'Preload',
                        name !== 'PAGE' &&
                     'preload-editor-main-section'
                        !ordered.includes(name)
                 );
                     ) {
                        ordered.push(name);
                    }
                 }
            );


             const configuredFields =
             const meta =
                 this.state.config &&
                 $('<div>')
                this.state.config.fields
                     .addClass(
                     ? Object.keys(
                         'preload-editor-meta'
                         this.state.config.fields
                     );
                     )
                    : [];


             configuredFields.forEach(
             $('<strong>')
                name => {
                .text(
                     const upper =
                    this.state.preloadTitle ||
                        name.toUpperCase();
                     'New preload'
                )
                .appendTo(meta);


                    if (
            if (
                        upper !== 'PAGE' &&
                this.state.mode === 'new'
                         !ordered.includes(upper)
            ) {
                     ) {
                $('<span>')
                        ordered.push(upper);
                    .addClass(
                    }
                         'preload-editor-badge'
                 }
                    )
                    .text('NEW')
                     .appendTo(meta);
            }
 
            editorSection.append(meta);
            editorSection.append(
                 this.renderEditorTools()
             );
             );


             const deleted =
             const textarea =
                 Array.isArray(
                 this.textarea(
                     this.state.deletedVariables
                     this.state.preloadContent
                 )
                 )
                     ? this.state.deletedVariables
                     .attr(
                     : [];
                        'id',
                        'preload-editor-content'
                    )
                    .addClass(
                        'preload-editor-content'
                     );


             deleted.forEach(
             const editorMount =
                 name => {
                 $('<div>')
                     const upper =
                     .addClass(
                         String(name).toUpperCase();
                         'preload-editor-codemirror-mount'
                    )
                    .append(textarea);


                    if (
            editorSection.append(editorMount);
                        upper !== 'PAGE' &&
            left.append(editorSection);
                        !ordered.includes(upper)
 
                    ) {
            const analysisSection =
                        ordered.push(upper);
                 this.createSection('Analysis');
                    }
                 }
            );


             const list =
             const analysisContainer =
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-variable-list'
                         'preload-editor-analysis'
                     );
                     );


             ordered.forEach(
             analysisSection.append(
                 name => {
                 analysisContainer
                    const isSelected =
            );
                        name ===
                        this.state.selectedVariable;


                    const isUsed =
            const variablesSection =
                        analysis.variables.includes(
                this.createSection('Variables');
                            name
                        );


                    const isConfigured =
            const detectVariablesButton =
                        !!(
                this.createButton(
                            this.state.config &&
                    'Detect variables',
                            this.state.config.fields &&
                    'preload-editor-button-primary preload-editor-detect-variables',
                            this.state.config.fields[name]
                    () =>
                        );
                        this.detectVariables()
                );


                    const item =
            const detectHelp =
                        $('<button>')
                $('<div>')
                            .attr({
                    .addClass(
                                type: 'button',
                        'preload-editor-help preload-editor-detect-variables-help'
                                'data-variable':
                    )
                                    name
                    .text(
                            })
                        'Refresh the variable list after editing the preload.'
                            .addClass(
                    );
                                'preload-editor-variable-item'
                            )
                            .toggleClass(
                                'is-selected',
                                isSelected
                            )
                            .on(
                                'click.preloadEditor',
                                () =>
                                    this.selectVariable(
                                        name
                                    )
                            );


                    $('<span>')
            variablesSection.append(
                        .addClass(
                $('<div>')
                            'preload-editor-variable-name'
                    .addClass(
                        )
                        'preload-editor-variable-detect'
                        .text(name)
                    )
                        .appendTo(item);
                    .append(
                        detectVariablesButton,
                        detectHelp
                    )
            );


                    if (name === 'PAGE') {
            const variableContainer =
                        item.append(
                $('<div>')
                            $('<span>')
                    .addClass(
                                .addClass(
                         'preload-editor-variables'
                                    'preload-editor-badge'
                     );
                                )
                                .text('AUTOMATIC')
                        );
                    } else if (!isConfigured) {
                         item.append(
                            $('<span>')
                                .addClass(
                                    'preload-editor-badge is-warning'
                                )
                                .text(
                                    isUsed
                                        ? 'NOT CONFIGURED'
                                        : 'DELETED'
                                )
                        );
                     } else if (!isUsed) {
                        item.append(
                            $('<span>')
                                .addClass(
                                    'preload-editor-badge is-warning'
                                )
                                .text('UNUSED')
                        );
                    }


                    list.append(item);
            variablesSection.append(
                 }
                 variableContainer
             );
             );


             container.append(list);
             const configurationSection =
                this.createSection(
                    'Variable configuration'
                );


             if (!ordered.length) {
             /*
                 container.append(
            * Import button lives at the top of the
                     $('<div>')
            * variable configuration section so the
                        .addClass(
            * workflow is always easy to find.
                            'preload-editor-empty'
            */
                        )
            const importDefinitionsButton =
                         .text(
                 this.createButton(
                            'No variables found in the preload.'
                     'Import definitions',
                        )
                    'preload-editor-button-primary preload-editor-import-definitions',
                 );
                    () =>
            }
                         this.importVariableDefinitions()
        },
                 );


        renderVariablesBySelector() {
             const importHelp =
             const container =
                 $('<div>')
                 this.root &&
                    .addClass(
                this.root.find(
                        'preload-editor-help preload-editor-import-definitions-help'
                    '.preload-editor-variables'
                    )
                );
                    .text(
                        'Import variable definitions from another preload config.json. Existing definitions can be replaced.'
                    );


             if (
             const importControls =
                 container &&
                 $('<div>')
                container.length
                    .addClass(
            ) {
                        'preload-editor-import-definition-controls'
                this.renderVariables(container);
                    )
            }
                    .append(
        },
                        importDefinitionsButton,
                        importHelp
                    );


        selectVariable(name) {
            configurationSection.append(
            name =
                 importControls
                 String(name || '')
            );
                    .trim()
                    .toUpperCase();


             if (!name) {
             const configurationContainer =
                return;
                $('<div>')
            }
                    .addClass(
                        'preload-editor-variable-configuration'
                    );


             const available =
             configurationSection.append(
                 ['PAGE'].concat(
                 configurationContainer
                    (
            );
                        this.analysis &&
 
                        this.analysis.variables
            const configSection =
                            ? this.analysis.variables
                this.createSection(
                            : []
                     'Configuration'
                     ).filter(
                        variable =>
                            variable !== 'PAGE'
                    )
                 );
                 );


             const configuredUnused =
             const configHelp =
                 this.state.config &&
                 $('<div>')
                this.state.config.fields
                     .addClass(
                     ? Object.keys(
                         'preload-editor-help'
                         this.state.config.fields
                     )
                     )
                        .map(
                    .text(
                            variable =>
                        'Review the exact config.json that will be uploaded to the wiki.'
                                variable.toUpperCase()
                    );
                        )
                    : [];


             configuredUnused.forEach(
             const reviewConfigButton =
                variable => {
                this.createButton(
                     if (
                     'Review config.json',
                        !available.includes(variable) &&
                    '',
                        variable !== 'PAGE'
                     () =>
                     ) {
                         this.createConfigReviewDialog()
                         available.push(variable);
                );
                    }
 
                 }
            configSection.append(
                configHelp,
                 reviewConfigButton
             );
             );


             if (!available.includes(name)) {
             right.append(
                 return;
                 analysisSection,
             }
                variablesSection,
                configurationSection,
                configSection
             );


             this.state.selectedVariable =
             layout.append(
                 name;
                left,
                 right
            );


             this.renderVariablesBySelector();
             const saveBar =
            this.renderVariableConfigurationBySelector();
                $('<div>')
        },
                    .addClass(
                        'preload-editor-save-bar'
                    );


        renderVariableConfigurationBySelector() {
             saveBar.append(
             const container =
                 this.createButton(
                this.root &&
                     'Save Preload',
                 this.root.find(
                    '',
                     '.preload-editor-variable-configuration'
                    () =>
                 );
                        this.savePreloadOnly()
                 ),


            if (
                this.createButton(
                container &&
                    'Save Config',
                container.length
                    '',
            ) {
                    () =>
                this.renderVariableConfiguration(
                        this.saveConfigOnly()
                    container
                 ),
                 );
            }
        },


        renderVariableConfiguration(container) {
                this.createButton(
            container.empty();
                    'Save',
                    'preload-editor-button-primary',
                    () =>
                        this.saveAll()
                )
            );


             const analysis =
             this.root
                this.analysis ||
                 .find('.preload-editor-footer')
                 this.analyzePreload(
                .append(saveBar);
                    this.extractIncludeonly(
                        this.getEditorContent()
                    )
                );


             const name =
             this.workspace.append(
                this.state.selectedVariable ||
                 layout
                 'PAGE';
            );


             const fields =
             this.reanalyze();
                this.state.config &&
                this.state.config.fields
                    ? this.state.config.fields
                    : {};


             const field =
             this.renderVariables(
                 fields[name];
                 variableContainer
            );


             if (name === 'PAGE') {
             this.renderAnalysis(
                container.append(
                analysisContainer
                    this.createAutomaticPageCard(
            );
                        field ||
                        this.getAutomaticPageField()
                    )
                );


                 return;
            this.renderVariableConfiguration(
             }
                 configurationContainer
             );


             if (!field) {
             this.updateEditorTools();
                const missing =
            this.updateLocalDraftIndicator();
                    $('<div>')
                        .addClass(
                            'preload-editor-variable preload-editor-variable-missing'
                        );


                 $('<div>')
            this.mountCodeMirror(
                    .addClass(
                 textarea.get(0)
                        'preload-editor-variable-header'
            ).catch(
                    )
                error => {
                     .append(
                     console.error(
                         $('<h3>')
                         'Preload Editor CodeMirror:',
                            .text(name),
                        error
                    );


                        $('<span>')
                    this.setStatus(
                            .addClass(
                        error.message ||
                                'preload-editor-badge is-warning'
                             'Could not initialize CodeMirror.',
                            )
                         'error'
                            .text(
                     );
                                'NOT CONFIGURED'
                 }
                             )
                    )
                    .appendTo(missing);
 
                $('<p>')
                    .addClass(
                        'preload-editor-analysis-note'
                    )
                    .text(
                         'This variable is currently not present in config.json.'
                     )
                    .appendTo(missing);
 
                container.append(missing);
                 return;
            }
 
            container.append(
                this.createVariableCard(
                    name,
                    field,
                    analysis
                )
             );
             );
         },
         },


         createAutomaticPageCard(field) {
         // =========================================================
            const card =
        // Variable detection
                $('<div>')
        // =========================================================
                    .addClass(
                        'preload-editor-variable preload-editor-variable-automatic'
                    )
                    .attr(
                        'data-variable',
                        'PAGE'
                    );


             const header =
        detectVariables() {
                 $('<div>')
             const source =
                    .addClass(
                 this.getEditorContent();
                        'preload-editor-variable-header'
                    );


             header.append(
             this.state.preloadContent =
                 $('<h3>').text('PAGE'),
                 source;


                 $('<span>')
            const content =
                    .addClass(
                 this.extractIncludeonly(
                        'preload-editor-badge'
                     source
                     )
                );
                    .text('AUTOMATIC')
            );


             card.append(header);
             const analysis =
                this.analyzePreload(
                    content
                );


             $('<p>')
             /*
                 .addClass(
            * If a variable was previously deleted from
                     'preload-editor-analysis-note'
            * the configuration, detecting the current
            * preload again should allow it to come back.
            */
            if (
                 Array.isArray(
                     this.state.deletedVariables
                 )
                 )
                 .text(
            ) {
                    'PAGE is a precompiled automatic variable. It is always available and is not edited like normal variables.'
                 const detected =
                )
                    new Set(
                .appendTo(card);
                        analysis.variables
                            .map(
                                name =>
                                    String(
                                        name
                                    ).toUpperCase()
                            )
                    );


            const label =
                this.state.deletedVariables =
                this.textInput(
                    this.state.deletedVariables
                    field.label ||
                        .filter(
                    'Page Name'
                            name =>
                ).prop(
                                !detected.has(
                    'disabled',
                                    String(
                    true
                                        name
                 );
                                    ).toUpperCase()
                                )
                        );
            } else {
                 this.state.deletedVariables = [];
            }


             const description =
             this.markDirty();
                this.textarea(
                    field.description ||
                    'Name of the page.'
                )
                    .addClass(
                        'preload-editor-small-textarea'
                    )
                    .prop(
                        'disabled',
                        true
                    );


             const type =
             this.reanalyze();
                this.textInput(
                    field.type || 'page'
                ).prop(
                    'disabled',
                    true
                );


             const required =
             this.renderVariablesBySelector();
                this.checkbox(
            this.renderVariableConfigurationBySelector();
                    field.required !== false
            this.renderAnalysisBySelector();
                ).prop(
            this.updateEditorTools();
                    'disabled',
                    true
                );


             const namespace =
             this.saveDraft();
                this.textInput(
                    field.namespace || 'Main'
                ).prop(
                    'disabled',
                    true
                );


             const allowNew =
             this.setStatus(
                this.checkbox(
                'Variables detected from the current preload.',
                    field['allow-new'] === true
                 'success'
                 ).prop(
            );
                    'disabled',
        },
                    true
                );


             card.append(
        reanalyze() {
                 this.fieldRow(
             const source =
                    'Label',
                 this.getEditorContent();
                    label
                ),


                 this.fieldRow(
            const content =
                    'Description',
                 this.extractIncludeonly(source);
                    description
                ),


                 this.fieldRow(
            const analysis =
                     'Type',
                 this.analyzePreload(
                    type
                     content
                 ),
                 );


                this.fieldRow(
            this.state.preloadContent =
                    'Required',
                source;
                    required
 
                 ),
            this.state.variables =
                 analysis.variables;


                this.fieldRow(
            this.state.conditions =
                    'Namespace',
                 analysis.conditions;
                    namespace
                 ),


                 this.fieldRow(
            this.state.config =
                     'Allow new page',
                 this.prepareConfigForVariables(
                     allowNew
                     this.state.config ||
                 )
                        this.emptyConfig(),
            );
                     analysis
                 );


             return card;
             this.analysis =
        },
                analysis;


        createVariableCard(
             const available =
            name,
                 ['PAGE'].concat(
            field,
                     analysis.variables.filter(
            analysis
                         name =>
        ) {
                            name !== 'PAGE'
             const card =
                 $('<div>')
                     .addClass(
                         'preload-editor-variable'
                     )
                     )
                    .attr(
                 );
                        'data-variable',
                        name
                    );
 
            const header =
                 $('<div>')
                    .addClass(
                        'preload-editor-variable-header'
                    );
 
            header.append(
                $('<h3>').text(name)
            );


             if (
             if (
                 !analysis.variables.includes(name)
                 !available.includes(
                    this.state.selectedVariable
                )
             ) {
             ) {
                 header.append(
                 this.state.selectedVariable =
                     $('<span>')
                     available[0] ||
                        .addClass(
                    'PAGE';
                            'preload-editor-badge is-warning'
                        )
                        .text('UNUSED')
                );
             }
             }
        },
        // =========================================================
        // Analysis
        // =========================================================


             card.append(header);
        renderAnalysis(container) {
             container.empty();


             const label =
             const analysis =
                 this.textInput(
                 this.analysis ||
                     field.label || ''
                this.analyzePreload(
                     this.extractIncludeonly(
                        this.getEditorContent()
                    )
                 );
                 );


             label.on(
             const variableCount =
                 'input.preloadEditor',
                analysis.variables.length;
                 () => {
 
                     field.label =
            const conditionCount =
                         label.val();
                 analysis.conditions.length;
 
            const configuredCount =
                 Object.keys(
                     this.state.config &&
                    this.state.config.fields
                         ? this.state.config.fields
                        : {}
                ).length;


                    this.markDirty();
            const summary =
                     this.renderConfigReviewAvailability();
                $('<div>')
                }
                     .addClass(
            );
                        'preload-editor-analysis-summary'
                    );


             card.append(
             const createMetric =
                 this.fieldRow(
                 (
                     'Label',
                     value,
                     label
                     label
                 )
                 ) =>
            );
                    $('<div>')
                        .addClass(
                            'preload-editor-analysis-metric'
                        )
                        .append(
                            $('<strong>')
                                .addClass(
                                    'preload-editor-analysis-value'
                                )
                                .text(value),


            const description =
                            $('<span>')
                this.textarea(
                                .addClass(
                    field.description || ''
                                    'preload-editor-analysis-label'
                ).addClass(
                                )
                    'preload-editor-small-textarea'
                                .text(label)
                );
                        );


             description.on(
             summary.append(
                 'input.preloadEditor',
                 createMetric(
                () => {
                    variableCount,
                    field.description =
                    variableCount === 1
                         description.val();
                        ? 'variable'
                         : 'variables'
                ),


                     this.markDirty();
                createMetric(
                     this.renderConfigReviewAvailability();
                     conditionCount,
                 }
                     conditionCount === 1
            );
                        ? 'condition marker'
                        : 'condition markers'
                 ),


            card.append(
                createMetric(
                this.fieldRow(
                    configuredCount,
                     'Description',
                     configuredCount === 1
                    description
                        ? 'configured field'
                        : 'configured fields'
                 )
                 )
             );
             );


             const types = [
             container.append(summary);
                ['text', 'Text'],
                ['textarea', 'Textarea'],
                ['number', 'Number'],
                ['boolean', 'Boolean'],
                ['select', 'Select'],
                ['multiselect', 'Multi-select'],
                ['page', 'Page'],
                ['user', 'User'],
                ['date', 'Date']
            ];


             const typeSelect =
             if (
                 this.select(
                analysis.conditionVariables.length
                     types.map(
            ) {
                         ([value, label]) => ({
                 container.append(
                             value,
                     $('<div>')
                            label
                         .addClass(
                         })
                             'preload-editor-analysis-note'
                    ),
                        )
                    field.type || 'text'
                         .text(
                            'Condition variables: ' +
                            analysis.conditionVariables.join(', ')
                        )
                 );
                 );
            }


             typeSelect.on(
             const validation =
                'change.preloadEditor',
                this.validateConfig(
                () => {
                    this.state.config,
                     field.type =
                     analysis
                        typeSelect.val();
                );


                     this.markDirty();
            if (validation.errors.length) {
                const errors =
                     $('<ul>')
                        .addClass(
                            'preload-editor-validation-errors'
                        );


                    this.renderVariableConfigurationBySelector();
                validation.errors.forEach(
                     this.renderConfigReviewAvailability();
                     error => {
                }
                        $('<li>')
            );
                            .text(error)
 
                            .appendTo(errors);
            card.append(
                     }
                this.fieldRow(
                    'Type',
                    typeSelect
                )
            );
 
            const required =
                this.checkbox(
                     field.required === true
                 );
                 );


            required.on(
                 container.append(errors);
                 'change.preloadEditor',
            }
                () => {
                    field.required =
                        required.prop('checked');


                     this.markDirty();
            if (validation.warnings.length) {
                    this.renderConfigReviewAvailability();
                const warnings =
                }
                     $('<ul>')
            );
                        .addClass(
                            'preload-editor-validation-warnings'
                        );


            card.append(
                validation.warnings.forEach(
                this.fieldRow(
                     warning => {
                     'Required',
                        $('<li>')
                    required
                            .text(warning)
                )
                            .appendTo(warnings);
            );
                     }
 
            const placeholder =
                this.textInput(
                     field.placeholder || ''
                 );
                 );


            placeholder.on(
                 container.append(warnings);
                 'input.preloadEditor',
            }
                () => {
                    const value =
                        placeholder.val();


                    if (value) {
            if (
                         field.placeholder =
                !validation.errors.length &&
                             value;
                !validation.warnings.length
                    } else {
            ) {
                         delete field.placeholder;
                container.append(
                    }
                    $('<div>')
                         .addClass(
                             'preload-editor-validation-success'
                        )
                         .text(
                            'Preload and configuration are in sync.'
                        )
                );
            }
        },


                    this.markDirty();
        renderAnalysisBySelector() {
                     this.renderConfigReviewAvailability();
            const container =
                 }
                this.root &&
            );
                this.root.find(
                     '.preload-editor-analysis'
                 );


             card.append(
             if (
                 this.fieldRow(
                 container &&
                    'Placeholder',
                 container.length
                    placeholder
             ) {
                 )
                 this.renderAnalysis(container);
             );
            }
 
        },
            const defaultInput =
                 this.textInput(
                    field.default === undefined
                        ? ''
                        : field.default
                );


            defaultInput.on(
        // =========================================================
                'input.preloadEditor',
        // Variables
                () => {
        // =========================================================
                    const value =
                        defaultInput.val();


                    if (value) {
        renderVariables(container) {
                        field.default =
            container.empty();
                            value;
                    } else {
                        delete field.default;
                    }


                     this.markDirty();
            const analysis =
                    this.renderConfigReviewAvailability();
                this.analysis ||
                 }
                this.analyzePreload(
            );
                     this.extractIncludeonly(
                        this.getEditorContent()
                    )
                 );


             card.append(
             const variables =
                 this.fieldRow(
                 analysis.variables.slice();
                    'Default',
                    defaultInput,
                    'For text-like fields this is used as a placeholder. For select fields it is the empty/default option.'
                )
            );


             if (
             const ordered =
                field.type === 'textarea'
                 ['PAGE'];
            ) {
                 const maxRows =
                    this.textInput(
                        field.maxRows || ''
                    ).attr(
                        'type',
                        'number'
                    );


                maxRows.on(
            variables.forEach(
                    'input.preloadEditor',
                name => {
                     () => {
                     if (
                         const value =
                         name !== 'PAGE' &&
                            parseInt(
                         !ordered.includes(name)
                                maxRows.val(),
                    ) {
                                10
                         ordered.push(name);
                            );
 
                         if (
                            Number.isFinite(value) &&
                            value > 0
                        ) {
                            field.maxRows =
                                value;
                        } else {
                            delete field.maxRows;
                        }
 
                        this.markDirty();
                         this.renderConfigReviewAvailability();
                     }
                     }
                 );
                 }
            );


                 card.append(
            const configuredFields =
                     this.fieldRow(
                 this.state.config &&
                         'Maximum rows',
                this.state.config.fields
                        maxRows
                     ? Object.keys(
                         this.state.config.fields
                     )
                     )
                );
                    : [];
            }


             if (
             configuredFields.forEach(
                 field.type === 'select' ||
                 name => {
                field.type === 'multiselect'
                    const upper =
            ) {
                        name.toUpperCase();
                this.renderSelectConfiguration(
                    card,
                    field
                );
            }


            const deleteButton =
                    if (
                this.createButton(
                        upper !== 'PAGE' &&
                    'Delete variable',
                        !ordered.includes(upper)
                    'preload-editor-button-danger preload-editor-delete-variable',
                     ) {
                     async () => {
                         ordered.push(upper);
                         const confirmed =
                    }
                            await this.createConfirmDialog(
                }
                                'Delete variable',
            );
                                'Delete "' +
                                name +
                                '" from the configuration?\n\nThe variable will remain in the preload, but its config.json field will be removed.',
                                'Delete variable',
                                'Cancel'
                            );


                        if (!confirmed) {
            const deleted =
                            return;
                Array.isArray(
                         }
                    this.state.deletedVariables
 
                )
                         this.deleteVariable(name);
                    ? this.state.deletedVariables
                    : [];
 
            deleted.forEach(
                name => {
                    const upper =
                        String(name).toUpperCase();
 
                    if (
                         upper !== 'PAGE' &&
                        !ordered.includes(upper)
                    ) {
                         ordered.push(upper);
                     }
                     }
                 );
                 }
            );


             card.append(
             const list =
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-variable-delete'
                         'preload-editor-variable-list'
                     )
                     );
                    .append(deleteButton)
            );


             return card;
             ordered.forEach(
        },
                name => {
                    const isSelected =
                        name ===
                        this.state.selectedVariable;


        async deleteVariable(name) {
                    const isUsed =
            name =
                        analysis.variables.includes(
                String(name || '')
                            name
                    .trim()
                        );
                    .toUpperCase();


            if (
                    const isConfigured =
                !name ||
                        !!(
                name === 'PAGE'
                            this.state.config &&
            ) {
                            this.state.config.fields &&
                return;
                            this.state.config.fields[name]
            }
                        );


            if (
                    const item =
                !this.state.config ||
                        $('<button>')
                !this.state.config.fields
                            .attr({
            ) {
                                type: 'button',
                return;
                                'data-variable':
            }
                                    name
                            })
                            .addClass(
                                'preload-editor-variable-item'
                            )
                            .toggleClass(
                                'is-selected',
                                isSelected
                            )
                            .on(
                                'click.preloadEditor',
                                () =>
                                    this.selectVariable(
                                        name
                                    )
                            );


            delete this.state.config.fields[name];
                    $('<span>')
                        .addClass(
                            'preload-editor-variable-name'
                        )
                        .text(name)
                        .appendTo(item);


            if (
                    if (name === 'PAGE') {
                !Array.isArray(
                        item.append(
                    this.state.deletedVariables
                            $('<span>')
                )
                                .addClass(
            ) {
                                    'preload-editor-badge'
                this.state.deletedVariables = [];
                                )
            }
                                .text('AUTOMATIC')
                        );
                    } else if (!isConfigured) {
                        item.append(
                            $('<span>')
                                .addClass(
                                    'preload-editor-badge is-warning'
                                )
                                .text(
                                    isUsed
                                        ? 'NOT CONFIGURED'
                                        : 'DELETED'
                                )
                        );
                    } else if (!isUsed) {
                        item.append(
                            $('<span>')
                                .addClass(
                                    'preload-editor-badge is-warning'
                                )
                                .text('UNUSED')
                        );
                    }


            if (
                    list.append(item);
                !this.state.deletedVariables.includes(
                 }
                    name
             );
                 )
             ) {
                this.state.deletedVariables.push(
                    name
                );
            }


             this.state.selectedVariable =
             container.append(list);
                name;


             this.markDirty();
             if (!ordered.length) {
                container.append(
                    $('<div>')
                        .addClass(
                            'preload-editor-empty'
                        )
                        .text(
                            'No variables found in the preload.'
                        )
                );
            }
        },


            this.renderVariablesBySelector();
        renderVariablesBySelector() {
             this.renderVariableConfigurationBySelector();
             const container =
            this.renderAnalysisBySelector();
                this.root &&
            this.renderConfigReviewAvailability();
                this.root.find(
                    '.preload-editor-variables'
                );


             this.setStatus(
             if (
                 'Variable "' +
                 container &&
                 name +
                 container.length
                 '" removed from the configuration.',
            ) {
                'warning'
                 this.renderVariables(container);
             );
             }
         },
         },


         // =========================================================
         selectVariable(name) {
        // Select configuration
            name =
        // =========================================================
                String(name || '')
                    .trim()
                    .toUpperCase();


        renderSelectConfiguration(
            if (!name) {
            card,
                return;
             field
             }
        ) {
 
             const source =
             const available =
                 this.textInput(
                 ['PAGE'].concat(
                     field.source || '',
                    (
                     'category:Example'
                        this.analysis &&
                        this.analysis.variables
                            ? this.analysis.variables
                            : []
                     ).filter(
                        variable =>
                            variable !== 'PAGE'
                     )
                 );
                 );


             source.on(
             const configuredUnused =
                 'input.preloadEditor',
                this.state.config &&
                () => {
                 this.state.config.fields
                    const value =
                    ? Object.keys(
                         source.val();
                        this.state.config.fields
                    )
                        .map(
                            variable =>
                                variable.toUpperCase()
                         )
                    : [];


                     if (value) {
            configuredUnused.forEach(
                         field.source = value;
                variable => {
                     } else {
                     if (
                         delete field.source;
                        !available.includes(variable) &&
                         variable !== 'PAGE'
                     ) {
                         available.push(variable);
                     }
                     }
 
                 }
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                 }
             );
             );


             card.append(
             if (!available.includes(name)) {
                this.fieldRow(
                 return;
                    'Source',
             }
                    source,
                    'Currently supported source format: category:Category Name'
                 )
             );


             const sourcePrefix =
             this.state.selectedVariable =
                this.textInput(
                 name;
                    field['source-prefix'] || ''
                 );


             sourcePrefix.on(
             this.renderVariablesBySelector();
                'input.preloadEditor',
            this.renderVariableConfigurationBySelector();
                () => {
        },
                    const value =
                        sourcePrefix.val();


                    if (value) {
        renderVariableConfigurationBySelector() {
                        field['source-prefix'] =
            const container =
                            value;
                this.root &&
                     } else {
                this.root.find(
                        delete field['source-prefix'];
                     '.preload-editor-variable-configuration'
                    }
                );


                    this.markDirty();
            if (
                    this.renderConfigReviewAvailability();
                container &&
                }
                container.length
            );
            ) {
                this.renderVariableConfiguration(
                    container
                );
            }
        },


            card.append(
        renderVariableConfiguration(container) {
                this.fieldRow(
            container.empty();
                    'Source prefix',
                    sourcePrefix,
                    'Only source values beginning with this text are included.'
                )
            );


             const excludeCategory =
             const analysis =
                 this.textInput(
                 this.analysis ||
                     field.exclude &&
                this.analyzePreload(
                    field.exclude.category ||
                     this.extractIncludeonly(
                     ''
                        this.getEditorContent()
                     )
                 );
                 );


             excludeCategory.on(
             const name =
                 'input.preloadEditor',
                this.state.selectedVariable ||
                () => {
                 'PAGE';
                    const value =
 
                        excludeCategory.val();
            const fields =
                this.state.config &&
                this.state.config.fields
                    ? this.state.config.fields
                    : {};


                    if (value) {
            const field =
                        field.exclude =
                fields[name];
                            Object.assign(
                                {},
                                field.exclude || {},
                                {
                                    category: value
                                }
                            );
                    } else if (
                        field.exclude
                    ) {
                        delete field.exclude.category;


                        if (
            if (name === 'PAGE') {
                            !Object.keys(
                container.append(
                                field.exclude
                    this.createAutomaticPageCard(
                            ).length
                         field ||
                         ) {
                         this.getAutomaticPageField()
                            delete field.exclude;
                     )
                         }
                );
                     }


                    this.markDirty();
                return;
                    this.renderConfigReviewAvailability();
            }
                }
            );


             card.append(
             if (!field) {
                 this.fieldRow(
                 const missing =
                     'Exclude category',
                     $('<div>')
                    excludeCategory
                        .addClass(
                )
                            'preload-editor-variable preload-editor-variable-missing'
            );
                        );


            card.append(
                $('<div>')
                this.createTransformControls(
                    .addClass(
                     field,
                        'preload-editor-variable-header'
                     'value',
                    )
                     'Value transformation'
                    .append(
                 ),
                        $('<h3>')
                            .text(name),
 
                        $('<span>')
                            .addClass(
                                'preload-editor-badge is-warning'
                            )
                            .text(
                                'NOT CONFIGURED'
                            )
                     )
                     .appendTo(missing);
 
                $('<p>')
                     .addClass(
                        'preload-editor-analysis-note'
                    )
                    .text(
                        'This variable is currently not present in config.json.'
                    )
                    .appendTo(missing);
 
                 container.append(missing);
                return;
            }


                 this.createTransformControls(
            container.append(
                 this.createVariableCard(
                    name,
                     field,
                     field,
                     'label',
                     analysis
                    'Label transformation'
                 )
                 )
             );
             );
        },


             const optionsValue =
        createAutomaticPageCard(field) {
                 Array.isArray(field.options)
             const card =
                     ? field.options
                 $('<div>')
                         .map(
                     .addClass(
                            option => {
                         'preload-editor-variable preload-editor-variable-automatic'
                                if (
                    )
                                    typeof option ===
                    .attr(
                                    'string'
                        'data-variable',
                                ) {
                        'PAGE'
                                    return option;
                    );
                                }
 
            const header =
                $('<div>')
                    .addClass(
                        'preload-editor-variable-header'
                    );


                                return (
            header.append(
                                    String(
                $('<h3>').text('PAGE'),
                                        option.value || ''
                                    ) +
                                    ' | ' +
                                    String(
                                        option.label ||
                                        option.value ||
                                        ''
                                    )
                                );
                            }
                        )
                        .join('\n')
                    : '';


            const options =
                 $('<span>')
                 this.textarea(
                     .addClass(
                     optionsValue
                        'preload-editor-badge'
                ).addClass(
                    )
                    'preload-editor-small-textarea'
                    .text('AUTOMATIC')
                );
            );


             options.on(
            card.append(header);
                 'input.preloadEditor',
 
                 () => {
            $('<p>')
                     const lines =
                .addClass(
                         options
                    'preload-editor-analysis-note'
                             .val()
                )
                             .split('\n')
                .text(
                             .map(
                    'PAGE is a precompiled automatic variable. It is always available and is not edited like normal variables.'
                                 line =>
                )
                                     line.trim()
                .appendTo(card);
                             )
 
                             .filter(Boolean);
            const label =
 
                this.textInput(
                     if (!lines.length) {
                    field.label ||
                         delete field.options;
                    'Page Name'
                ).prop(
                    'disabled',
                    true
                );
 
            const description =
                this.textarea(
                    field.description ||
                    'Name of the page.'
                )
                    .addClass(
                        'preload-editor-small-textarea'
                    )
                    .prop(
                        'disabled',
                        true
                    );
 
            const type =
                this.textInput(
                    field.type || 'page'
                ).prop(
                    'disabled',
                    true
                );
 
            const required =
                this.checkbox(
                    field.required !== false
                ).prop(
                    'disabled',
                    true
                );
 
            const namespace =
                this.textInput(
                    field.namespace || 'Main'
                ).prop(
                    'disabled',
                    true
                );
 
            const allowNew =
                this.checkbox(
                    field['allow-new'] === true
                ).prop(
                    'disabled',
                    true
                );
 
            card.append(
                this.fieldRow(
                    'Label',
                    label
                ),
 
                this.fieldRow(
                    'Description',
                    description
                ),
 
                this.fieldRow(
                    'Type',
                    type
                ),
 
                this.fieldRow(
                    'Required',
                    required
                ),
 
                this.fieldRow(
                    'Namespace',
                    namespace
                ),
 
                this.fieldRow(
                    'Allow new page',
                    allowNew
                )
            );
 
            return card;
        },
 
        createVariableCard(
            name,
            field,
            analysis
        ) {
            const card =
                $('<div>')
                    .addClass(
                        'preload-editor-variable'
                    )
                    .attr(
                        'data-variable',
                        name
                    );
 
            const header =
                $('<div>')
                    .addClass(
                        'preload-editor-variable-header'
                    );
 
            header.append(
                $('<h3>').text(name)
            );
 
            if (
                !analysis.variables.includes(name)
            ) {
                header.append(
                    $('<span>')
                        .addClass(
                            'preload-editor-badge is-warning'
                        )
                        .text('UNUSED')
                );
            }
 
            card.append(header);
 
            const label =
                this.textInput(
                    field.label || ''
                );
 
            label.on(
                'input.preloadEditor',
                () => {
                    field.label =
                        label.val();
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Label',
                    label
                )
            );
 
            const description =
                this.textarea(
                    field.description || ''
                ).addClass(
                    'preload-editor-small-textarea'
                );
 
            description.on(
                'input.preloadEditor',
                () => {
                    field.description =
                        description.val();
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Description',
                    description
                )
            );
 
            const types = [
                ['text', 'Text'],
                ['textarea', 'Textarea'],
                ['number', 'Number'],
                ['boolean', 'Boolean'],
                ['select', 'Select'],
                ['multiselect', 'Multi-select'],
                ['page', 'Page'],
                ['user', 'User'],
                ['date', 'Date'],
                ['file', 'File']
            ];
 
            const typeSelect =
                this.select(
                    types.map(
                        ([value, label]) => ({
                            value,
                            label
                        })
                    ),
                    field.type || 'text'
                );
 
            typeSelect.on(
                'change.preloadEditor',
                () => {
                    field.type =
                        typeSelect.val();
 
                    this.markDirty();
 
                    this.renderVariableConfigurationBySelector();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Type',
                    typeSelect
                )
            );
 
            const required =
                this.checkbox(
                    field.required === true
                );
 
            required.on(
                'change.preloadEditor',
                () => {
                    field.required =
                        required.prop('checked');
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Required',
                    required
                )
            );
 
            const placeholder =
                this.textInput(
                    field.placeholder || ''
                );
 
            placeholder.on(
                'input.preloadEditor',
                () => {
                    const value =
                        placeholder.val();
 
                    if (value) {
                        field.placeholder =
                            value;
                    } else {
                        delete field.placeholder;
                    }
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Placeholder',
                    placeholder
                )
            );
 
            const defaultInput =
                this.textInput(
                    field.default === undefined
                        ? ''
                        : field.default
                );
 
            defaultInput.on(
                'input.preloadEditor',
                () => {
                    const value =
                        defaultInput.val();
 
                    if (value) {
                        field.default =
                            value;
                    } else {
                        delete field.default;
                    }
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Default',
                    defaultInput,
                    'For text-like fields this is used as a placeholder. For select fields it is the empty/default option.'
                )
            );
 
            if (
                field.type === 'textarea'
            ) {
                const maxRows =
                    this.textInput(
                        field.maxRows || ''
                    ).attr(
                        'type',
                        'number'
                    );
 
                maxRows.on(
                    'input.preloadEditor',
                    () => {
                        const value =
                            parseInt(
                                maxRows.val(),
                                10
                            );
 
                        if (
                            Number.isFinite(value) &&
                            value > 0
                        ) {
                            field.maxRows =
                                value;
                        } else {
                            delete field.maxRows;
                        }
 
                        this.markDirty();
                        this.renderConfigReviewAvailability();
                    }
                );
 
                card.append(
                    this.fieldRow(
                        'Maximum rows',
                        maxRows
                    )
                );
            }
 
            if (
                field.type === 'select' ||
                field.type === 'multiselect'
            ) {
                this.renderSelectConfiguration(
                    card,
                    field
                );
            }
 
            if (
                field.type === 'file'
            ) {
                this.renderFileConfiguration(
                    card,
                    field
                );
            }
 
            const deleteButton =
                this.createButton(
                    'Delete variable',
                    'preload-editor-button-danger preload-editor-delete-variable',
                    async () => {
                        const confirmed =
                            await this.createConfirmDialog(
                                'Delete variable',
                                'Delete "' +
                                name +
                                '" from the configuration?\n\nThe variable will remain in the preload, but its config.json field will be removed.',
                                'Delete variable',
                                'Cancel'
                            );
 
                        if (!confirmed) {
                            return;
                        }
 
                        this.deleteVariable(name);
                    }
                );
 
            card.append(
                $('<div>')
                    .addClass(
                        'preload-editor-variable-delete'
                    )
                    .append(deleteButton)
            );
 
            return card;
        },
 
        async deleteVariable(name) {
            name =
                String(name || '')
                    .trim()
                    .toUpperCase();
 
            if (
                !name ||
                name === 'PAGE'
            ) {
                return;
            }
 
            if (
                !this.state.config ||
                !this.state.config.fields
            ) {
                return;
            }
 
            delete this.state.config.fields[name];
 
            if (
                !Array.isArray(
                    this.state.deletedVariables
                )
            ) {
                this.state.deletedVariables = [];
            }
 
            if (
                !this.state.deletedVariables.includes(
                    name
                )
            ) {
                this.state.deletedVariables.push(
                    name
                );
            }
 
            this.state.selectedVariable =
                name;
 
            this.markDirty();
 
            this.renderVariablesBySelector();
            this.renderVariableConfigurationBySelector();
            this.renderAnalysisBySelector();
            this.renderConfigReviewAvailability();
 
            this.setStatus(
                'Variable "' +
                name +
                '" removed from the configuration.',
                'warning'
            );
        },
 
        // =========================================================
        // Select configuration
        // =========================================================
 
        renderSelectConfiguration(
            card,
            field
        ) {
            const source =
                this.textInput(
                    field.source || '',
                    'category:Example'
                );
 
            source.on(
                'input.preloadEditor',
                () => {
                    const value =
                        source.val();
 
                    if (value) {
                        field.source = value;
                    } else {
                        delete field.source;
                    }
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Source',
                    source,
                    'Currently supported source format: category:Category Name'
                )
            );
 
            const sourcePrefix =
                this.textInput(
                    field['source-prefix'] || ''
                );
 
            sourcePrefix.on(
                'input.preloadEditor',
                () => {
                    const value =
                        sourcePrefix.val();
 
                    if (value) {
                        field['source-prefix'] =
                            value;
                    } else {
                        delete field['source-prefix'];
                    }
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Source prefix',
                    sourcePrefix,
                    'Only source values beginning with this text are included.'
                )
            );
 
            const excludeCategory =
                this.textInput(
                    field.exclude &&
                    field.exclude.category ||
                    ''
                );
 
            excludeCategory.on(
                'input.preloadEditor',
                () => {
                    const value =
                        excludeCategory.val();
 
                    if (value) {
                        field.exclude =
                            Object.assign(
                                {},
                                field.exclude || {},
                                {
                                    category: value
                                }
                            );
                    } else if (
                        field.exclude
                    ) {
                        delete field.exclude.category;
 
                        if (
                            !Object.keys(
                                field.exclude
                            ).length
                        ) {
                            delete field.exclude;
                        }
                    }
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Exclude category',
                    excludeCategory
                )
            );
 
            card.append(
                this.createTransformControls(
                    field,
                    'value',
                    'Value transformation'
                ),
 
                this.createTransformControls(
                    field,
                    'label',
                    'Label transformation'
                )
            );
 
            const optionsValue =
                Array.isArray(field.options)
                    ? field.options
                        .map(
                            option => {
                                if (
                                    typeof option ===
                                    'string'
                                ) {
                                    return option;
                                }
 
                                return (
                                    String(
                                        option.value || ''
                                    ) +
                                    ' | ' +
                                    String(
                                        option.label ||
                                        option.value ||
                                        ''
                                    )
                                );
                            }
                        )
                        .join('\n')
                    : '';
 
            const options =
                this.textarea(
                    optionsValue
                ).addClass(
                    'preload-editor-small-textarea'
                );
 
             options.on(
                 'input.preloadEditor',
                 () => {
                     const lines =
                         options
                             .val()
                             .split('\n')
                             .map(
                                 line =>
                                     line.trim()
                             )
                             .filter(Boolean);
 
                     if (!lines.length) {
                         delete field.options;
                    } else {
                        field.options =
                            lines.map(
                                line => {
                                    const parts =
                                        line.split(
                                            /\s*\|\s*/
                                        );
 
                                    if (
                                        parts.length ===
                                        1
                                    ) {
                                        return parts[0];
                                    }
 
                                    return {
                                        value:
                                            parts[0],
 
                                        label:
                                            parts
                                                .slice(1)
                                                .join(
                                                    ' | '
                                                )
                                    };
                                }
                            );
                    }
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Manual options',
                    options,
                    'One per line. Optional label format: value | label. Leave empty when using a source.'
                )
            );
        },
       
                renderFileConfiguration(
            card,
            field
        ) {
            const acceptValue =
                Array.isArray(field.accept)
                    ? field.accept.join('\n')
                    : '';
 
            const accept =
                this.textarea(
                    acceptValue
                ).addClass(
                    'preload-editor-small-textarea'
                );
 
            accept.on(
                'input.preloadEditor',
                () => {
                    const values =
                        accept
                            .val()
                            .split('\n')
                            .map(
                                value =>
                                    value.trim()
                            )
                            .filter(Boolean);
 
                    if (values.length) {
                        field.accept = values;
                    } else {
                        delete field.accept;
                    }
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Accepted MIME types',
                    accept,
                    'One MIME type per line. Example: image/png'
                )
            );
 
            const filename =
                this.textInput(
                    field.filename ||
                    '%%PAGE%%.%%EXT%%'
                );
 
            filename.on(
                'input.preloadEditor',
                () => {
                    const value =
                        filename.val();
 
                    if (value) {
                        field.filename = value;
                    } else {
                        delete field.filename;
                    }
 
                    this.markDirty();
                    this.renderConfigReviewAvailability();
                }
            );
 
            card.append(
                this.fieldRow(
                    'Filename',
                    filename,
                    'Filename template. %%PAGE%% is replaced with the page name and %%EXT%% with the uploaded file extension.'
                )
            );
 
            const filetext =
                this.textarea(
                    field.filetext || ''
                );
 
            filetext.on(
                'input.preloadEditor',
                () => {
                    const value =
                        filetext.val();
 
                    if (value) {
                        field.filetext = value;
                     } else {
                     } else {
                         field.options =
                         delete field.filetext;
                            lines.map(
                                line => {
                                    const parts =
                                        line.split(
                                            /\s*\|\s*/
                                        );
 
                                    if (
                                        parts.length ===
                                        1
                                    ) {
                                        return parts[0];
                                    }
 
                                    return {
                                        value:
                                            parts[0],
 
                                        label:
                                            parts
                                                .slice(1)
                                                .join(
                                                    ' | '
                                                )
                                    };
                                }
                            );
                     }
                     }


Line 5,029: Line 5,359:
             card.append(
             card.append(
                 this.fieldRow(
                 this.fieldRow(
                     'Manual options',
                     'File page text',
                     options,
                     filetext,
                     'One per line. Optional label format: value | label. Leave empty when using a source.'
                     'Wikitext used for the uploaded File: page.'
                 )
                 )
             );
             );
Line 5,812: Line 6,142:


             let depth = 0;
             let depth = 0;
 
                        analysis.conditions.forEach(
            analysis.conditions.forEach(
                 condition => {
                 condition => {
                     if (
                     if (
Line 5,885: Line 6,214:
         if (
         if (
             mw.config.get('wgPageName') !==
             mw.config.get('wgPageName') !==
             'Petit_Planet:Preload_Editor'
             'Petit_Planet_Wiki:Preload_Editor'
         ) {
         ) {
             return;
             return;