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
Line 30: Line 30:
         },
         },


        // =========================================================
         // API
         // API
        // =========================================================


         async getPage(title) {
         async getPage(title) {
Line 115: Line 113:
         },
         },


        // =========================================================
         // Preload parsing
         // Preload parsing
        // =========================================================


         extractIncludeonly(source) {
         extractIncludeonly(source) {
Line 154: Line 150:
         findConditions(content) {
         findConditions(content) {
             const conditions = [];
             const conditions = [];
             const pattern =
             const pattern =
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
Line 169: Line 164:
                     expression:
                     expression:
                         match[2].trim(),
                         match[2].trim(),
                     index:
                     index: match.index
                        match.index
                 });
                 });
             }
             }
Line 183: Line 177:
                 condition => {
                 condition => {
                     if (
                     if (
                         condition.type !== 'if' &&
                         condition.type !==
                         condition.type !== 'elseif'
                            'if' &&
                         condition.type !==
                            'elseif'
                     ) {
                     ) {
                         return;
                         return;
Line 217: Line 213:
             return {
             return {
                 variables:
                 variables:
                     this.findVariables(content),
                     this.findVariables(
                        content
                    ),


                 conditions:
                 conditions:
                     this.findConditions(content),
                     this.findConditions(
                        content
                    ),


                 conditionVariables:
                 conditionVariables:
Line 229: Line 229:
         },
         },


        // =========================================================
         // Config
         // Config
        // =========================================================


         getAutomaticPageField() {
         getAutomaticPageField() {
             return {
             return {
                 label: 'Page Name',
                 label: 'Page Name',
                 description:
                 description: 'Name of the page.',
                    'Name of the page.',
                 type: 'page',
                 type: 'page',
                 required: true,
                 required: true,
Line 264: Line 261:
                 Array.isArray(config)
                 Array.isArray(config)
             ) {
             ) {
                 config = this.emptyConfig();
                 config =
                    this.emptyConfig();
             }
             }


Line 271: Line 269:
                 typeof config.settings !==
                 typeof config.settings !==
                     'object' ||
                     'object' ||
                 Array.isArray(config.settings)
                 Array.isArray(
                    config.settings
                )
             ) {
             ) {
                 config.settings = {};
                 config.settings = {};
Line 280: Line 280:
                 typeof config.fields !==
                 typeof config.fields !==
                     'object' ||
                     'object' ||
                 Array.isArray(config.fields)
                 Array.isArray(
                    config.fields
                )
             ) {
             ) {
                 config.fields = {};
                 config.fields = {};
Line 286: Line 288:


             /*
             /*
             * PAGE is always a special automatic field.
             * PAGE is always a special,
            * automatic field.
            *
            * Its configuration is precompiled
            * and cannot be edited.
             */
             */
             config.fields.PAGE =
             config.fields.PAGE =
Line 300: Line 306:
             return {
             return {
                 label:
                 label:
                     this.humanizeName(name),
                     this.humanizeName(
                        name
                    ),


                 description: '',
                 description: '',
Line 335: Line 343:
         },
         },


        /*
        * Makes sure every variable currently used
        * by the preload has a configuration field.
        *
        * This is the important part for newly typed
        * variables such as %%ZHS%%.
        */
         prepareConfigForVariables(
         prepareConfigForVariables(
             config,
             config,
Line 357: Line 358:


             /*
             /*
             * PAGE is always automatic/precompiled.
             * PAGE is always automatic.
             */
             */
             fields.PAGE =
             fields.PAGE =
Line 363: Line 364:


             /*
             /*
             * Add every variable currently found
             * Add every variable currently
             * in the preload.
             * detected in the preload.
             *
             *
             * Existing configuration is preserved.
             * Existing configuration is
             * New variables receive a default field.
            * preserved.
            *
             * New variables receive a
            * default configuration.
             */
             */
             analysis.variables.forEach(
             analysis.variables.forEach(
Line 374: Line 378:
                         return;
                         return;
                     }
                     }
                    const existingField =
                        existing[name];


                     fields[name] =
                     fields[name] =
                         existingField ||
                         existing[name] ||
                         this.createDefaultField(
                         this.createDefaultField(
                             name,
                             name,
Line 388: Line 389:


             /*
             /*
             * Preserve existing fields which are no
             * Preserve existing fields even
             * longer used. These are shown as UNUSED
            * if they are currently unused.
             * rather than silently deleted.
            *
            * This allows users to keep
             * configuration around while
             * editing the preload.
             */
             */
             Object.keys(existing).forEach(
             Object.keys(existing).forEach(
Line 437: Line 441:
                 typeof config.fields !==
                 typeof config.fields !==
                     'object' ||
                     'object' ||
                 Array.isArray(config.fields)
                 Array.isArray(
                    config.fields
                )
             ) {
             ) {
                 return {
                 return {
Line 556: Line 562:
         },
         },


        // =========================================================
         // Transformation helpers
         // Transformation helpers
        // =========================================================


         transformationObject(
         transformationObject(
Line 602: Line 606:
         },
         },


        // =========================================================
         // UI helpers
         // UI helpers
        // =========================================================


         createSection(
         createSection(
Line 707: Line 709:
                             ? ''
                             ? ''
                             : value,
                             : value,
                     placeholder:
                     placeholder:
                         placeholder || ''
                         placeholder || ''
Line 767: Line 770:
                                 option.value
                                 option.value
                             ) ===
                             ) ===
                                String(
                            String(
                                    value ===
                                value ===
                                        undefined
                                    undefined
                                        ? ''
                                    ? ''
                                        : value
                                    : value
                                )
                            )
                         )
                         )
                         .appendTo(select);
                         .appendTo(
                            select
                        );
                 }
                 }
             );
             );
Line 781: Line 786:
         },
         },


        // =========================================================
         // CodeMirror
         // CodeMirror
        // =========================================================


         destroyEditor() {
         destroyEditor() {
Line 821: Line 824:


         getEditorContent() {
         getEditorContent() {
            /*
            * Always read directly from the
            * CodeMirror state when available.
            *
            * This is important because the
            * native textarea may not contain
            * the actual current document.
            */
             if (
             if (
                 this.editorView &&
                 this.editorView &&
Line 876: Line 887:


             return {
             return {
                 from: selection.from,
                 from:
                 to: selection.to,
                    selection.from,
 
                 to:
                    selection.to,
 
                 text:
                 text:
                     this.editorView.state.doc
                     this.editorView.state.doc.sliceString(
                        .sliceString(
                        selection.from,
                            selection.from,
                        selection.to
                            selection.to
                    )
                        )
             };
             };
         },
         },
Line 910: Line 924:


             const from =
             const from =
                 selectionFrom === undefined
                 selectionFrom ===
                    undefined
                     ? selection.from
                     ? selection.from
                     : selectionFrom;
                     : selectionFrom;


             const to =
             const to =
                 selectionTo === undefined
                 selectionTo ===
                    undefined
                     ? selection.to
                     ? selection.to
                     : selectionTo;
                     : selectionTo;
Line 940: Line 956:
         },
         },


         // =========================================================
         // V2.3 editor tools
        // Editor tools
        // =========================================================


         wrapSelectionWithVariable() {
         wrapSelectionWithVariable() {
Line 1,057: Line 1,071:


                         cursorOffset =
                         cursorOffset =
                             opening.length + 1;
                             opening.length +
                            1;
                     }
                     }


Line 1,066: Line 1,081:
                     text =
                     text =
                         '<!-- @elseif -->';
                         '<!-- @elseif -->';
                     cursorOffset =
                     cursorOffset =
                         text.length;
                         text.length;
                     break;
                     break;


Line 1,073: Line 1,090:
                     text =
                     text =
                         '<!-- @else -->';
                         '<!-- @else -->';
                     cursorOffset =
                     cursorOffset =
                         text.length;
                         text.length;
                     break;
                     break;


Line 1,080: Line 1,099:
                     text =
                     text =
                         '<!-- @endif -->';
                         '<!-- @endif -->';
                     cursorOffset =
                     cursorOffset =
                         text.length;
                         text.length;
                     break;
                     break;


Line 1,147: Line 1,168:
                             option.label
                             option.label
                         )
                         )
                         .appendTo(select);
                         .appendTo(
                            select
                        );
                 }
                 }
             );
             );
