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 31: Line 31:
         // API
         // API


async getPage(title) {
        async getPage(title) {
    const response = await new mw.Api().get({
            const response = await new mw.Api().get({
        action: 'query',
                action: 'query',
        prop: 'revisions',
                prop: 'revisions',
        titles: title,
                titles: title,
        rvprop: 'content',
                rvprop: 'content',
        rvslots: 'main',
                rvslots: 'main',
        formatversion: 2
                formatversion: 2
    });
            });
 
    const page =
            const page =
        response &&
                response &&
        response.query &&
                response.query &&
        response.query.pages &&
                response.query.pages &&
        response.query.pages[0];
                response.query.pages[0];
 
    if (!page || page.missing) return null;
            if (!page || page.missing) return null;
 
    const content =
            const content =
        page.revisions &&
                page.revisions &&
        page.revisions[0] &&
                page.revisions[0] &&
        page.revisions[0].slots &&
                page.revisions[0].slots &&
        page.revisions[0].slots.main &&
                page.revisions[0].slots.main &&
        page.revisions[0].slots.main.content;
                page.revisions[0].slots.main.content;
 
    return {
            return {
        title: page.title || title,
                title: page.title || title,
        content: typeof content === 'string' ? content : ''
                content: typeof content === 'string' ? content : ''
    };
            };
},
        },


         async pageExists(title) {
         async pageExists(title) {
Line 76: Line 76:
             });
             });


             return response?.query?.search || [];
             return (
                response &&
                response.query &&
                response.query.search
            ) || [];
         },
         },


Line 99: Line 103:
                 /<includeonly>([\s\S]*?)<\/includeonly>/i
                 /<includeonly>([\s\S]*?)<\/includeonly>/i
             );
             );
             return match ? match[1].trim() : String(source || '').trim();
 
             return match
                ? match[1].trim()
                : String(source || '').trim();
         },
         },


Line 109: Line 116:
             while ((match = pattern.exec(content)) !== null) {
             while ((match = pattern.exec(content)) !== null) {
                 const name = match[1].toUpperCase();
                 const name = match[1].toUpperCase();
                 if (!variables.includes(name)) variables.push(name);
 
                 if (!variables.includes(name)) {
                    variables.push(name);
                }
             }
             }


Line 119: Line 129:
             const pattern =
             const pattern =
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
             let match;
             let match;


Line 136: Line 147:


             this.findConditions(content).forEach(condition => {
             this.findConditions(content).forEach(condition => {
                 if (condition.type !== 'if' &&
                 if (
                     condition.type !== 'elseif') return;
                    condition.type !== 'if' &&
                     condition.type !== 'elseif'
                ) {
                    return;
                }


                 const matches =
                 const matches =
                     condition.expression.match(/[A-Za-z0-9_-]+/g) || [];
                     condition.expression.match(
                        /[A-Za-z0-9_-]+/g
                    ) || [];


                 matches.forEach(name => {
                 matches.forEach(name => {
                     name = name.toUpperCase();
                     name = name.toUpperCase();
                     if (!variables.includes(name)) variables.push(name);
 
                     if (!variables.includes(name)) {
                        variables.push(name);
                    }
                 });
                 });
             });
             });
Line 155: Line 175:
                 variables: this.findVariables(content),
                 variables: this.findVariables(content),
                 conditions: this.findConditions(content),
                 conditions: this.findConditions(content),
                 conditionVariables: this.findConditionVariables(content)
                 conditionVariables:
                    this.findConditionVariables(content)
             };
             };
         },
         },