Line 1,327: Line 1,350:
                         },
                         },
                         {
                         {
                             value:
                             value: 'elseif',
                                'elseif',
                             label: '@elseif'
                             label:
                                '@elseif'
                         },
                         },
                         {
                         {
                             value: 'else',
                             value: 'else',
                             label:
                             label: '@else'
                                '@else'
                         },
                         },
                         {
                         {
                             value:
                             value: 'endif',
                                'endif',
                             label: '@endif'
                             label:
                                '@endif'
                         }
                         }
                     ],
                     ],
Line 1,411: Line 1,429:
         },
         },


        // =========================================================
         // Preload syntax highlighting
         // Preload syntax highlighting
        // =========================================================


         getKnownVariables() {
         getKnownVariables() {
Line 1,428: Line 1,444:
                     'object'
                     'object'
             ) {
             ) {
                 Object.keys(fields).forEach(
                 Object.keys(
                    fields
                ).forEach(
                     name => {
                     name => {
                         known.add(
                         known.add(
Line 1,460: Line 1,478:


             while (
             while (
                 (match =
                 (
                    variablePattern.exec(
                    match =
                        source
                        variablePattern.exec(
                    )) !== null
                            source
                        )
                ) !== null
             ) {
             ) {
                 const name =
                 const name =
Line 1,493: Line 1,513:


             while (
             while (
                 (match =
                 (
                    conditionPattern.exec(
                    match =
                        source
                        conditionPattern.exec(
                    )) !== null
                            source
                        )
                ) !== null
             ) {
             ) {
                 decorations.push(
                 decorations.push(
Line 1,647: Line 1,669:
                 syntaxField;
                 syntaxField;
         },
         },
        // =========================================================
        // Editor state synchronization
        // =========================================================
        /*
        * Re-analyze the current CodeMirror content,
        * create configuration fields for newly found
        * variables, keep selection valid, and update
        * all live UI.
        */
        syncEditorState() {
            this.reanalyze();
            this.renderVariablesBySelector();
            this.renderVariableConfigurationBySelector();
            this.renderAnalysisBySelector();
            this.renderConfigPreviewBySelector();
            this.updateEditorTools();
        },
        // =========================================================
        // CodeMirror mounting
        // =========================================================


         async mountCodeMirror(
         async mountCodeMirror(
Line 1,680: Line 1,674:
         ) {
         ) {
             return new Promise(
             return new Promise(
                 (resolve, reject) => {
                 (
                    resolve,
                    reject
                ) => {
                     let settled = false;
                     let settled = false;


Line 1,726: Line 1,723:


                                     /*
                                     /*
                                     * This now performs the
                                     * Keep the editor state
                                     * entire live synchronization.
                                     * synchronized, but do not
                                    * rebuild the right panel
                                    * on every CodeMirror event.
                                     *
                                     *
                                     * So typing %%ZHS%% will:
                                     * The Detect variables
                                     *
                                     * button is the explicit
                                     * - detect ZHS
                                     * refresh action.
                                    * - create config.fields.ZHS
                                    * - add ZHS to the variable list
                                    * - make ZHS selectable
                                    * - update its configuration
                                    * - update config preview
                                    * - update editor tools
                                     */
                                     */
                                     this.syncEditorState();
                                     this.state
                                        .preloadContent =
                                        view.state.doc.toString();
                                 };
                                 };


Line 1,781: Line 1,776:
                                 );
                                 );


                                /*
                                 resolve(view);
                                * Run one initial synchronization
                                * after CodeMirror is mounted so the
                                * UI is based on the actual document.
                                */
                                this.syncEditorState();
 
                                 resolve(
                                    view
                                );
                             } catch (
                             } catch (
                                 error
                                 error
                             ) {
                             ) {
                                 reject(
                                 reject(error);
                                    error
                                );
                             }
                             }
                         };
                         };
Line 1,840: Line 1,824:
         },
         },


        // =========================================================
         // Main UI
         // Main UI
        // =========================================================


         init() {
         init() {
Line 1,871: Line 1,853:
         renderShell() {
         renderShell() {
             this.destroyEditor();
             this.destroyEditor();
             this.root.empty();
             this.root.empty();


Line 2,025: Line 2,006:
         },
         },


        // =========================================================
         // Load / create
         // Load / create
        // =========================================================


         async loadExisting(
         async loadExisting(
Line 2,227: Line 2,206:
                 preloadContent:
                 preloadContent:
                     this.defaults.preload,
                     this.defaults.preload,
                 config:
                 config:
                     this.emptyConfig(),
                     this.emptyConfig(),
                 variables: ['PAGE'],
                 variables: ['PAGE'],
                 conditions: [],
                 conditions: [],
                 selectedVariable:
                 selectedVariable: 'PAGE',
                    'PAGE',
                 mode: 'new',
                 mode: 'new',
                 dirty: true
                 dirty: true
Line 2,284: Line 2,264:
                     preloadContent:
                     preloadContent:
                         sample,
                         sample,
                     config:
                     config:
                         this.emptyConfig(),
                         this.emptyConfig(),
                     variables: ['PAGE'],
                     variables: ['PAGE'],
                     conditions: [],
                     conditions: [],
                     selectedVariable:
                     selectedVariable: 'PAGE',
                        'PAGE',
                     mode: 'new',
                     mode: 'new',
                     dirty: true
                     dirty: true
Line 2,395: Line 2,376:
                             () => {
                             () => {
                                 overlay.remove();
                                 overlay.remove();
                                 resolve(
                                 resolve(null);
                                    null
                                );
                             }
                             }
                         );
                         );
Line 2,417: Line 2,396:


                                 overlay.remove();
                                 overlay.remove();
                                 resolve(
                                 resolve(value);
                                    value
                                );
                             }
                             }
                         );
                         );
Line 2,524: Line 2,501:


             this.state = {
             this.state = {
                 preloadTitle:
                 preloadTitle: title,
                    title,
                 preloadSource: source,
 
                 preloadContent: source,
                 preloadSource:
                    source,
 
                 preloadContent:
                    source,
 
                 config,
                 config,


Line 2,558: Line 2,529:
         },
         },


        // =========================================================
         // Editor
         // Editor
        // =========================================================


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


Line 2,659: Line 2,627:
             /*
             /*
             * RIGHT PANEL
             * RIGHT PANEL
            *
            * Analysis
            * Variables
            * Variable configuration
            * Generated configuration
            * Actions
             */
             */


Line 2,680: Line 2,654:
                     'Variables'
                     'Variables'
                 );
                 );
            /*
            * Explicit variable detection button.
            *
            * This is intentionally separate from
            * CodeMirror's input event because
            * CodeMirror 6 owns its own document
            * state and does not always synchronize
            * through the native textarea/input event.
            */
            const detectVariablesButton =
                this.createButton(
                    'Detect variables',
                    'preload-editor-button-primary preload-editor-detect-variables',
                    () =>
                        this.detectVariables()
                );
            const detectHelp =
                $('<div>')
                    .addClass(
                        'preload-editor-help preload-editor-detect-variables-help'
                    )
                    .text(
                        'Refresh the variable list after editing the preload.'
                    );
            const detectControls =
                $('<div>')
                    .addClass(
                        'preload-editor-variable-detect'
                    )
                    .append(
                        detectVariablesButton,
                        detectHelp
                    );
            variablesSection.append(
                detectControls
            );


             const variableContainer =
             const variableContainer =
Line 2,798: Line 2,812:
         },
         },


         reanalyze() {
         /*
        * Explicitly re-read the CodeMirror document
        * and refresh every part of the right panel.
        *
        * This is the important new part.
        */
        detectVariables() {
             const source =
             const source =
                 this.getEditorContent();
                 this.getEditorContent();


             const content =
             /*
                this.extractIncludeonly(
            * Make sure the editor's actual document
                    source
            * becomes the canonical preload content.
                );
            */
 
            const analysis =
                this.analyzePreload(
                    content
                );
 
             this.state.preloadContent =
             this.state.preloadContent =
                 source;
                 source;


             this.state.variables =
             /*
                analysis.variables;
            * Mark the preload as modified because
 
            * the user has edited the CodeMirror
             this.state.conditions =
            * document.
                analysis.conditions;
            */
             this.state.dirty = true;


             /*
             /*
             * This is where newly typed variables
             * Re-run the complete preload analysis.
            * become configuration fields.
             *
             *
             * For example:
             * This discovers new variables such as:
             *
             *
             * %%ZHS%%
             * %%ZHS%%
             *
             *
             * becomes:
             * and creates their default config
             *
             * entries.
             * config.fields.ZHS = {
             */
            *    label: 'Zhs',
            this.reanalyze();
            *    description: '',
 
            *     type: 'text'
            /*
             * };
             * Refresh the right panel.
             */
             */
             this.state.config =
             this.renderVariablesBySelector();
                this.prepareConfigForVariables(
 
                    this.state.config ||
            this.renderVariableConfigurationBySelector();
                        this.emptyConfig(),
 
                    analysis
            this.renderAnalysisBySelector();
                );


             this.analysis =
             this.renderConfigPreviewBySelector();
                analysis;


             /*
             /*
             * PAGE is always available.
             * Refresh the variable selectors in
            * the editor toolbar too.
             */
             */
             const available =
             this.updateEditorTools();
                ['PAGE'].concat(
                    analysis.variables.filter(
                        name =>
                            name !== 'PAGE'
                    )
                );


             /*
             /*
             * If the selected variable
             * Refresh syntax highlighting so a newly
            * disappeared from the preload,
             * detected variable becomes "known".
            * move selection to PAGE or
             * the first available variable.
             */
             */
             if (
             if (
                 !available.includes(
                 this.editorView &&
                    this.state
                this.editor &&
                        .selectedVariable
                this.editor.syntaxField
                )
             ) {
             ) {
                 this.state
                 /*
                    .selectedVariable =
                * The syntax decoration field normally
                    available[0] ||
                * updates when CodeMirror's document
                     'PAGE';
                * changes. Here we force a tiny
                * configuration refresh by replacing
                * the current syntax field with the
                * same configuration.
                *
                * If the field is already up to date,
                * CodeMirror simply keeps the current
                * document unchanged.
                */
                try {
                    this.editorView.dispatch({
                        effects:
                            this.editorView.state
                                .field(
                                    this.editor.syntaxField
                                )
                                ? []
                                : []
                     });
                } catch (
                    error
                ) {
                    /*
                    * Syntax highlighting is cosmetic;
                    * never let it break variable
                    * detection.
                    */
                    console.warn(
                        'Preload Editor syntax refresh:',
                        error
                    );
                }
             }
             }
            this.setStatus(
                'Variables detected from the current preload.',
                'success'
            );
         },
         },


         renderAnalysis(
         reanalyze() {
             container
             const source =
        ) {
                this.getEditorContent();
             container.empty();
 
             const content =
                this.extractIncludeonly(
                    source
                );


             const analysis =
             const analysis =
                this.analysis ||
                 this.analyzePreload(
                 this.analyzePreload(
                     this.extractIncludeonly(
                     content
                        this.getEditorContent()
                    )
                 );
                 );


             const variableCount =
             this.state.preloadContent =
                 analysis.variables.length;
                 source;


             const conditionCount =
             this.state.variables =
                 analysis.conditions.length;
                 analysis.variables;


             const configuredCount =
             this.state.conditions =
                Object.keys(
                 analysis.conditions;
                    this.state.config &&
                    this.state.config.fields
                        ? this.state.config.fields
                        : {}
                 ).length;


             const summary =
             this.state.config =
                 $('<div>')
                 this.prepareConfigForVariables(
                     .addClass(
                     this.state.config ||
                        'preload-editor-analysis-summary'
                        this.emptyConfig(),
                    );
                    analysis
                );


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


                            $('<span>')
            /*
                                .addClass(
            * PAGE is always available.
                                    'preload-editor-analysis-label'
            *
                                )
            * If the selected variable
                                .text(label)
            * disappeared, select PAGE or
                        );
            * the first currently used variable.
            */
            const available =
                ['PAGE'].concat(
                    analysis.variables.filter(
                        name =>
                            name !==
                            'PAGE'
                    )
                );


             summary.append(
             if (
                 createMetric(
                 !available.includes(
                     variableCount,
                     this.state.selectedVariable
                    variableCount === 1
                        ? 'variable'
                        : 'variables'
                ),
 
                createMetric(
                    conditionCount,
                    conditionCount === 1
                        ? 'condition marker'
                        : 'condition markers'
                ),
 
                createMetric(
                    configuredCount,
                    configuredCount === 1
                        ? 'configured field'
                        : 'configured fields'
                 )
                 )
             );
             ) {
                this.state.selectedVariable =
                    available[0] ||
                    'PAGE';
            }
        },


             container.append(
        renderAnalysis(
                summary
             container
             );
        ) {
             container.empty();


             if (
             const analysis =
                 analysis.conditionVariables
                 this.analysis ||
                    .length
                 this.analyzePreload(
            ) {
                     this.extractIncludeonly(
                 container.append(
                         this.getEditorContent()
                     $('<div>')
                    )
                        .addClass(
                            'preload-editor-analysis-note'
                        )
                         .text(
                            'Condition variables: ' +
                            analysis.conditionVariables.join(
                                ', '
                            )
                        )
                 );
                 );
            }


             const validation =
             const variableCount =
                 this.validateConfig(
                 analysis.variables.length;
                    this.state.config,
                    analysis
                );


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


                 validation.errors.forEach(
            const configuredCount =
                     error => {
                 Object.keys(
                         $('<li>')
                    this.state.config &&
                            .text(error)
                     this.state.config.fields
                            .appendTo(
                         ? this.state.config.fields
                                errors
                        : {}
                            );
                 ).length;
                    }
                 );


                 container.append(
            const summary =
                     errors
                 $('<div>')
                );
                    .addClass(
            }
                        'preload-editor-analysis-summary'
                     );


             if (
             const createMetric =
                validation.warnings.length
                (
            ) {
                    value,
                 const warnings =
                    label
                     $('<ul>')
                 ) =>
                     $('<div>')
                         .addClass(
                         .addClass(
                             'preload-editor-validation-warnings'
                             'preload-editor-analysis-metric'
                        )
                        .append(
                            $('<strong>')
                                .addClass(
                                    'preload-editor-analysis-value'
                                )
                                .text(value),
 
                            $('<span>')
                                .addClass(
                                    'preload-editor-analysis-label'
                                )
                                .text(label)
                         );
                         );


                 validation.warnings.forEach(
            summary.append(
                     warning => {
                 createMetric(
                         $('<li>')
                     variableCount,
                            .text(warning)
                    variableCount === 1
                            .appendTo(
                         ? 'variable'
                                warnings
                        : 'variables'
                            );
                ),
                    }
 
                 );
                createMetric(
                    conditionCount,
                    conditionCount === 1
                        ? 'condition marker'
                        : 'condition markers'
                ),
 
                createMetric(
                    configuredCount,
                    configuredCount === 1
                        ? 'configured field'
                        : 'configured fields'
                 )
            );


                container.append(
            container.append(
                    warnings
                summary
                );
            );
            }


             if (
             if (
                 !validation.errors.length &&
                 analysis.conditionVariables
                !validation.warnings.length
                    .length
             ) {
             ) {
                 container.append(
                 container.append(
                     $('<div>')
                     $('<div>')
                         .addClass(
                         .addClass(
                             'preload-editor-validation-success'
                             'preload-editor-analysis-note'
                         )
                         )
                         .text(
                         .text(
                             'Preload and configuration are in sync.'
                             'Condition variables: ' +
                            analysis
                                .conditionVariables
                                .join(', ')
                         )
                         )
                 );
                 );
             }
             }
        },


        renderAnalysisBySelector() {
             const validation =
             const container =
                 this.validateConfig(
                 this.root &&
                    this.state.config,
                this.root.find(
                     analysis
                     '.preload-editor-analysis'
                 );
                 );


             if (
             if (
                 container &&
                 validation.errors.length
                container.length
             ) {
             ) {
                 this.renderAnalysis(
                 const errors =
                     container
                    $('<ul>')
                        .addClass(
                            'preload-editor-validation-errors'
                        );
 
                validation.errors.forEach(
                    error => {
                        $('<li>')
                            .text(error)
                            .appendTo(
                                errors
                            );
                     }
                );
 
                container.append(
                    errors
                 );
                 );
             }
             }
        },


        // =========================================================
            if (
        // Variables
                validation.warnings.length
        // =========================================================
            ) {
 
                const warnings =
        renderVariables(
                    $('<ul>')
            container
                        .addClass(
        ) {
                            'preload-editor-validation-warnings'
            container.empty();
                        );


            const analysis =
                 validation.warnings.forEach(
                 this.analysis ||
                     warning => {
                this.analyzePreload(
                        $('<li>')
                     this.extractIncludeonly(
                            .text(warning)
                        this.getEditorContent()
                            .appendTo(
                     )
                                warnings
                            );
                     }
                 );
                 );


            const variables =
                 container.append(
                 analysis.variables.slice();
                    warnings
                );
            }


             /*
             if (
            * PAGE is always shown first.
                !validation.errors.length &&
            */
                !validation.warnings.length
             const ordered = ['PAGE'];
             ) {
                container.append(
                    $('<div>')
                        .addClass(
                            'preload-editor-validation-success'
                        )
                        .text(
                            'Preload and configuration are in sync.'
                        )
                );
            }
        },


             variables.forEach(
        renderAnalysisBySelector() {
            const container =
                this.root &&
                this.root.find(
                    '.preload-editor-analysis'
                );
 
            if (
                container &&
                container.length
            ) {
                this.renderAnalysis(
                    container
                );
            }
        },
 
        /*
        * Variable list
        */
 
        renderVariables(
            container
        ) {
            container.empty();
 
            const analysis =
                this.analysis ||
                this.analyzePreload(
                    this.extractIncludeonly(
                        this.getEditorContent()
                    )
                );
 
            const variables =
                analysis.variables.slice();
 
            const ordered =
                ['PAGE'];
 
             variables.forEach(
                 name => {
                 name => {
                     if (
                     if (
Line 3,098: Line 3,196:
                         )
                         )
                     ) {
                     ) {
                         ordered.push(name);
                         ordered.push(
                            name
                        );
                     }
                     }
                 }
                 }
             );
             );


            /*
             const list =
            * Existing configured fields which
                 $('<div>')
            * are no longer used are also shown.
            *
            * This preserves the previous behaviour
            * where unused configuration is visible.
            */
            const fields =
                this.state.config &&
                this.state.config.fields
                    ? this.state.config.fields
                    : {};
 
            Object.keys(fields).forEach(
                name => {
                    const upper =
                        name.toUpperCase();
 
                    if (
                        upper !== 'PAGE' &&
                        !ordered.includes(
                            upper
                        )
                    ) {
                        ordered.push(
                            upper
                        );
                    }
                }
            );
 
             const list =
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-variable-list'
                         'preload-editor-variable-list'
Line 3,186: Line 3,255:


                     if (
                     if (
                         name === 'PAGE'
                         name ===
                        'PAGE'
                     ) {
                     ) {
                         item.append(
                         item.append(
Line 3,220: Line 3,290:
                 list
                 list
             );
             );
            if (
                !ordered.length
            ) {
                container.append(
                    $('<div>')
                        .addClass(
                            'preload-editor-empty'
                        )
                        .text(
                            'No variables found in the preload.'
                        )
                );
            }
         },
         },


Line 3,239: Line 3,323:
         },
         },


         // =========================================================
         /*
        // Variable selection / configuration
        * Select a variable and display only
        // =========================================================
        * its configuration.
 
        *
        * No scrolling.
        */
         selectVariable(
         selectVariable(
             name
             name
Line 3,260: Line 3,346:
                         this.analysis &&
                         this.analysis &&
                         this.analysis.variables
                         this.analysis.variables
                             ? this.analysis.variables
                             ? this.analysis
                                .variables
                             : []
                             : []
                     ).filter(
                     ).filter(
Line 3,269: Line 3,356:
                 );
                 );


            /*
             if (
            * Also allow selecting an existing
            * unused configuration field.
            */
            const fields =
                this.state.config &&
                this.state.config.fields
                    ? this.state.config.fields
                    : {};
 
            Object.keys(fields).forEach(
                fieldName => {
                    const upper =
                        fieldName.toUpperCase();
 
                    if (
                        !available.includes(
                            upper
                        )
                    ) {
                        available.push(
                            upper
                        );
                    }
                }
            );
 
             if (
                 !available.includes(
                 !available.includes(
                     name
                     name
Line 3,360: Line 3,420:


             if (
             if (
                 name === 'PAGE'
                 name ===
                'PAGE'
             ) {
             ) {
                 container.append(
                 container.append(
Line 3,380: Line 3,441:
         },
         },


         // =========================================================
         /*
        // Automatic PAGE
        * PAGE is automatic/precompiled.
        // =========================================================
        */


         createAutomaticPageCard(
         createAutomaticPageCard(
Line 3,390: Line 3,451:
                 $('<div>')
                 $('<div>')
                     .addClass(
                     .addClass(
                         'preload-editor-variable ' +
                         'preload-editor-variable preload-editor-variable-automatic'
                        'preload-editor-variable-automatic'
                     )
                     )
                     .attr(
                     .attr(
Line 3,405: Line 3,465:


             header.append(
             header.append(
                 $('<h3>').text(
                 $('<h3>')
                    'PAGE'
                    .text('PAGE'),
                ),


                 $('<span>')
                 $('<span>')
Line 3,436: Line 3,495:
                 this.textInput(
                 this.textInput(
                     field.label ||
                     field.label ||
                        'Page Name'
                    'Page Name'
                 ).prop(
                 ).prop(
                     'disabled',
                     'disabled',
Line 3,445: Line 3,504:
                 this.textarea(
                 this.textarea(
                     field.description ||
                     field.description ||
                        'Name of the page.'
                    'Name of the page.'
                 )
                 )
                     .addClass(
                     .addClass(
Line 3,458: Line 3,517:
                 this.textInput(
                 this.textInput(
                     field.type ||
                     field.type ||
                        'page'
                    'page'
                 ).prop(
                 ).prop(
                     'disabled',
                     'disabled',
Line 3,476: Line 3,535:
                 this.textInput(
                 this.textInput(
                     field.namespace ||
                     field.namespace ||
                        'Main'
                    'Main'
                 ).prop(
                 ).prop(
                     'disabled',
                     'disabled',
Line 3,525: Line 3,584:
             return card;
             return card;
         },
         },
        // =========================================================
        // Normal variable card
        // =========================================================


         createVariableCard(
         createVariableCard(
Line 3,552: Line 3,607:


             header.append(
             header.append(
                 $('<h3>').text(
                 $('<h3>')
                    name
                    .text(name)
                )
             );
             );


Line 3,576: Line 3,630:
                 header
                 header
             );
             );
            // Label


             const label =
             const label =
Line 3,602: Line 3,654:
                 )
                 )
             );
             );
            // Description


             const description =
             const description =
                 this.textarea(
                 this.textarea(
                     field.description ||
                     field.description ||
                        ''
                    ''
                 ).addClass(
                 ).addClass(
                     'preload-editor-small-textarea'
                     'preload-editor-small-textarea'
Line 3,631: Line 3,681:
                 )
                 )
             );
             );
            // Type


             const types = [
             const types = [
Line 3,710: Line 3,758:
                 )
                 )
             );
             );
            // Required


             const required =
             const required =
Line 3,739: Line 3,785:
                 )
                 )
             );
             );
            // Placeholder


             const placeholder =
             const placeholder =
                 this.textInput(
                 this.textInput(
                     field.placeholder ||
                     field.placeholder ||
                        ''
                    ''
                 );
                 );


Line 3,773: Line 3,817:
                 )
                 )
             );
             );
            // Default


             const defaultInput =
             const defaultInput =
Line 3,810: Line 3,852:
                 )
                 )
             );
             );
            // Textarea options


             if (
             if (
Line 3,820: Line 3,860:
                     this.textInput(
                     this.textInput(
                         field.maxRows ||
                         field.maxRows ||
                            ''
                        ''
                     ).attr(
                     ).attr(
                         'type',
                         'type',
Line 3,860: Line 3,900:
                 );
                 );
             }
             }
            // Select / multiselect


             if (
             if (
Line 3,877: Line 3,915:
             return card;
             return card;
         },
         },
        // =========================================================
        // Select configuration
        // =========================================================


         renderSelectConfiguration(
         renderSelectConfiguration(
Line 3,888: Line 3,922:
             const source =
             const source =
                 this.textInput(
                 this.textInput(
                     field.source ||
                     field.source || '',
                        '',
                     'category:Example'
                     'category:Example'
                 );
                 );
Line 3,922: Line 3,955:
             const sourcePrefix =
             const sourcePrefix =
                 this.textInput(
                 this.textInput(
                     field[
                     field['source-prefix'] ||
                        'source-prefix'
                    ''
                    ] || ''
                 );
                 );


Line 3,961: Line 3,993:
                     field.exclude &&
                     field.exclude &&
                     field.exclude.category ||
                     field.exclude.category ||
                        ''
                    ''
                 );
                 );