Line 163: Line 184:
         emptyConfig() {
         emptyConfig() {
             return {
             return {
                 settings: { incomplete: 'add-stub' },
                 settings: {
                    incomplete: 'add-stub'
                },
                 fields: {
                 fields: {
                     PAGE: {
                     PAGE: {
Line 178: Line 201:


         normalizeConfig(config) {
         normalizeConfig(config) {
             if (!config ||
             if (
                !config ||
                 typeof config !== 'object' ||
                 typeof config !== 'object' ||
                 Array.isArray(config)) {
                 Array.isArray(config)
            ) {
                 config = this.emptyConfig();
                 config = this.emptyConfig();
             }
             }


             if (!config.settings ||
             if (
                !config.settings ||
                 typeof config.settings !== 'object' ||
                 typeof config.settings !== 'object' ||
                 Array.isArray(config.settings)) {
                 Array.isArray(config.settings)
            ) {
                 config.settings = {};
                 config.settings = {};
             }
             }


             if (!config.fields ||
             if (
                !config.fields ||
                 typeof config.fields !== 'object' ||
                 typeof config.fields !== 'object' ||
                 Array.isArray(config.fields)) {
                 Array.isArray(config.fields)
            ) {
                 config.fields = {};
                 config.fields = {};
             }
             }
Line 214: Line 243:
                 label: this.humanizeName(name),
                 label: this.humanizeName(name),
                 description: '',
                 description: '',
                 type: analysis.conditionVariables.includes(name)
                 type:
                    ? 'boolean'
                    analysis.conditionVariables.includes(name)
                    : 'text'
                        ? 'boolean'
                        : 'text'
             };
             };
         },
         },
Line 226: Line 256:
                 .replace(/\s+/g, ' ')
                 .replace(/\s+/g, ' ')
                 .trim()
                 .trim()
                 .replace(/\b\w/g, char => char.toUpperCase());
                 .replace(
                    /\b\w/g,
                    char => char.toUpperCase()
                );
         },
         },


Line 246: Line 279:
             analysis.variables.forEach(name => {
             analysis.variables.forEach(name => {
                 if (name === 'PAGE') return;
                 if (name === 'PAGE') return;
                 fields[name] =
                 fields[name] =
                     existing[name] ||
                     existing[name] ||
                     this.createDefaultField(name, analysis);
                     this.createDefaultField(
                        name,
                        analysis
                    );
             });
             });


             Object.keys(existing).forEach(name => {
             Object.keys(existing).forEach(name => {
                 const upper = name.toUpperCase();
                 const upper = name.toUpperCase();
                 if (!fields[upper]) fields[upper] = existing[name];
 
                 if (!fields[upper]) {
                    fields[upper] = existing[name];
                }
             });
             });


             config.fields = fields;
             config.fields = fields;
             return config;
             return config;
         },
         },
Line 264: Line 305:
             const warnings = [];
             const warnings = [];


             if (!config ||
             if (
                !config ||
                 typeof config !== 'object' ||
                 typeof config !== 'object' ||
                 Array.isArray(config)) {
                 Array.isArray(config)
            ) {
                 return {
                 return {
                     valid: false,
                     valid: false,
Line 274: Line 317:
             }
             }


             if (!config.fields ||
             if (
                !config.fields ||
                 typeof config.fields !== 'object' ||
                 typeof config.fields !== 'object' ||
                 Array.isArray(config.fields)) {
                 Array.isArray(config.fields)
            ) {
                 return {
                 return {
                     valid: false,
                     valid: false,
Line 285: Line 330:


             const configured =
             const configured =
                 Object.keys(config.fields).map(name => name.toUpperCase());
                 Object.keys(config.fields).map(
                    name => name.toUpperCase()
                );


             analysis.variables.forEach(name => {
             analysis.variables.forEach(name => {
                 if (!configured.includes(name)) {
                 if (!configured.includes(name)) {
                     errors.push(
                     errors.push(
                         'Variable "' + name + '" is not configured.'
                         'Variable "' +
                        name +
                        '" is not configured.'
                     );
                     );
                 }
                 }
Line 298: Line 347:
                 if (!analysis.variables.includes(name)) {
                 if (!analysis.variables.includes(name)) {
                     warnings.push(
                     warnings.push(
                         'Field "' + name + '" is not used by the preload.'
                         'Field "' +
                        name +
                        '" is not used by the preload.'
                     );
                     );
                 }
                 }
Line 304: Line 355:


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


             Object.entries(config.fields).forEach(([name, field]) => {
             Object.entries(config.fields).forEach(
                if (!field ||
                ([name, field]) => {
                    typeof field !== 'object' ||
                    if (
                    Array.isArray(field)) {
                        !field ||
                    errors.push(
                        typeof field !== 'object' ||
                        'Field "' + name + '" must be an object.'
                        Array.isArray(field)
                    );
                    ) {
                     return;
                        errors.push(
                }
                            'Field "' +
                            name +
                            '" must be an object.'
                        );
                        return;
                    }
 
                     if (!field.type) {
                        warnings.push(
                            'Field "' +
                            name +
                            '" has no type.'
                        );
                    }


                if (!field.type) {
                    if (
                    warnings.push(
                        (
                        'Field "' + name + '" has no type.'
                            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.'
                        );
                    }
                 }
                 }
 
             );
                if ((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.'
                    );
                }
             });


             return {
             return {
Line 344: Line 411:
         // Transformation helpers
         // Transformation helpers


         transformationObject(transform, prefix, suffix, from, to) {
         transformationObject(
            transform,
            prefix,
            suffix,
            from,
            to
        ) {
             if (!transform) return null;
             if (!transform) return null;


Line 361: Line 434:
                     return from === ''
                     return from === ''
                         ? null
                         ? null
                         : { transform, from, to: to || '' };
                         : {
                            transform,
                            from,
                            to: to || ''
                        };


                 default:
                 default:
Line 374: Line 451:
                 .addClass('preload-editor-section');
                 .addClass('preload-editor-section');


             if (className) section.addClass(className);
             if (className) {
                section.addClass(className);
            }


             $('<h2>')
             $('<h2>')
                 .addClass('preload-editor-section-title')
                 .addClass(
                    'preload-editor-section-title'
                )
                 .text(title)
                 .text(title)
                 .appendTo(section);
                 .appendTo(section);
Line 390: Line 471:
                 .text(label);
                 .text(label);


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


Line 415: Line 502:


             row.append(input);
             row.append(input);
             return row;
             return row;
         },
         },
Line 422: Line 510:
                 .attr({
                 .attr({
                     type: 'text',
                     type: 'text',
                     value: value == null ? '' : value,
                     value:
                     placeholder: placeholder || ''
                        value == null
                            ? ''
                            : value,
                     placeholder:
                        placeholder || ''
                 })
                 })
                 .addClass('preload-editor-input');
                 .addClass('preload-editor-input');
Line 430: Line 522:
         textarea(value) {
         textarea(value) {
             return $('<textarea>')
             return $('<textarea>')
                 .val(value == null ? '' : value)
                 .val(
                 .addClass('preload-editor-textarea');
                    value == null
                        ? ''
                        : value
                )
                 .addClass(
                    'preload-editor-textarea'
                );
         },
         },


Line 438: Line 536:
                 .attr('type', 'checkbox')
                 .attr('type', 'checkbox')
                 .prop('checked', !!checked)
                 .prop('checked', !!checked)
                 .addClass('preload-editor-checkbox');
                 .addClass(
                    'preload-editor-checkbox'
                );
         },
         },


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


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


             const { view, inputHandler } = this.editor;
             const {
                view,
                inputHandler
            } = this.editor;


             if (view && inputHandler) {
             if (view && inputHandler) {
                 view.dom.removeEventListener('input', inputHandler);
                 view.dom.removeEventListener(
                    'input',
                    inputHandler
                );
             }
             }


             if (view) view.destroy();
             if (view) {
                view.destroy();
            }


             this.editor = null;
             this.editor = null;
Line 486: Line 604:


         getEditorContent() {
         getEditorContent() {
             if (this.editorView && this.editorView.state) {
             if (
                this.editorView &&
                this.editorView.state
            ) {
                 return this.editorView.state.doc.toString();
                 return this.editorView.state.doc.toString();
             }
             }
Line 501: Line 622:
             }
             }


             const current = this.editorView.state.doc.toString();
             const current =
                this.editorView.state.doc.toString();
 
             if (current === content) return;
             if (current === content) return;


Line 514: Line 637:


         getEditorSelection() {
         getEditorSelection() {
             if (!this.editorView?.state) return null;
             if (
 
                !this.editorView ||
             const selection = this.editorView.state.selection.main;
                !this.editorView.state
            ) {
                return null;
            }
 
             const selection =
                this.editorView.state.selection.main;


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


         focusEditor() {
         focusEditor() {
             if (this.editorView) this.editorView.focus();
             if (this.editorView) {
                this.editorView.focus();
            }
         },
         },


Line 541: Line 673:


             const view = this.editorView;
             const view = this.editorView;
             const selection = view.state.selection.main;
             const selection =
                view.state.selection.main;


             const from =
             const from =
Line 559: Line 692:


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


Line 569: Line 708:


         wrapSelectionWithVariable() {
         wrapSelectionWithVariable() {
             const selected = this.getEditorSelection();
             const selected =
                this.getEditorSelection();


             if (!selected || selected.from === selected.to) {
             if (
                !selected ||
                selected.from === selected.to
            ) {
                 this.focusEditor();
                 this.focusEditor();
                 return;
                 return;
Line 577: Line 720:


             const replacement =
             const replacement =
                 '%%' + selected.text + '%%';
                 '%%' +
                selected.text +
                '%%';


             this.insertEditorText(
             this.insertEditorText(
Line 583: Line 728:
                 selected.from,
                 selected.from,
                 selected.to,
                 selected.to,
                 selected.from + replacement.length
                 selected.from +
                    replacement.length
             );
             );
         },
         },


         insertVariable(name) {
         insertVariable(name) {
             name = String(name || '').trim().toUpperCase();
             name = String(name || '')
                .trim()
                .toUpperCase();
 
             if (!name) return;
             if (!name) return;


             const selected = this.getEditorSelection();
             const selected =
                this.getEditorSelection();
 
             if (!selected) return;
             if (!selected) return;


             const text = '%%' + name + '%%';
             const text =
                '%%' +
                name +
                '%%';


             this.insertEditorText(
             this.insertEditorText(
Line 604: Line 758:


         insertCondition(type, variable) {
         insertCondition(type, variable) {
             type = String(type || '').toLowerCase();
             type =
             variable = String(variable || '').trim().toUpperCase();
                String(type || '')
                    .toLowerCase();
 
             variable =
                String(variable || '')
                    .trim()
                    .toUpperCase();
 
            const selected =
                this.getEditorSelection();


            const selected = this.getEditorSelection();
             if (!selected) return;
             if (!selected) return;


Line 615: Line 777:
             switch (type) {
             switch (type) {
                 case 'if': {
                 case 'if': {
                     const opening = '<!-- @if ' + variable + ' -->';
                     if (!variable) return;
                    const closing = '<!-- @endif -->';


                     if (selected.from !== selected.to) {
                    const opening =
                        '<!-- @if ' +
                        variable +
                        ' -->';
 
                    const closing =
                        '<!-- @endif -->';
 
                     if (
                        selected.from !==
                        selected.to
                    ) {
                         text =
                         text =
                             opening +
                             opening +
Line 625: Line 797:
                             '\n' +
                             '\n' +
                             closing;
                             closing;
                         cursorOffset = text.length;
 
                         cursorOffset =
                            text.length;
                     } else {
                     } else {
                         text =
                         text =
Line 631: Line 805:
                             '\n\n' +
                             '\n\n' +
                             closing;
                             closing;
                         cursorOffset = opening.length + 1;
 
                         cursorOffset =
                            opening.length + 1;
                     }
                     }


Line 660: Line 836:
                 selected.from,
                 selected.from,
                 selected.to,
                 selected.to,
                 selected.from + cursorOffset
                 selected.from +
                    cursorOffset
             );
             );
         },
         },


         getToolVariables() {
         getToolVariables() {
             const variables = new Set(['PAGE']);
             const variables =
                new Set(['PAGE']);


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


Line 678: Line 863:
         createToolSelect(options, className) {
         createToolSelect(options, className) {
             const select = $('<select>')
             const select = $('<select>')
                 .addClass('preload-editor-tool-select');
                 .addClass(
                    'preload-editor-tool-select'
                );


             if (className) select.addClass(className);
             if (className) {
                select.addClass(className);
            }


             options.forEach(option => {
             options.forEach(option => {
                 $('<option>')
                 $('<option>')
                     .attr('value', option.value)
                     .attr(
                        'value',
                        option.value
                    )
                     .text(option.label)
                     .text(option.label)
                     .appendTo(select);
                     .appendTo(select);
Line 695: Line 887:
             if (!this.editorTools) return;
             if (!this.editorTools) return;


             const variables = this.getToolVariables();
             const variables =
                this.getToolVariables();
 
             const current =
             const current =
                 this.editorTools.variableSelect.val();
                 this.editorTools.variableSelect.val();
             const currentCondition =
             const currentCondition =
                 this.editorTools.conditionVariableSelect.val();
                 this.editorTools
                    .conditionVariableSelect
                    .val();


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


                 variables.forEach(name => {
                 variables.forEach(name => {
                     $('<option>')
                     $('<option>')
                         .attr('value', name)
                         .attr(
                            'value',
                            name
                        )
                         .text(name)
                         .text(name)
                         .appendTo(select);
                         .appendTo(select);
                 });
                 });


                 if (value && variables.includes(value)) {
                 if (
                    value &&
                    variables.includes(value)
                ) {
                     select.val(value);
                     select.val(value);
                 } else if (variables.length) {
                 } else if (variables.length) {
                     select.val(variables[0]);
                     select.val(
                        variables[0]
                    );
                 }
                 }
             };
             };
Line 724: Line 932:


             populate(
             populate(
                 this.editorTools.conditionVariableSelect,
                 this.editorTools
                    .conditionVariableSelect,
                 currentCondition
                 currentCondition
             );
             );
Line 733: Line 942:


             const type =
             const type =
                 this.editorTools.conditionTypeSelect.val();
                 this.editorTools
                    .conditionTypeSelect
                    .val();


             this.editorTools.conditionVariableWrap.toggle(
             this.editorTools
                type === 'if'
                .conditionVariableWrap
            );
                .toggle(type === 'if');
         },
         },


         renderEditorTools() {
         renderEditorTools() {
             const tools = $('<div>')
             const tools = $('<div>')
                 .addClass('preload-editor-tools');
                 .addClass(
                    'preload-editor-tools'
                );


             // Wrap selection
             // Wrap selection
Line 748: Line 961:
             const wrapGroup = $('<div>')
             const wrapGroup = $('<div>')
                 .addClass(
                 .addClass(
                     'preload-editor-tool-group preload-editor-tool-group-wrap'
                     'preload-editor-tool-group ' +
                    'preload-editor-tool-group-wrap'
                 );
                 );


Line 755: Line 969:
                     'Wrap selection with %%',
                     'Wrap selection with %%',
                     'preload-editor-tool-button',
                     'preload-editor-tool-button',
                     () => this.wrapSelectionWithVariable()
                     () =>
                        this.wrapSelectionWithVariable()
                 )
                 )
             );
             );
Line 763: Line 978:
             const variableGroup = $('<div>')
             const variableGroup = $('<div>')
                 .addClass(
                 .addClass(
                     'preload-editor-tool-group preload-editor-tool-group-variable'
                     'preload-editor-tool-group ' +
                    'preload-editor-tool-group-variable'
                 );
                 );


             $('<span>')
             $('<span>')
                 .addClass('preload-editor-tool-label')
                 .addClass(
                 .text('Insert existing variable')
                    'preload-editor-tool-label'
                )
                 .text(
                    'Insert existing variable'
                )
                 .appendTo(variableGroup);
                 .appendTo(variableGroup);


             const variableSelect = this.createToolSelect(
             const variableSelect =
                [],
                this.createToolSelect(
                'preload-editor-variable-select'
                    [],
            );
                    'preload-editor-variable-select'
                );


             variableGroup.append(
             variableGroup.append(
Line 781: Line 1,002:
                     'Insert',
                     'Insert',
                     'preload-editor-tool-button',
                     'preload-editor-tool-button',
                     () => this.insertVariable(variableSelect.val())
                     () =>
                        this.insertVariable(
                            variableSelect.val()
                        )
                 )
                 )
             );
             );
Line 789: Line 1,013:
             const conditionGroup = $('<div>')
             const conditionGroup = $('<div>')
                 .addClass(
                 .addClass(
                     'preload-editor-tool-group preload-editor-tool-group-condition'
                     'preload-editor-tool-group ' +
                    'preload-editor-tool-group-condition'
                 );
                 );


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


             const conditionTypeSelect = this.createToolSelect(
             const conditionTypeSelect =
                [
                this.createToolSelect(
                    { value: 'if', label: '@if' },
                    [
                    { value: 'elseif', label: '@elseif' },
                        {
                    { value: 'else', label: '@else' },
                            value: 'if',
                    { value: 'endif', label: '@endif' }
                            label: '@if'
                ],
                        },
                'preload-editor-condition-type-select'
                        {
            );
                            value: 'elseif',
                            label: '@elseif'
                        },
                        {
                            value: 'else',
                            label: '@else'
                        },
                        {
                            value: 'endif',
                            label: '@endif'
                        }
                    ],
                    'preload-editor-condition-type-select'
                );


             const conditionVariableSelect = this.createToolSelect(
             const conditionVariableSelect =
                [],
                this.createToolSelect(
                'preload-editor-condition-variable-select'
                    [],
            );
                    'preload-editor-condition-variable-select'
                );


             const conditionVariableWrap = $('<span>')
             const conditionVariableWrap =
                .addClass(
                $('<span>')
                    'preload-editor-condition-variable'
                    .addClass(
                )
                        'preload-editor-condition-variable'
                .append(conditionVariableSelect);
                    )
 
                    .append(
             const insertCondition = this.createButton(
                        conditionVariableSelect
                'Insert',
                    );
                'preload-editor-tool-button',
 
                () => this.insertCondition(
             const insertCondition =
                    conditionTypeSelect.val(),
                this.createButton(
                    conditionVariableSelect.val()
                    'Insert',
                )
                    'preload-editor-tool-button',
            );
                    () =>
                        this.insertCondition(
                            conditionTypeSelect.val(),
                            conditionVariableSelect.val()
                        )
                );


             conditionTypeSelect.on(
             conditionTypeSelect.on(
                 'change.preloadEditor',
                 'change.preloadEditor',
                 () => this.updateConditionVariableVisibility()
                 () =>
                    this.updateConditionVariableVisibility()
             );
             );


Line 867: Line 1,114:


         getKnownVariables() {
         getKnownVariables() {
             const known = new Set(['PAGE']);
             const known =
             const fields = this.state.config?.fields;
                new Set(['PAGE']);
 
             const fields =
                this.state.config &&
                this.state.config.fields;


             if (fields && typeof fields === 'object') {
             if (
                 Object.keys(fields).forEach(name => {
                fields &&
                    known.add(String(name).toUpperCase());
                typeof fields === 'object'
                 });
            ) {
                 Object.keys(fields).forEach(
                    name => {
                        known.add(
                            String(name).toUpperCase()
                        );
                    }
                 );
             }
             }


Line 879: Line 1,137:
         },
         },


         createSyntaxDecorations(state, Decoration) {
         createSyntaxDecorations(
            state,
            Decoration
        ) {
             const decorations = [];
             const decorations = [];
             const source = state.doc.toString();
             const source =
             const knownVariables = this.getKnownVariables();
                state.doc.toString();
 
             const knownVariables =
                this.getKnownVariables();
 
            const variablePattern =
                /%%([A-Za-z0-9_-]+)%%/g;


            const variablePattern = /%%([A-Za-z0-9_-]+)%%/g;
             let match;
             let match;


             while ((match = variablePattern.exec(source)) !== null) {
             while (
                 const name = match[1].toUpperCase();
                (match =
                 const known = knownVariables.has(name);
                    variablePattern.exec(source)) !==
                null
            ) {
                 const name =
                    match[1].toUpperCase();
 
                 const known =
                    knownVariables.has(name);


                 decorations.push(
                 decorations.push(
Line 901: Line 1,174:
                     }).range(
                     }).range(
                         match.index,
                         match.index,
                         match.index + match[0].length
                         match.index +
                            match[0].length
                     )
                     )
                 );
                 );
Line 909: Line 1,183:
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;
                 /<!--\s*@(if|elseif|else|endif)\b([\s\S]*?)-->/gi;


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


                 const directiveStart =
                 const directiveStart =
                     match.index + match[0].indexOf('@');
                     match.index +
                    match[0].indexOf('@');


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


                 const type = match[1].toLowerCase();
                 const type =
                    match[1].toLowerCase();


                 if (type !== 'if' && type !== 'elseif') continue;
                 if (
                    type !== 'if' &&
                    type !== 'elseif'
                ) {
                    continue;
                }
 
                const expression =
                    match[2];


                const expression = match[2];
                 const expressionStart =
                 const expressionStart =
                     match.index + match[0].indexOf(expression);
                     match.index +
                    match[0].indexOf(
                        expression
                    );
 
                const expressionPattern =
                    /[A-Za-z0-9_-]+/g;


                const expressionPattern = /[A-Za-z0-9_-]+/g;
                 let expressionMatch;
                 let expressionMatch;


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


                     if (!knownVariables.has(name)) continue;
                     if (
                        !knownVariables.has(name)
                    ) {
                        continue;
                    }


                     const start =
                     const start =
                         expressionStart + expressionMatch.index;
                         expressionStart +
                        expressionMatch.index;


                     decorations.push(
                     decorations.push(
Line 960: Line 1,267:
                         }).range(
                         }).range(
                             start,
                             start,
                             start + expressionMatch[0].length
                             start +
                                expressionMatch[0]
                                    .length
                         )
                         )
                     );
                     );
Line 966: Line 1,275:
             }
             }


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


         async installSyntaxHighlighting(view) {
         async installSyntaxHighlighting(view) {
             await mw.loader.using('ext.CodeMirror.v6.lib');
             await mw.loader.using(
                'ext.CodeMirror.v6.lib'
            );
 
            const require =
                mw.loader.require;


            const require = mw.loader.require;
             const {
             const {
                 StateField,
                 StateField,
Line 978: Line 1,294:
                 EditorView,
                 EditorView,
                 Decoration
                 Decoration
             } = require('ext.CodeMirror.v6.lib');
             } = require(
                'ext.CodeMirror.v6.lib'
            );
 
            const syntaxField =
                StateField.define({
                    create: state =>
                        this.createSyntaxDecorations(
                            state,
                            Decoration
                        ),


            const syntaxField = StateField.define({
                    update: (
                create: state =>
                        decorations,
                    this.createSyntaxDecorations(state, Decoration),
                        transaction
                    ) =>
                        transaction.docChanged
                            ? this.createSyntaxDecorations(
                                transaction.state,
                                Decoration
                            )
                            : decorations,


                update: (decorations, transaction) =>
                    provide: field =>
                    transaction.docChanged
                        EditorView.decorations.from(
                        ? this.createSyntaxDecorations(
                             field
                             transaction.state,
                            Decoration
                         )
                         )
                        : decorations,
                 });
 
                 provide: field =>
                    EditorView.decorations.from(field)
            });


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


             this.editor.syntaxField = syntaxField;
             this.editor.syntaxField =
                syntaxField;
         },
         },


Line 1,006: Line 1,337:


         async mountCodeMirror(textarea) {
         async mountCodeMirror(textarea) {
             return new Promise((resolve, reject) => {
             return new Promise(
                let settled = false;
                (resolve, reject) => {
                    let settled = false;


                const cleanup = () => {
                    const cleanup = () => {
                    mw.hook('dev.CCM.ready').remove(onReady);
                        mw.hook(
                    mw.hook('dev.CCM.load').remove(onLoad);
                            'dev.CCM.ready'
                };
                        ).remove(onReady);


                const onReady = async cm => {
                        mw.hook(
                    if (
                            'dev.CCM.load'
                        settled ||
                         ).remove(onLoad);
                        !cm ||
                     };
                        cm.textarea !== textarea ||
                         !cm.view
                     ) return;


                     settled = true;
                     const onReady = async cm => {
                    cleanup();
                        if (
                            settled ||
                            !cm ||
                            cm.textarea !==
                                textarea ||
                            !cm.view
                        ) {
                            return;
                        }


                    const view = cm.view;
                        settled = true;
                        cleanup();


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


                         this.state.dirty = true;
                         const inputHandler =
                        this.reanalyze();
                            () => {
                        this.renderVariablesBySelector();
                                this.state
                        this.renderAnalysisBySelector();
                                    .preloadContent =
                        this.renderConfigPreviewBySelector();
                                    view.state.doc.toString();
                        this.updateEditorTools();
                    };


                    view.dom.addEventListener(
                                this.state.dirty =
                        'input',
                                    true;
                        inputHandler
                    );


                    view.dom.classList.add(
                                this.reanalyze();
                        'preload-editor-codemirror'
                                this.renderVariablesBySelector();
                    );
                                this.renderAnalysisBySelector();
                                this.renderConfigPreviewBySelector();
                                this.updateEditorTools();
                            };


                    const wrapper =
                         view.dom.addEventListener(
                         view.dom.closest(
                             'input',
                             '.ext-codemirror-wrapper'
                            inputHandler
                         );
                         );


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


                    this.editor = {
                        const wrapper =
                        cm,
                            view.dom.closest(
                        view,
                                '.ext-codemirror-wrapper'
                        textarea,
                            );
                        inputHandler
                    };


                    this.editorView = view;
                        if (wrapper) {
                    this.state.preloadContent =
                            wrapper.classList.add(
                        view.state.doc.toString();
                                'preload-editor-codemirror-wrapper'
                            );
                        }


                    try {
                         this.editor = {
                         await this.installSyntaxHighlighting(view);
                            cm,
                        resolve(view);
                            view,
                    } catch (error) {
                            textarea,
                         reject(error);
                            inputHandler
                    }
                         };
                };


                const onLoad = loader => {
                        this.editorView = view;
                    if (
 
                        settled ||
                        this.state.preloadContent =
                        typeof loader !== 'function'
                            view.state.doc.toString();
                    ) return;
 
                        try {
                            await this.installSyntaxHighlighting(
                                view
                            );
 
                            resolve(view);
                        } catch (error) {
                            reject(error);
                        }
                    };
 
                    const onLoad = loader => {
                        if (
                            settled ||
                            typeof loader !==
                                'function'
                        ) {
                            return;
                        }
 
                        try {
                            loader(textarea);
                        } catch (error) {
                            settled = true;
                            cleanup();
                            reject(error);
                        }
                    };


                     try {
                     mw.hook(
                         loader(textarea);
                         'dev.CCM.ready'
                     } catch (error) {
                     ).add(onReady);
                        settled = true;
                        cleanup();
                        reject(error);
                    }
                };


                mw.hook('dev.CCM.ready').add(onReady);
                    mw.hook(
                mw.hook('dev.CCM.load').add(onLoad);
                        'dev.CCM.load'
             });
                    ).add(onLoad);
                }
             );
         },
         },


Line 1,101: Line 1,458:


         init() {
         init() {
             const root = $('#mw-content-text');
             const root =
                $('#mw-content-text');
 
             if (!root.length) return;
             if (!root.length) return;


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


             root.append(this.root);
             root.append(this.root);
             this.renderShell();
             this.renderShell();
         },
         },
Line 1,116: Line 1,481:
             this.root.empty();
             this.root.empty();


             const loader = this.createSection('Preload');
             const loader =
                this.createSection(
                    'Preload'
                );


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


             const loadButton = this.createButton(
             const loadButton =
                'Load',
                this.createButton(
                'preload-editor-button-primary',
                    'Load',
                () => this.loadExisting(titleInput.val())
                    'preload-editor-button-primary',
            );
                    () =>
 
                        this.loadExisting(
             const newButton = this.createButton(
                            titleInput.val()
                'New',
                        )
                '',
                );
                () => this.startNew()
 
            );
             const newButton =
                this.createButton(
                    'New',
                    '',
                    () => this.startNew()
                );


             const sampleButton = this.createButton(
             const sampleButton =
                'Load Sample Page',
                this.createButton(
                '',
                    'Load Sample Page',
                () => this.loadSamplePage()
                    '',
            );
                    () =>
                        this.loadSamplePage()
                );


             loader.append(
             loader.append(
                 $('<div>')
                 $('<div>')
                     .addClass('preload-editor-toolbar')
                     .addClass(
                        'preload-editor-toolbar'
                    )
                     .append(
                     .append(
                         titleInput,
                         titleInput,
Line 1,154: Line 1,535:
             this.root.append(loader);
             this.root.append(loader);


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


             this.root.append(this.status);
             this.workspace =
                $('<div>')
                    .addClass(
                        'preload-editor-workspace'
                    );


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


            this.root.append(this.workspace);
             this.renderWelcome();
             this.renderWelcome();
         },
         },
Line 1,169: Line 1,561:
             this.workspace.empty();
             this.workspace.empty();


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


             $('<p>')
             $('<p>')
Line 1,177: Line 1,572:
                 .appendTo(section);
                 .appendTo(section);


             const tips = $('<ul>')
             const tips =
                .addClass('preload-editor-tips');
                $('<ul>')
                    .addClass(
                        'preload-editor-tips'
                    );


             [
             [
Line 1,185: Line 1,583:
                 'Sample page: loads normal wiki page content so you can turn it into a preload.'
                 'Sample page: loads normal wiki page content so you can turn it into a preload.'
             ].forEach(text => {
             ].forEach(text => {
                 $('<li>').text(text).appendTo(tips);
                 $('<li>')
                    .text(text)
                    .appendTo(tips);
             });
             });


Line 1,202: Line 1,602:
             );
             );


             if (type) this.status.addClass('is-' + type);
             if (type) {
             this.status.text(message || '');
                this.status.addClass(
                    'is-' + type
                );
            }
 
             this.status.text(
                message || ''
            );
         },
         },


Line 1,209: Line 1,616:


         async loadExisting(title) {
         async loadExisting(title) {
             title = String(title || '').trim();
             title =
                String(title || '').trim();


             if (!title) {
             if (!title) {
Line 1,216: Line 1,624:
                     { type: 'warn' }
                     { type: 'warn' }
                 );
                 );
                 return;
                 return;
             }
             }


             this.setStatus('Loading preload...', 'loading');
             this.setStatus(
                'Loading preload...',
                'loading'
            );


             try {
             try {
                 const page = await this.getPage(title);
                 const page =
                    await this.getPage(title);


                 if (!page) {
                 if (!page) {
                     const suggestions =
                     const suggestions =
                         await this.findSimilarPreloads(title);
                         await this.findSimilarPreloads(
                            title
                        );
 
                    this.renderMissingPage(
                        title,
                        suggestions
                    );


                    this.renderMissingPage(title, suggestions);
                     this.setStatus(
                     this.setStatus(
                         'Preload not found.',
                         'Preload not found.',
                         'warning'
                         'warning'
                     );
                     );
                     return;
                     return;
                 }
                 }
Line 1,246: Line 1,666:
                 );
                 );
             } catch (error) {
             } catch (error) {
                 console.error('Preload Editor:', error);
                 console.error(
                    'Preload Editor:',
                    error
                );
 
                 this.setStatus(
                 this.setStatus(
                     error.message || 'Could not load preload.',
                     error.message ||
                        'Could not load preload.',
                     'error'
                     'error'
                 );
                 );
Line 1,255: Line 1,680:


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


             if (!query) query = title;
             if (!query) {
                query = title;
            }


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


                 return results.filter(result =>
                 return results.filter(
                    /\/Preload$/i.test(result.title)
                    result =>
                        /\/Preload$/i.test(
                            result.title
                        )
                 );
                 );
             } catch (error) {
             } catch (error) {
Line 1,272: Line 1,709:
                     error
                     error
                 );
                 );
                 return [];
                 return [];
             }
             }
         },
         },


         renderMissingPage(title, suggestions) {
         renderMissingPage(
            title,
            suggestions
        ) {
             this.workspace.empty();
             this.workspace.empty();


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


             $('<p>')
             $('<p>')
                 .text('No preload exists at "' + title + '".')
                 .text(
                    'No preload exists at "' +
                    title +
                    '".'
                )
                 .appendTo(section);
                 .appendTo(section);


Line 1,290: Line 1,738:
                     .appendTo(section);
                     .appendTo(section);


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


                 suggestions.forEach(result => {
                 suggestions.forEach(
                    list.append(
                    result => {
                        this.createButton(
                        list.append(
                            result.title,
                            this.createButton(
                            'preload-editor-suggestion',
                                result.title,
                            () => this.loadExisting(result.title)
                                'preload-editor-suggestion',
                         )
                                () =>
                     );
                                    this.loadExisting(
                 });
                                        result.title
                                    )
                            )
                         );
                     }
                 );


                 section.append(list);
                 section.append(list);
Line 1,310: Line 1,766:
                     'Create this preload',
                     'Create this preload',
                     'preload-editor-button-primary',
                     'preload-editor-button-primary',
                     () => this.startNew(title)
                     () =>
                        this.startNew(title)
                 )
                 )
             );
             );
Line 1,318: Line 1,775:


         async startNew(title) {
         async startNew(title) {
             title = String(title || '').trim();
             title =
                String(title || '').trim();


             this.state = {
             this.state = {
                 preloadTitle: title,
                 preloadTitle: title,
                 preloadSource: '',
                 preloadSource: '',
                 preloadContent: this.defaults.preload,
                 preloadContent:
                 config: this.emptyConfig(),
                    this.defaults.preload,
                 config:
                    this.emptyConfig(),
                 variables: ['PAGE'],
                 variables: ['PAGE'],
                 conditions: [],
                 conditions: [],
Line 1,340: Line 1,800:


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


             if (!result) return;
             if (!result) return;
Line 1,353: Line 1,814:


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


                 if (!page) {
                 if (!page) {
                     throw new Error(
                     throw new Error(
                         'Sample page not found: ' + result
                         'Sample page not found: ' +
                        result
                     );
                     );
                 }
                 }


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


                 this.state = {
                 this.state = {
Line 1,368: Line 1,833:
                     preloadSource: '',
                     preloadSource: '',
                     preloadContent: sample,
                     preloadContent: sample,
                     config: this.emptyConfig(),
                     config:
                        this.emptyConfig(),
                     variables: ['PAGE'],
                     variables: ['PAGE'],
                     conditions: [],
                     conditions: [],
Line 1,397: Line 1,863:


         convertSampleToPreload(source) {
         convertSampleToPreload(source) {
             let content = String(source || '').trim();
             let content =
                String(source || '').trim();


             const include = content.match(
             const include =
                /<includeonly>([\s\S]*?)<\/includeonly>/i
                content.match(
            );
                    /<includeonly>([\s\S]*?)<\/includeonly>/i
                );


             if (include) content = include[1].trim();
             if (include) {
                content =
                    include[1].trim();
            }


             content = content
             content =
                .replace(
                content
                    /<noinclude>[\s\S]*?<\/noinclude>/gi,
                    .replace(
                    ''
                        /<noinclude>[\s\S]*?<\/noinclude>/gi,
                )
                        ''
                .trim();
                    )
                    .trim();


             return (
             return (
Line 1,420: Line 1,892:
         },
         },


         createPromptDialog(title, message) {
         createPromptDialog(
             return new Promise(resolve => {
            title,
                const overlay = $('<div>')
            message
                    .addClass('preload-editor-prompt-overlay');
        ) {
             return new Promise(
                resolve => {
                    const overlay =
                        $('<div>')
                            .addClass(
                                'preload-editor-prompt-overlay'
                            );


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


                $('<h2>')
                    $('<h2>')
                    .text(title)
                        .text(title)
                    .appendTo(box);
                        .appendTo(box);


                $('<p>')
                    $('<p>')
                    .text(message)
                        .text(message)
                    .appendTo(box);
                        .appendTo(box);


                const input = this.textInput();
                    const input =
                        this.textInput();


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


                const cancel = this.createButton(
                    const cancel =
                    'Cancel',
                        this.createButton(
                    '',
                            'Cancel',
                    () => {
                            '',
                        overlay.remove();
                            () => {
                        resolve(null);
                                overlay.remove();
                     }
                                resolve(null);
                );
                            }
                        );
 
                     const confirm =
                        this.createButton(
                            'Load',
                            'preload-editor-button-primary',
                            () => {
                                const value =
                                    String(
                                        input.val() ||
                                        ''
                                    ).trim();


                const confirm = this.createButton(
                                if (!value) return;
                    'Load',
                    'preload-editor-button-primary',
                    () => {
                        const value =
                            String(input.val() || '').trim();


                        if (!value) return;
                                overlay.remove();
                                resolve(value);
                            }
                        );


                        overlay.remove();
                    buttons.append(
                         resolve(value);
                         cancel,
                     }
                        confirm
                );
                     );


                buttons.append(cancel, confirm);
                    box.append(
                box.append(input, buttons);
                        input,
                overlay.append(box);
                        buttons
                $(document.body).append(overlay);
                    );


                input.trigger('focus');
                    overlay.append(box);
                    $(document.body)
                        .append(overlay);


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


                        if (event.key === 'Escape') {
        async loadDefinition(
                            cancel.trigger('click');
            title,
                        }
            source
                     }
        ) {
            const content =
                this.extractIncludeonly(
                     source
                 );
                 );
            });
        },


        async loadDefinition(title, source) {
             let config =
            const content = this.extractIncludeonly(source);
                this.emptyConfig();
             let config = this.emptyConfig();


             const configTitle =
             const configTitle =
Line 1,495: Line 2,014:


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


             if (configPage) {
             if (configPage) {
                 try {
                 try {
                     config = JSON.parse(configPage.content);
                     config =
                        JSON.parse(
                            configPage.content
                        );
                 } catch (error) {
                 } catch (error) {
                     throw new Error(
                     throw new Error(
Line 1,509: Line 2,033:


             const analysis =
             const analysis =
                 this.analyzePreload(content);
                 this.analyzePreload(
                    content
                );


             config = this.prepareConfigForVariables(
             config =
                config,
                this.prepareConfigForVariables(
                analysis
                    config,
            );
                    analysis
                );


             this.state = {
             this.state = {
Line 1,521: Line 2,048:
                 preloadContent: source,
                 preloadContent: source,
                 config,
                 config,
                 variables: analysis.variables,
                 variables:
                 conditions: analysis.conditions,
                    analysis.variables,
                 conditions:
                    analysis.conditions,
                 mode: 'edit',
                 mode: 'edit',
                 dirty: false
                 dirty: false
Line 1,536: Line 2,065:
             this.workspace.empty();
             this.workspace.empty();


             const layout = $('<div>')
             const layout =
                .addClass('preload-editor-layout');
                $('<div>')
                    .addClass(
                        'preload-editor-layout'
                    );


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


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


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


             const meta = $('<div>')
             const meta =
                .addClass('preload-editor-meta');
                $('<div>')
                    .addClass(
                        'preload-editor-meta'
                    );


             $('<strong>')
             $('<strong>')
                 .text(this.state.preloadTitle || 'New preload')
                 .text(
                    this.state.preloadTitle ||
                    'New preload'
                )
                 .appendTo(meta);
                 .appendTo(meta);


             if (this.state.mode === 'new') {
             if (
                this.state.mode ===
                'new'
            ) {
                 $('<span>')
                 $('<span>')
                     .addClass('preload-editor-badge')
                     .addClass(
                        'preload-editor-badge'
                    )
                     .text('NEW')
                     .text('NEW')
                     .appendTo(meta);
                     .appendTo(meta);
Line 1,570: Line 2,120:
             );
             );


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


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


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


             const analysisSection =
             const analysisSection =
                 this.createSection('Analysis');
                 this.createSection(
                    'Analysis'
                );


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


             analysisSection.append(analysisContainer);
             analysisSection.append(
                analysisContainer
            );


             const variablesSection =
             const variablesSection =
                 this.createSection('Variables');
                 this.createSection(
                    'Variables'
                );


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


             variablesSection.append(variableContainer);
             variablesSection.append(
                variableContainer
            );


             const configSection =
             const configSection =
                 this.createSection('Generated configuration');
                 this.createSection(
                    'Generated configuration'
                );


             const configPreview = $('<pre>')
             const configPreview =
                .addClass('preload-editor-config-preview');
                $('<pre>')
                    .addClass(
                        'preload-editor-config-preview'
                    );


             configSection.append(configPreview);
             configSection.append(
                configPreview
            );


             const actions = $('<div>')
             const actions =
                .addClass('preload-editor-actions');
                $('<div>')
                    .addClass(
                        'preload-editor-actions'
                    );


             actions.append(
             actions.append(
Line 1,630: Line 2,216:
             );
             );


             layout.append(left, right);
             layout.append(
             this.workspace.append(layout);
                left,
                right
            );
 
             this.workspace.append(
                layout
            );


             this.reanalyze();
             this.reanalyze();
             this.renderVariables(variableContainer);
 
             this.renderAnalysis(analysisContainer);
             this.renderVariables(
             this.renderConfigPreview(configPreview);
                variableContainer
            );
 
             this.renderAnalysis(
                analysisContainer
            );
 
             this.renderConfigPreview(
                configPreview
            );
 
             this.updateEditorTools();
             this.updateEditorTools();


             this.mountCodeMirror(textarea.get(0)).catch(error => {
             this.mountCodeMirror(
                textarea.get(0)
            ).catch(error => {
                 console.error(
                 console.error(
                     'Preload Editor CodeMirror:',
                     'Preload Editor CodeMirror:',
Line 1,654: Line 2,258:


         reanalyze() {
         reanalyze() {
             const source = this.getEditorContent();
             const source =
             const content = this.extractIncludeonly(source);
                this.getEditorContent();
            const analysis = this.analyzePreload(content);
 
             const content =
                this.extractIncludeonly(
                    source
                );


             this.state.preloadContent = source;
            const analysis =
             this.state.variables = analysis.variables;
                this.analyzePreload(
             this.state.conditions = analysis.conditions;
                    content
                );
 
             this.state.preloadContent =
                source;
 
             this.state.variables =
                analysis.variables;
 
             this.state.conditions =
                analysis.conditions;


             this.state.config =
             this.state.config =
Line 1,669: Line 2,287:
                 );
                 );


             this.analysis = analysis;
             this.analysis =
                analysis;
         },
         },


Line 1,683: Line 2,302:
                 );
                 );


             const variableCount = analysis.variables.length;
             const variableCount =
            const conditionCount = analysis.conditions.length;
                analysis.variables.length;


             const summary = $('<div>')
             const conditionCount =
                 .addClass('preload-editor-analysis-summary');
                 analysis.conditions.length;


             summary.append(
            const summary =
                $('<div>')
                    .addClass(
                        'preload-editor-analysis-summary'
                    );
 
             summary.append(
                 $('<span>').text(
                 $('<span>').text(
                     variableCount +
                     variableCount +
                     ' variable' +
                     ' variable' +
                     (variableCount === 1 ? '' : 's')
                     (
                        variableCount === 1
                            ? ''
                            : 's'
                    )
                 ),
                 ),
                 $('<span>').text(
                 $('<span>').text(
                     conditionCount +
                     conditionCount +
                     ' condition marker' +
                     ' condition marker' +
                     (conditionCount === 1 ? '' : 's')
                     (
                        conditionCount === 1
                            ? ''
                            : 's'
                    )
                 )
                 )
             );
             );


             container.append(summary);
             container.append(
                summary
            );


             if (analysis.conditionVariables.length) {
             if (
                analysis.conditionVariables.length
            ) {
                 container.append(
                 container.append(
                     $('<div>')
                     $('<div>')
Line 1,712: Line 2,349:
                         .text(
                         .text(
                             'Condition variables: ' +
                             'Condition variables: ' +
                             analysis.conditionVariables.join(', ')
                             analysis.conditionVariables.join(
                                ', '
                            )
                         )
                         )
                 );
                 );
             }
             }


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


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


                 validation.errors.forEach(error => {
                 validation.errors.forEach(
                    $('<li>').text(error).appendTo(errors);
                    error => {
                 });
                        $('<li>')
                            .text(error)
                            .appendTo(errors);
                    }
                 );


                 container.append(errors);
                 container.append(
                    errors
                );
             }
             }


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


                 validation.warnings.forEach(warning => {
                 validation.warnings.forEach(
                    $('<li>').text(warning).appendTo(warnings);
                    warning => {
                 });
                        $('<li>')
                            .text(warning)
                            .appendTo(warnings);
                    }
                 );


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


             if (!validation.errors.length &&
             if (
                 !validation.warnings.length) {
                !validation.errors.length &&
                 !validation.warnings.length
            ) {
                 container.append(
                 container.append(
                     $('<div>')
                     $('<div>')
Line 1,764: Line 2,424:
         renderAnalysisBySelector() {
         renderAnalysisBySelector() {
             const container =
             const container =
                 this.root?.find(
                 this.root &&
                this.root.find(
                     '.preload-editor-analysis'
                     '.preload-editor-analysis'
                 );
                 );


             if (container?.length) {
             if (
                 this.renderAnalysis(container);
                container &&
                container.length
            ) {
                 this.renderAnalysis(
                    container
                );
             }
             }
         },
         },
Line 1,784: Line 2,450:
                 );
                 );


             const variables = analysis.variables.slice();
             const variables =
                analysis.variables.slice();
 
             const ordered = ['PAGE'];
             const ordered = ['PAGE'];


             variables.forEach(name => {
             variables.forEach(name => {
                 if (name !== 'PAGE' &&
                 if (
                     !ordered.includes(name)) {
                    name !== 'PAGE' &&
                     !ordered.includes(name)
                ) {
                     ordered.push(name);
                     ordered.push(name);
                 }
                 }
Line 1,797: Line 2,467:
                 this.state.config.fields || {}
                 this.state.config.fields || {}
             ).forEach(name => {
             ).forEach(name => {
                 const upper = name.toUpperCase();
                 const upper =
                    name.toUpperCase();


                 if (!ordered.includes(upper)) {
                 if (
                    !ordered.includes(upper)
                ) {
                     ordered.push(upper);
                     ordered.push(upper);
                 }
                 }
Line 1,807: Line 2,480:
                 const field =
                 const field =
                     this.state.config.fields[name] ||
                     this.state.config.fields[name] ||
                     this.createDefaultField(name, analysis);
                     this.createDefaultField(
                        name,
                        analysis
                    );


                 this.state.config.fields[name] = field;
                 this.state.config.fields[name] =
                    field;


                 container.append(
                 container.append(
Line 1,823: Line 2,500:
         renderVariablesBySelector() {
         renderVariablesBySelector() {
             const container =
             const container =
                 this.root?.find(
                 this.root &&
                this.root.find(
                     '.preload-editor-variables'
                     '.preload-editor-variables'
                 );
                 );


             if (container?.length) {
             if (
                 this.renderVariables(container);
                container &&
                container.length
            ) {
                 this.renderVariables(
                    container
                );
             }
             }
         },
         },


         createVariableCard(name, field, analysis) {
         createVariableCard(
             const card = $('<div>')
            name,
                .addClass('preload-editor-variable')
            field,
                .attr('data-variable', name);
            analysis
        ) {
             const card =
                $('<div>')
                    .addClass(
                        'preload-editor-variable'
                    )
                    .attr(
                        'data-variable',
                        name
                    );


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


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


             if (name === 'PAGE') {
             if (name === 'PAGE') {
                 header.append(
                 header.append(
                     $('<span>')
                     $('<span>')
                         .addClass('preload-editor-badge')
                         .addClass(
                            'preload-editor-badge'
                        )
                         .text('AUTOMATIC')
                         .text('AUTOMATIC')
                 );
                 );
             } else if (!analysis.variables.includes(name)) {
             } else if (
                !analysis.variables.includes(name)
            ) {
                 header.append(
                 header.append(
                     $('<span>')
                     $('<span>')
Line 1,860: Line 2,562:
             card.append(header);
             card.append(header);


             const label = this.textInput(field.label || '');
             const label =
                this.textInput(
                    field.label || ''
                );
 
            label.on(
                'input.preloadEditor',
                () => {
                    field.label =
                        label.val();
 
                    this.markDirty();
                    this.renderConfigPreviewBySelector();
                }
            );


             label.on('input.preloadEditor', () => {
             card.append(
                field.label = label.val();
                this.fieldRow(
                this.markDirty();
                    'Label',
                 this.renderConfigPreviewBySelector();
                    label
             });
                 )
             );


             card.append(this.fieldRow('Label', label));
             const description =
                this.textarea(
                    field.description || ''
                ).addClass(
                    'preload-editor-small-textarea'
                );


             const description = this.textarea(
             description.on(
                 field.description || ''
                 'input.preloadEditor',
            ).addClass('preload-editor-small-textarea');
                () => {
                    field.description =
                        description.val();


            description.on('input.preloadEditor', () => {
                    this.markDirty();
                field.description = description.val();
                    this.renderConfigPreviewBySelector();
                this.markDirty();
                }
                this.renderConfigPreviewBySelector();
             );
             });


             card.append(
             card.append(
Line 1,893: Line 2,616:
                 ['boolean', 'Boolean'],
                 ['boolean', 'Boolean'],
                 ['select', 'Select'],
                 ['select', 'Select'],
                 ['multiselect', 'Multi-select'],
                 [
                    'multiselect',
                    'Multi-select'
                ],
                 ['page', 'Page'],
                 ['page', 'Page'],
                 ['user', 'User'],
                 ['user', 'User'],
Line 1,899: Line 2,625:
             ];
             ];


             const typeSelect = this.select(
             const typeSelect =
                types.map(([value, label]) => ({
                this.select(
                    value,
                    types.map(
                    label
                        ([value, label]) => ({
                })),
                            value,
                field.type || 'text'
                            label
            );
                        })
                    ),
                    field.type || 'text'
                );


             typeSelect.on('change.preloadEditor', () => {
             typeSelect.on(
                field.type = typeSelect.val();
                'change.preloadEditor',
                this.markDirty();
                () => {
                    field.type =
                        typeSelect.val();


                card.replaceWith(
                    this.markDirty();
                    this.createVariableCard(
 
                        name,
                    card.replaceWith(
                        field,
                        this.createVariableCard(
                        analysis
                            name,
                    )
                            field,
                );
                            analysis
                        )
                    );


                this.renderConfigPreviewBySelector();
                    this.renderConfigPreviewBySelector();
             });
                }
             );


             card.append(
             card.append(
Line 1,932: Line 2,666:
                 field.type = 'page';
                 field.type = 'page';


                 const required = this.checkbox(
                 const required =
                    field.required !== false
                    this.checkbox(
                );
                        field.required !== false
                    );


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


                    this.markDirty();
                        this.markDirty();
                    this.renderConfigPreviewBySelector();
                        this.renderConfigPreviewBySelector();
                 });
                    }
                 );


                 card.append(
                 card.append(
Line 1,951: Line 2,691:
                 );
                 );


                 const namespace = this.select(
                 const namespace =
                    [
                    this.select(
                        { value: 'Main', label: 'Main' },
                        [
                        { value: 'Template', label: 'Template' },
                            {
                        { value: 'File', label: 'File' },
                                value: 'Main',
                        { value: 'Category', label: 'Category' },
                                label: 'Main'
                        { value: 'User', label: 'User' }
                            },
                    ],
                            {
                    field.namespace || 'Main'
                                value: 'Template',
                 );
                                label: 'Template'
                            },
                            {
                                value: 'File',
                                label: 'File'
                            },
                            {
                                value: 'Category',
                                label: 'Category'
                            },
                            {
                                value: 'User',
                                label: 'User'
                            }
                        ],
                        field.namespace || 'Main'
                    );
 
                 namespace.on(
                    'change.preloadEditor',
                    () => {
                        field.namespace =
                            namespace.val();


                namespace.on('change.preloadEditor', () => {
                        this.markDirty();
                    field.namespace = namespace.val();
                        this.renderConfigPreviewBySelector();
                    this.markDirty();
                    }
                    this.renderConfigPreviewBySelector();
                 );
                 });


                 card.append(
                 card.append(
Line 1,975: Line 2,736:
                 );
                 );


                 const allowNew = this.checkbox(
                 const allowNew =
                    field['allow-new'] === true
                    this.checkbox(
                );
                        field['allow-new'] === true
                    );


                 allowNew.on('change.preloadEditor', () => {
                 allowNew.on(
                    field['allow-new'] =
                    'change.preloadEditor',
                        allowNew.prop('checked');
                    () => {
                        field['allow-new'] =
                            allowNew.prop(
                                'checked'
                            );


                    this.markDirty();
                        this.markDirty();
                    this.renderConfigPreviewBySelector();
                        this.renderConfigPreviewBySelector();
                 });
                    }
                 );


                 card.append(
                 card.append(
Line 1,997: Line 2,764:
             }
             }


             const required = this.checkbox(
             const required =
                field.required === true
                this.checkbox(
            );
                    field.required === true
                );


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


                this.markDirty();
                    this.markDirty();
                this.renderConfigPreviewBySelector();
                    this.renderConfigPreviewBySelector();
             });
                }
             );


             card.append(
             card.append(
Line 2,016: Line 2,789:
             );
             );


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


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


                if (value) field.placeholder = value;
                    if (value) {
                else delete field.placeholder;
                        field.placeholder =
                            value;
                    } else {
                        delete field.placeholder;
                    }


                this.markDirty();
                    this.markDirty();
                this.renderConfigPreviewBySelector();
                    this.renderConfigPreviewBySelector();
             });
                }
             );


             card.append(
             card.append(
Line 2,037: Line 2,819:
             );
             );


             const defaultInput = this.textInput(
             const defaultInput =
                field.default === undefined
                this.textInput(
                    ? ''
                    field.default === undefined
                    : field.default
                        ? ''
            );
                        : field.default
                );


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


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


                this.markDirty();
                    this.markDirty();
                this.renderConfigPreviewBySelector();
                    this.renderConfigPreviewBySelector();
             });
                }
             );


             card.append(
             card.append(
Line 2,061: Line 2,852:
             );
             );


             if (field.type === 'textarea') {
             if (
                 const maxRows = this.textInput(
                field.type === 'textarea'
                    field.maxRows || ''
            ) {
                ).attr('type', 'number');
                 const maxRows =
                    this.textInput(
                        field.maxRows || ''
                    ).attr(
                        'type',
                        'number'
                    );
 
                maxRows.on(
                    'input.preloadEditor',
                    () => {
                        const value =
                            parseInt(
                                maxRows.val(),
                                10
                            );


                maxRows.on('input.preloadEditor', () => {
                        if (
                    const value =
                            Number.isFinite(
                         parseInt(maxRows.val(), 10);
                                value
                            ) &&
                            value > 0
                        ) {
                            field.maxRows =
                                value;
                         } else {
                            delete field.maxRows;
                        }


                    if (Number.isFinite(value) && value > 0) {
                        this.markDirty();
                        field.maxRows = value;
                         this.renderConfigPreviewBySelector();
                    } else {
                         delete field.maxRows;
                     }
                     }
                );


                    this.markDirty();
                 card.append(
                    this.renderConfigPreviewBySelector();
                     this.fieldRow(
                });
 
                 card.append(
                     this.fieldRow(
                         'Maximum rows',
                         'Maximum rows',
                         maxRows
                         maxRows
Line 2,088: Line 2,897:
             }
             }


             if (field.type === 'select' ||
             if (
                 field.type === 'multiselect') {
                field.type === 'select' ||
                 field.type === 'multiselect'
            ) {
                 this.renderSelectConfiguration(
                 this.renderSelectConfiguration(
                     card,
                     card,
Line 2,099: Line 2,910:
         },
         },


         renderSelectConfiguration(card, field) {
         renderSelectConfiguration(
             const source = this.textInput(
            card,
                field.source || '',
            field
                'category:Example'
        ) {
            );
             const source =
                this.textInput(
                    field.source || '',
                    'category:Example'
                );


             source.on('input.preloadEditor', () => {
             source.on(
                const value = source.val();
                'input.preloadEditor',
                () => {
                    const value =
                        source.val();


                if (value) field.source = value;
                    if (value) {
                else delete field.source;
                        field.source = value;
                    } else {
                        delete field.source;
                    }


                this.markDirty();
                    this.markDirty();
                this.renderConfigPreviewBySelector();
                    this.renderConfigPreviewBySelector();
             });
                }
             );


             card.append(
             card.append(
Line 2,123: Line 2,945:
             );
             );


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


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


                if (value) {
                    if (value) {
                    field['source-prefix'] = value;
                        field['source-prefix'] =
                } else {
                            value;
                    delete field['source-prefix'];
                    } else {
                        delete field[
                            'source-prefix'
                        ];
                    }
 
                    this.markDirty();
                    this.renderConfigPreviewBySelector();
                 }
                 }
 
             );
                this.markDirty();
                this.renderConfigPreviewBySelector();
             });


             card.append(
             card.append(
Line 2,148: Line 2,978:
             );
             );


             const excludeCategory = this.textInput(
             const excludeCategory =
                field.exclude && field.exclude.category || ''
                this.textInput(
            );
                    field.exclude &&
                    field.exclude.category ||
                    ''
                );


             excludeCategory.on('input.preloadEditor', () => {
             excludeCategory.on(
                const value = excludeCategory.val();
                'input.preloadEditor',
                () => {
                    const value =
                        excludeCategory.val();


                if (value) {
                    if (value) {
                    field.exclude = Object.assign(
                        field.exclude =
                        {},
                            Object.assign(
                        field.exclude || {},
                                {},
                        { category: value }
                                field.exclude || {},
                    );
                                {
                } else if (field.exclude) {
                                    category:
                    delete field.exclude.category;
                                        value
                                }
                            );
                    } else if (
                        field.exclude
                    ) {
                        delete field.exclude.category;


                    if (!Object.keys(field.exclude).length) {
                        if (
                        delete field.exclude;
                            !Object.keys(
                                field.exclude
                            ).length
                        ) {
                            delete field.exclude;
                        }
                     }
                     }
                    this.markDirty();
                    this.renderConfigPreviewBySelector();
                 }
                 }
 
             );
                this.markDirty();
                this.renderConfigPreviewBySelector();
             });


             card.append(
             card.append(
Line 2,195: Line 3,042:
             const optionsValue =
             const optionsValue =
                 Array.isArray(field.options)
                 Array.isArray(field.options)
                     ? field.options.map(option => {
                     ? field.options
                        if (typeof option === 'string') {
                        .map(option => {
                            return option;
                            if (
                        }
                                typeof option ===
                                'string'
                            ) {
                                return option;
                            }


                        return (
                            return (
                            String(option.value || '') +
                                String(
                            ' | ' +
                                    option.value ||
                            String(
                                    ''
                                option.label ||
                                ) +
                                option.value ||
                                ' | ' +
                                ''
                                String(
                            )
                                    option.label ||
                        );
                                    option.value ||
                    }).join('\n')
                                    ''
                                )
                            );
                        })
                        .join('\n')
                     : '';
                     : '';


             const options = this.textarea(optionsValue)
             const options =
                 .addClass('preload-editor-small-textarea');
                this.textarea(
                    optionsValue
                 ).addClass(
                    'preload-editor-small-textarea'
                );
 
            options.on(
                'input.preloadEditor',
                () => {
                    const lines =
                        options
                            .val()
                            .split('\n')
                            .map(
                                line =>
                                    line.trim()
                            )
                            .filter(Boolean);


            options.on('input.preloadEditor', () => {
                    if (!lines.length) {
                const lines = options
                        delete field.options;
                     .val()
                     } else {
                    .split('\n')
                        field.options =
                    .map(line => line.trim())
                            lines.map(line => {
                    .filter(Boolean);
                                const parts =
                                    line.split(
                                        /\s*\|\s*/
                                    );


                if (!lines.length) {
                                if (
                    delete field.options;
                                    parts.length ===
                } else {
                                    1
                    field.options = lines.map(line => {
                                ) {
                        const parts =
                                    return parts[0];
                            line.split(/\s*\|\s*/);
                                }


                        if (parts.length === 1) {
                                return {
                             return parts[0];
                                    value:
                        }
                                        parts[0],
                                    label:
                                        parts
                                            .slice(1)
                                            .join(
                                                ' | '
                                            )
                                };
                             });
                    }


                        return {
                    this.markDirty();
                            value: parts[0],
                     this.renderConfigPreviewBySelector();
                            label: parts.slice(1).join(' | ')
                        };
                     });
                 }
                 }
 
             );
                this.markDirty();
                this.renderConfigPreviewBySelector();
             });


             card.append(
             card.append(
Line 2,253: Line 3,131:
         },
         },


         createTransformControls(field, property, title) {
         createTransformControls(
             const wrapper = $('<div>')
            field,
                .addClass('preload-editor-transform');
            property,
 
            title
        ) {
             const wrapper =
                $('<div>')
                    .addClass(
                        'preload-editor-transform'
                    );
 
             $('<h4>')
             $('<h4>')
                 .text(title)
                 .text(title)
Line 2,263: Line 3,148:
             const existing =
             const existing =
                 field[property] &&
                 field[property] &&
                 typeof field[property] === 'object'
                 typeof field[property] ===
                    'object'
                     ? field[property]
                     ? field[property]
                     : {};
                     : {};


             const transform = this.select(
             const transform =
                [
                this.select(
                    {
                    [
                        value: '',
                        {
                        label: 'No transformation'
                            value: '',
                    },
                            label:
                    {
                                'No transformation'
                        value: 'remove-prefix',
                        },
                        label: 'Remove prefix'
                        {
                    },
                            value:
                    {
                                'remove-prefix',
                        value: 'remove-suffix',
                            label:
                        label: 'Remove suffix'
                                'Remove prefix'
                    },
                        },
                    {
                        {
                        value: 'replace',
                            value:
                        label: 'Replace'
                                'remove-suffix',
                    }
                            label:
                ],
                                'Remove suffix'
                existing.transform || ''
                        },
            );
                        {
                            value: 'replace',
                            label: 'Replace'
                        }
                    ],
                    existing.transform || ''
                );


             const prefix = this.textInput(
             const prefix =
                existing.prefix || ''
                this.textInput(
            );
                    existing.prefix || ''
                );


             const suffix = this.textInput(
             const suffix =
                existing.suffix || ''
                this.textInput(
            );
                    existing.suffix || ''
                );


             const from = this.textInput(
             const from =
                existing.from || ''
                this.textInput(
            );
                    existing.from || ''
                );


             const to = this.textInput(
             const to =
                existing.to || ''
                this.textInput(
            );
                    existing.to || ''
                );


             const controls = $('<div>')
             const controls =
                .addClass(
                $('<div>')
                    'preload-editor-transform-controls'
                    .addClass(
                );
                        'preload-editor-transform-controls'
                    );


             const prefixRow =
             const prefixRow =
                 this.fieldRow('Prefix', prefix);
                 this.fieldRow(
                    'Prefix',
                    prefix
                );


             const suffixRow =
             const suffixRow =
                 this.fieldRow('Suffix', suffix);
                 this.fieldRow(
                    'Suffix',
                    suffix
                );


             const fromRow =
             const fromRow =
                 this.fieldRow('From', from);
                 this.fieldRow(
                    'From',
                    from
                );


             const toRow =
             const toRow =
                 this.fieldRow('To', to);
                 this.fieldRow(
                    'To',
                    to
                );


             const renderInputs = () => {
             const renderInputs =
                const type = transform.val();
                () => {
                    const type =
                        transform.val();


                prefixRow.toggle(
                    prefixRow.toggle(
                    type === 'remove-prefix'
                        type ===
                );
                            'remove-prefix'
                    );


                suffixRow.toggle(
                    suffixRow.toggle(
                    type === 'remove-suffix'
                        type ===
                );
                            'remove-suffix'
                    );


                fromRow.toggle(type === 'replace');
                    fromRow.toggle(
                toRow.toggle(type === 'replace');
                        type === 'replace'
            };
                    );


            const update = () => {
                    toRow.toggle(
                const type = transform.val();
                        type === 'replace'
                 let object = null;
                    );
                 };


                if (type === 'remove-prefix' && prefix.val()) {
            const update =
                    object = {
                 () => {
                        transform: type,
                     const type =
                        prefix: prefix.val()
                         transform.val();
                    };
                 } else if (
                    type === 'remove-suffix' &&
                    suffix.val()
                ) {
                    object = {
                        transform: type,
                        suffix: suffix.val()
                     };
                } else if (type === 'replace' && from.val()) {
                    object = {
                         transform: type,
                        from: from.val(),
                        to: to.val()
                    };
                }


                if (object) {
                    let object = null;
                    field[property] = object;
                } else {
                    delete field[property];
                }


                this.markDirty();
                    if (
                this.renderConfigPreviewBySelector();
                        type ===
            };
                            'remove-prefix' &&
                        prefix.val()
                    ) {
                        object = {
                            transform: type,
                            prefix:
                                prefix.val()
                        };
                    } else if (
                        type ===
                            'remove-suffix' &&
                        suffix.val()
                    ) {
                        object = {
                            transform: type,
                            suffix:
                                suffix.val()
                        };
                    } else if (
                        type === 'replace' &&
                        from.val()
                    ) {
                        object = {
                            transform: type,
                            from: from.val(),
                            to: to.val()
                        };
                    }


             transform.on(
                    if (object) {
                        field[property] =
                            object;
                    } else {
                        delete field[property];
                    }
 
                    this.markDirty();
                    this.renderConfigPreviewBySelector();
                };
 
             transform.on(
                 'change.preloadEditor',
                 'change.preloadEditor',
                 () => {
                 () => {
Line 2,380: Line 3,312:
             );
             );


             [prefix, suffix, from, to].forEach(input => {
             [
                prefix,
                suffix,
                from,
                to
            ].forEach(input => {
                 input.on(
                 input.on(
                     'input.preloadEditor',
                     'input.preloadEditor',
Line 2,399: Line 3,336:


             renderInputs();
             renderInputs();
             wrapper.append(controls);
 
             wrapper.append(
                controls
            );


             return wrapper;
             return wrapper;
Line 2,407: Line 3,347:


         getCleanConfig() {
         getCleanConfig() {
             const config = JSON.parse(
             const config =
                JSON.stringify(this.state.config)
                JSON.parse(
             );
                    JSON.stringify(
                        this.state.config
                    )
                );
 
             const fields =
                config.fields || {};


            const fields = config.fields || {};
             const orderedFields = {};
             const orderedFields = {};


             if (fields.PAGE) {
             if (fields.PAGE) {
                 orderedFields.PAGE = fields.PAGE;
                 orderedFields.PAGE =
                    fields.PAGE;
             }
             }


Line 2,421: Line 3,367:
                 .filter(
                 .filter(
                     name =>
                     name =>
                         name.toUpperCase() !== 'PAGE'
                         name.toUpperCase() !==
                        'PAGE'
                 )
                 )
                 .forEach(name => {
                 .forEach(name => {
                     orderedFields[name] = fields[name];
                     orderedFields[name] =
                        fields[name];
                 });
                 });


             config.fields = orderedFields;
             config.fields =
                orderedFields;
 
             return config;
             return config;
         },
         },
Line 2,441: Line 3,391:
         renderConfigPreview(container) {
         renderConfigPreview(container) {
             if (container) {
             if (container) {
                 container.text(this.getConfigJSON());
                 container.text(
                    this.getConfigJSON()
                );
             }
             }
         },
         },
Line 2,447: Line 3,399:
         renderConfigPreviewBySelector() {
         renderConfigPreviewBySelector() {
             const preview =
             const preview =
                 this.root?.find(
                 this.root &&
                this.root.find(
                     '.preload-editor-config-preview'
                     '.preload-editor-config-preview'
                 );
                 );


             if (preview?.length) {
             if (
                 this.renderConfigPreview(preview);
                preview &&
                preview.length
            ) {
                 this.renderConfigPreview(
                    preview
                );
             }
             }


Line 2,467: Line 3,425:
             let title =
             let title =
                 String(
                 String(
                     this.state.preloadTitle || ''
                     this.state.preloadTitle ||
                    ''
                 ).trim();
                 ).trim();


Line 2,480: Line 3,439:


                 title = result;
                 title = result;
                 this.state.preloadTitle = title;
                 this.state.preloadTitle =
                    title;
             }
             }


Line 2,489: Line 3,449:
                 this.validateBeforeSave();
                 this.validateBeforeSave();


             if (validation.errors.length) {
             if (
                 this.showValidationErrors(validation);
                validation.errors.length
            ) {
                 this.showValidationErrors(
                    validation
                );
 
                 return;
                 return;
             }
             }
Line 2,501: Line 3,466:
             try {
             try {
                 const exists =
                 const exists =
                     await this.pageExists(title);
                     await this.pageExists(
                        title
                    );


                 await this.savePage(
                 await this.savePage(
Line 2,512: Line 3,479:
                 );
                 );


                 this.state.mode = 'edit';
                 this.state.mode =
                 this.state.dirty = false;
                    'edit';
 
                 this.state.dirty =
                    false;


                 this.setStatus(
                 this.setStatus(
Line 2,522: Line 3,492:
                 mw.notify(
                 mw.notify(
                     'Preload saved.',
                     'Preload saved.',
                     { type: 'success' }
                     {
                        type: 'success'
                    }
                 );
                 );
             } catch (error) {
             } catch (error) {
Line 2,539: Line 3,511:
                     error.message ||
                     error.message ||
                         'Could not save preload.',
                         'Could not save preload.',
                     { type: 'error' }
                     {
                        type: 'error'
                    }
                 );
                 );
             }
             }
Line 2,547: Line 3,521:
             let title =
             let title =
                 String(
                 String(
                     this.state.preloadTitle || ''
                     this.state.preloadTitle ||
                    ''
                 ).trim();
                 ).trim();


Line 2,560: Line 3,535:


                 title = result;
                 title = result;
                 this.state.preloadTitle = title;
                 this.state.preloadTitle =
                    title;
             }
             }


Line 2,569: Line 3,545:
                 this.validateBeforeSave();
                 this.validateBeforeSave();


             if (validation.errors.length) {
             if (
                 this.showValidationErrors(validation);
                validation.errors.length
            ) {
                 this.showValidationErrors(
                    validation
                );
 
                 return;
                 return;
             }
             }


             const configTitle =
             const configTitle =
                 title.replace(/\/+$/, '') +
                 title.replace(
                    /\/+$/,
                    ''
                ) +
                 '/config.json';
                 '/config.json';


Line 2,585: Line 3,569:
             try {
             try {
                 const preloadExists =
                 const preloadExists =
                     await this.pageExists(title);
                     await this.pageExists(
                        title
                    );


                 await this.savePage(
                 await this.savePage(
Line 2,597: Line 3,583:


                 const configExists =
                 const configExists =
                     await this.pageExists(configTitle);
                     await this.pageExists(
                        configTitle
                    );


                 await this.savePage(
                 await this.savePage(
Line 2,608: Line 3,596:
                 );
                 );


                 this.state.mode = 'edit';
                 this.state.mode =
                 this.state.dirty = false;
                    'edit';
                 this.state.preloadTitle = title;
 
                 this.state.dirty =
                    false;
 
                 this.state.preloadTitle =
                    title;


                 this.setStatus(
                 this.setStatus(
Line 2,619: Line 3,612:
                 mw.notify(
                 mw.notify(
                     'Preload and config saved.',
                     'Preload and config saved.',
                     { type: 'success' }
                     {
                        type: 'success'
                    }
                 );
                 );
             } catch (error) {
             } catch (error) {
Line 2,636: Line 3,631:
                     error.message ||
                     error.message ||
                         'Could not save preload/config.',
                         'Could not save preload/config.',
                     { type: 'error' }
                     {
                        type: 'error'
                    }
                 );
                 );
             }
             }
Line 2,647: Line 3,644:
             const title =
             const title =
                 String(
                 String(
                     this.state.preloadTitle || ''
                     this.state.preloadTitle ||
                    ''
                 ).trim();
                 ).trim();


Line 2,668: Line 3,666:
                 this.getEditorContent();
                 this.getEditorContent();


             this.state.preloadContent = content;
             this.state.preloadContent =
                content;


             if (!content.trim()) {
             if (!content.trim()) {
Line 2,676: Line 3,675:
             }
             }


             if (!/<includeonly>/i.test(content)) {
             if (
                !/<includeonly>/i.test(
                    content
                )
            ) {
                 warnings.push(
                 warnings.push(
                     'The preload does not contain an <includeonly> section.'
                     'The preload does not contain an <includeonly> section.'
Line 2,682: Line 3,685:
             }
             }


             if (!/<noinclude>/i.test(content)) {
             if (
                !/<noinclude>/i.test(
                    content
                )
            ) {
                 warnings.push(
                 warnings.push(
                     'The preload does not contain a <noinclude> section.'
                     'The preload does not contain a <noinclude> section.'
Line 2,690: Line 3,697:
             const analysis =
             const analysis =
                 this.analyzePreload(
                 this.analyzePreload(
                     this.extractIncludeonly(content)
                     this.extractIncludeonly(
                        content
                    )
                 );
                 );


Line 2,699: Line 3,708:
                 );
                 );


             errors.push(...configValidation.errors);
             errors.push(
             warnings.push(...configValidation.warnings);
                ...configValidation.errors
            );
 
             warnings.push(
                ...configValidation.warnings
            );


             let depth = 0;
             let depth = 0;


             analysis.conditions.forEach(condition => {
             analysis.conditions.forEach(
                if (condition.type === 'if') {
                condition => {
                    depth++;
                    if (
                } else if (condition.type === 'endif') {
                        condition.type ===
                    depth--;
                        'if'
                    ) {
                        depth++;
                    } else if (
                        condition.type ===
                        'endif'
                    ) {
                        depth--;


                    if (depth < 0) {
                        if (depth < 0) {
                        errors.push(
                            errors.push(
                            'Found @endif without a matching @if.'
                                'Found @endif without a matching @if.'
                        );
                            );
                        depth = 0;
 
                    }
                            depth = 0;
                } else if (
                        }
                    condition.type === 'elseif' ||
                    } else if (
                    condition.type === 'else'
                        condition.type ===
                ) {
                            'elseif' ||
                    if (depth === 0) {
                        condition.type ===
                        errors.push(
                            'else'
                            '@' +
                    ) {
                            condition.type +
                        if (depth === 0) {
                            ' exists outside an @if block.'
                            errors.push(
                        );
                                '@' +
                                condition.type +
                                ' exists outside an @if block.'
                            );
                        }
                     }
                     }
                 }
                 }
             });
             );


             if (depth > 0) {
             if (depth > 0) {
Line 2,739: Line 3,764:
                 errors,
                 errors,
                 warnings,
                 warnings,
                 valid: errors.length === 0
                 valid:
                    errors.length === 0
             };
             };
         },
         },


         showValidationErrors(validation) {
         showValidationErrors(
            validation
        ) {
             this.renderAnalysisBySelector();
             this.renderAnalysisBySelector();


Line 2,753: Line 3,781:
             mw.notify(
             mw.notify(
                 validation.errors.join('\n'),
                 validation.errors.join('\n'),
                 { type: 'error' }
                 {
                    type: 'error'
                }
             );
             );
         }
         }
     };
     };


     mw.PreloadEditor = PreloadEditor;
     mw.PreloadEditor =
        PreloadEditor;


     $(function () {
     $(function () {