Line 3,984: Line 4,016:
                         field.exclude
                         field.exclude
                     ) {
                     ) {
                         delete field
                         delete field.exclude
                            .exclude
                             .category;
                             .category;


Line 4,126: Line 4,157:
             );
             );
         },
         },
        // =========================================================
        // Transformation controls
        // =========================================================


         createTransformControls(
         createTransformControls(
Line 4,176: Line 4,203:
                         },
                         },
                         {
                         {
                             value:
                             value: 'replace',
                                'replace',
                             label:
                             label:
                                 'Replace'
                                 'Replace'
Line 4,189: Line 4,215:
                 this.textInput(
                 this.textInput(
                     existing.prefix ||
                     existing.prefix ||
                        ''
                    ''
                 );
                 );


Line 4,195: Line 4,221:
                 this.textInput(
                 this.textInput(
                     existing.suffix ||
                     existing.suffix ||
                        ''
                    ''
                 );
                 );


Line 4,201: Line 4,227:
                 this.textInput(
                 this.textInput(
                     existing.from ||
                     existing.from ||
                        ''
                    ''
                 );
                 );


Line 4,207: Line 4,233:
                 this.textInput(
                 this.textInput(
                     existing.to ||
                     existing.to ||
                        ''
                    ''
                 );
                 );


Line 4,315: Line 4,341:
                             object;
                             object;
                     } else {
                     } else {
                         delete field[
                         delete field[property];
                            property
                        ];
                     }
                     }


Line 4,352: Line 4,376:
                     transform
                     transform
                 ),
                 ),
                 prefixRow,
                 prefixRow,
                 suffixRow,
                 suffixRow,
Line 4,367: Line 4,392:
         },
         },


        // =========================================================
         // Config output
         // Config output
        // =========================================================


         getCleanConfig() {
         getCleanConfig() {
Line 4,382: Line 4,405:
                 config.fields || {};
                 config.fields || {};


             const orderedFields = {};
             const orderedFields =
                {};


             /*
             /*
             * PAGE is always first.
             * PAGE is always first and automatic.
             */
             */
             orderedFields.PAGE =
             orderedFields.PAGE =
                 this.getAutomaticPageField();
                 this.getAutomaticPageField();


             Object.keys(fields)
             Object.keys(
                fields
            )
                 .filter(
                 .filter(
                     name =>
                     name =>
Line 4,447: Line 4,473:


         markDirty() {
         markDirty() {
             this.state.dirty =
             this.state.dirty = true;
                true;
         },
         },


        // =========================================================
         // Save
         // Save
        // =========================================================


         async savePreloadOnly() {
         async savePreloadOnly() {
Line 4,459: Line 4,482:
                 String(
                 String(
                     this.state.preloadTitle ||
                     this.state.preloadTitle ||
                        ''
                    ''
                 ).trim();
                 ).trim();


Line 4,479: Line 4,502:
             }
             }


            /*
            * Always pull the actual CodeMirror
            * document before saving.
            */
             this.state.preloadContent =
             this.state.preloadContent =
                 this.getEditorContent();
                 this.getEditorContent();
Line 4,529: Line 4,556:
                     'Preload saved.',
                     'Preload saved.',
                     {
                     {
                         type: 'success'
                         type:
                            'success'
                     }
                     }
                 );
                 );
Line 4,550: Line 4,578:
                         'Could not save preload.',
                         'Could not save preload.',
                     {
                     {
                         type: 'error'
                         type:
                            'error'
                     }
                     }
                 );
                 );
Line 4,560: Line 4,589:
                 String(
                 String(
                     this.state.preloadTitle ||
                     this.state.preloadTitle ||
                        ''
                    ''
                 ).trim();
                 ).trim();


Line 4,580: Line 4,609:
             }
             }


            /*
            * Always use the actual CodeMirror
            * document when saving.
            */
             this.state.preloadContent =
             this.state.preloadContent =
                 this.getEditorContent();
                 this.getEditorContent();


             /*
             /*
             * Ensure the latest editor content is
             * Make sure the configuration is
             * represented in the config before saving.
             * synchronized with anything newly
            * typed into the preload.
            *
            * This means even if the user forgot
            * to click Detect variables, saving
            * will still catch new variables.
             */
             */
             this.reanalyze();
             this.reanalyze();
Line 4,660: Line 4,698:
                     'Preload and config saved.',
                     'Preload and config saved.',
                     {
                     {
                         type: 'success'
                         type:
                            'success'
                     }
                     }
                 );
                 );
Line 4,681: Line 4,720:
                         'Could not save preload/config.',
                         'Could not save preload/config.',
                     {
                     {
                         type: 'error'
                         type:
                            'error'
                     }
                     }
                 );
                 );
Line 4,694: Line 4,734:
                 String(
                 String(
                     this.state.preloadTitle ||
                     this.state.preloadTitle ||
                        ''
                    ''
                 ).trim();
                 ).trim();


Line 4,754: Line 4,794:


             /*
             /*
             * Synchronize configuration one last
             * Synchronize configuration one final
             * time before validation.
             * time before saving.
            *
            * This catches variables even if the
            * user never clicked Detect variables.
             */
             */
             this.state.config =
             this.state.config =
Line 4,852: Line 4,895:
                 ),
                 ),
                 {
                 {
                     type: 'error'
                     type:
                        'error'
                 }
                 }
             );
             );