미디어위키:MainPageManifesto.js: 두 판 사이의 차이

(Install package: clbiwiki-mainpage-mud-yisunshin-local-font-20260712 / js/MainPageManifesto.js)
태그: 되돌려진 기여
(Install package: clbiwiki-mainpage-yisunshin-runtime-font-loader-20260712 / js/MainPageManifesto.js)
태그: 되돌려진 기여
7번째 줄: 7번째 줄:
   The prose stays left-aligned. This script measures the actual rendered
   The prose stays left-aligned. This script measures the actual rendered
   line fragments and shifts only the paragraphs so their visual mass is
   line fragments and shifts only the paragraphs so their visual mass is
   centered inside the 660px manifesto column. The title remains geometrically centered. The vertical -100px placement and
   centered inside the 660px manifesto column. The title remains geometrically
  integer-pixel snap remain owned jointly by MainPage.css and this script.
  centered and the vertical -100px placement remains owned by MainPage.css.


   The title divider is the hard horizontal boundary. Optical correction is
   The title divider is the hard horizontal boundary. Optical correction is
   clamped so no rendered line can cross either end of that divider.
   clamped so no rendered line can cross either end of that divider.
  The local typeface is loaded from bytes with the FontFace API instead of
  relying only on CSS @font-face. This bypasses an incorrect font MIME type
  and tries WOFF2 first, then the already uploaded TTF as a fallback.
*/
*/
(function (window, document, mw) {
(function (window, document, mw) {
18번째 줄: 22번째 줄:
     var SELECTOR = '.main-portal .main-manifesto-inner';
     var SELECTOR = '.main-portal .main-manifesto-inner';
     var SHIFT_PROPERTY = '--manifesto-optical-shift-x';
     var SHIFT_PROPERTY = '--manifesto-optical-shift-x';
    var SNAP_X_PROPERTY = '--manifesto-snap-x';
    var SNAP_Y_PROPERTY = '--manifesto-snap-y';
     var MAX_SHIFT = 48;
     var MAX_SHIFT = 48;
     var DIVIDER_SAFE_INSET = 1;
     var DIVIDER_SAFE_INSET = 1;
29번째 줄: 31번째 줄:
         });
         });
     }) : null;
     }) : null;
    var FONT_FAMILY = 'YiSunShinRuntime';
    var FONT_VERSION = '20260712-runtime-loader-001';
    var FONT_SOURCES = [
        { type: 'woff2', path: '/fonts/YiSunShinBold.woff2' },
        { type: 'truetype', path: '/fonts/YiSunShinBold.ttf' }
    ];
    var fontPromise = null;
    var loadedFontFace = null;
    var fontState = {
        status: 'idle',
        family: FONT_FAMILY,
        source: '',
        sourceType: '',
        error: ''
    };
    function versionedFontUrl(path) {
        var separator = path.indexOf('?') === -1 ? '?' : '&';
        return path + separator + 'v=' + encodeURIComponent(FONT_VERSION);
    }
    function loadFontSource(source) {
        var url = versionedFontUrl(source.path);
        return window.fetch(url, {
            credentials: 'same-origin',
            cache: 'no-store'
        }).then(function (response) {
            if (!response.ok) throw new Error(source.type + ' HTTP ' + response.status + ' at ' + url);
            return response.arrayBuffer();
        }).then(function (buffer) {
            var face = new window.FontFace(FONT_FAMILY, buffer, {
                style: 'normal',
                weight: '400'
            });
            return face.load().then(function (loaded) {
                document.fonts.add(loaded);
                loadedFontFace = loaded;
                return {
                    type: source.type,
                    url: url
                };
            });
        });
    }
    function tryFontSource(index, errors) {
        var source;
        if (index >= FONT_SOURCES.length) {
            throw new Error(errors.join(' | ') || 'No usable YiSunShin font source');
        }
        source = FONT_SOURCES[index];
        return loadFontSource(source).catch(function (error) {
            errors.push(error && error.message ? error.message : String(error));
            return tryFontSource(index + 1, errors);
        });
    }
    function loadRuntimeFont(forceReload) {
        if (forceReload && loadedFontFace && document.fonts && typeof document.fonts.delete === 'function') {
            try { document.fonts.delete(loadedFontFace); } catch (error) {}
            loadedFontFace = null;
            fontPromise = null;
        }
        if (fontPromise) return fontPromise;
        if (!window.FontFace || !document.fonts || typeof window.fetch !== 'function') {
            fontState.status = 'unsupported';
            fontState.error = 'FontFace, document.fonts, or fetch is unavailable';
            return Promise.reject(new Error(fontState.error));
        }
        fontState.status = 'loading';
        fontState.source = '';
        fontState.sourceType = '';
        fontState.error = '';
        fontPromise = tryFontSource(0, []).then(function (result) {
            fontState.status = 'ready';
            fontState.source = result.url;
            fontState.sourceType = result.type;
            fontState.error = '';
            return result;
        }).catch(function (error) {
            fontState.status = 'failed';
            fontState.error = error && error.message ? error.message : String(error);
            fontPromise = null;
            throw error;
        });
        return fontPromise;
    }
    function forceRuntimeFont(inner) {
        if (!inner) return;
        inner.style.setProperty('font-family', "'" + FONT_FAMILY + "', serif", 'important');
        inner.style.setProperty('font-weight', '400', 'important');
        inner.style.setProperty('font-style', 'normal', 'important');
        inner.style.setProperty('font-synthesis', 'none', 'important');
        Array.prototype.forEach.call(inner.querySelectorAll('*'), function (node) {
            node.style.setProperty('font-family', "'" + FONT_FAMILY + "', serif", 'important');
            node.style.setProperty('font-weight', '400', 'important');
            node.style.setProperty('font-style', 'normal', 'important');
            node.style.setProperty('font-synthesis', 'none', 'important');
        });
    }
    function applyRuntimeFont(inner) {
        if (!inner) return;
        loadRuntimeFont(false).then(function (result) {
            forceRuntimeFont(inner);
            inner.setAttribute('data-manifesto-font-status', 'ready');
            inner.setAttribute('data-manifesto-font-source', result.type);
            inner.removeAttribute('data-manifesto-font-error');
            schedule(inner);
        }).catch(function (error) {
            inner.setAttribute('data-manifesto-font-status', 'failed');
            inner.setAttribute('data-manifesto-font-error', error && error.message ? error.message : String(error));
            schedule(inner);
            if (window.console && typeof window.console.error === 'function') {
                window.console.error('[MainPageManifesto] YiSunShin font load failed:', error);
            }
        });
    }


     function isVisible(element) {
     function isVisible(element) {
63번째 줄: 196번째 줄:
         var maximumShift;
         var maximumShift;
         var shift;
         var shift;
        var snapX;
        var snapY;


         if (!isVisible(inner)) return;
         if (!isVisible(inner)) return;


         /*
         /* Remove the previous correction before reading unshifted line geometry. */
        * Remove previous corrections first. The wrapper is then snapped to
        * whole CSS-pixel coordinates without transforms, so the glyph raster
        * origin remains stable on Windows Chromium as well.
        */
         inner.style.setProperty(SHIFT_PROPERTY, '0px');
         inner.style.setProperty(SHIFT_PROPERTY, '0px');
        inner.style.setProperty(SNAP_X_PROPERTY, '0px');
        inner.style.setProperty(SNAP_Y_PROPERTY, '0px');


         window.requestAnimationFrame(function () {
         window.requestAnimationFrame(function () {
81번째 줄: 206번째 줄:


             innerRect = inner.getBoundingClientRect();
             innerRect = inner.getBoundingClientRect();
             snapX = Math.round(innerRect.left) - innerRect.left;
             desiredCenter = innerRect.left + (innerRect.width / 2);
            snapY = Math.round(innerRect.top) - innerRect.top;
 
            if (Math.abs(snapX) < 0.001) snapX = 0;
            if (Math.abs(snapY) < 0.001) snapY = 0;
 
            inner.style.setProperty(SNAP_X_PROPERTY, snapX.toFixed(3) + 'px');
            inner.style.setProperty(SNAP_Y_PROPERTY, snapY.toFixed(3) + 'px');
            inner.setAttribute('data-pixel-snap-x', snapX.toFixed(3));
            inner.setAttribute('data-pixel-snap-y', snapY.toFixed(3));
 
            window.requestAnimationFrame(function () {
                if (!isVisible(inner)) return;


                innerRect = inner.getBoundingClientRect();
            Array.prototype.forEach.call(inner.querySelectorAll('p'), function (paragraph) {
                desiredCenter = innerRect.left + (innerRect.width / 2);
                getLineRects(paragraph).forEach(function (rect) {
 
                    /* Area weighting approximates the visible typographic mass. */
                Array.prototype.forEach.call(inner.querySelectorAll('p'), function (paragraph) {
                    var weight = rect.width * rect.height;
                    getLineRects(paragraph).forEach(function (rect) {
                    weightedCenter += (rect.left + (rect.width / 2)) * weight;
                        /* Area weighting approximates the visible typographic mass. */
                    totalWeight += weight;
                        var weight = rect.width * rect.height;
                    minimumLeft = Math.min(minimumLeft, rect.left);
                        weightedCenter += (rect.left + (rect.width / 2)) * weight;
                    maximumRight = Math.max(maximumRight, rect.right);
                        totalWeight += weight;
                        minimumLeft = Math.min(minimumLeft, rect.left);
                        maximumRight = Math.max(maximumRight, rect.right);
                    });
                 });
                 });
            });


                if (!totalWeight || !isFinite(minimumLeft) || !isFinite(maximumRight)) {
            if (!totalWeight || !isFinite(minimumLeft) || !isFinite(maximumRight)) {
                    inner.style.setProperty(SHIFT_PROPERTY, '0px');
                inner.style.setProperty(SHIFT_PROPERTY, '0px');
                    inner.removeAttribute('data-optical-shift-x');
                inner.removeAttribute('data-optical-shift-x');
                    return;
                return;
                }
            }


                shift = desiredCenter - (weightedCenter / totalWeight);
            shift = desiredCenter - (weightedCenter / totalWeight);
                shift = Math.max(-MAX_SHIFT, Math.min(MAX_SHIFT, shift));
            shift = Math.max(-MAX_SHIFT, Math.min(MAX_SHIFT, shift));


                /*
            /*
                * The title divider spans the full inner box. Keep every rendered
            * The title divider spans the full inner box. Keep every rendered
                * line inside that same horizontal range. A long line that already
            * line inside that same horizontal range. A long line that already
                * reaches the divider therefore limits the optical correction rather
            * reaches the divider therefore limits the optical correction rather
                * than being pushed beyond it.
            * than being pushed beyond it.
                */
            */
                minimumShift = Math.ceil((innerRect.left + DIVIDER_SAFE_INSET) - minimumLeft);
            minimumShift = Math.ceil((innerRect.left + DIVIDER_SAFE_INSET) - minimumLeft);
                maximumShift = Math.floor((innerRect.right - DIVIDER_SAFE_INSET) - maximumRight);
            maximumShift = Math.floor((innerRect.right - DIVIDER_SAFE_INSET) - maximumRight);


                if (minimumShift <= maximumShift) {
            if (minimumShift <= maximumShift) {
                    shift = Math.max(minimumShift, Math.min(maximumShift, shift));
                shift = Math.max(minimumShift, Math.min(maximumShift, shift));
                } else {
            } else {
                    /* The text itself is wider than the divider; do not compound it. */
                /* The text itself is wider than the divider; do not compound it. */
                    shift = 0;
                shift = 0;
                }
            }


                shift = Math.round(shift);
            shift = Math.round(shift);
                inner.style.setProperty(SHIFT_PROPERTY, shift + 'px');
            inner.style.setProperty(SHIFT_PROPERTY, shift + 'px');
                inner.setAttribute('data-optical-shift-x', String(shift));
            inner.setAttribute('data-optical-shift-x', String(shift));
                inner.setAttribute('data-optical-shift-min', String(minimumShift));
            inner.setAttribute('data-optical-shift-min', String(minimumShift));
                inner.setAttribute('data-optical-shift-max', String(maximumShift));
            inner.setAttribute('data-optical-shift-max', String(maximumShift));
            });
         });
         });
     }
     }
162번째 줄: 271번째 줄:
                 if (observed) observed.add(inner);
                 if (observed) observed.add(inner);
             }
             }
            applyRuntimeFont(inner);
             schedule(inner);
             schedule(inner);
         });
         });
168번째 줄: 278번째 줄:
     function recalculateAll() {
     function recalculateAll() {
         Array.prototype.forEach.call(document.querySelectorAll(SELECTOR), schedule);
         Array.prototype.forEach.call(document.querySelectorAll(SELECTOR), schedule);
    }
    function reloadFont() {
        var inners = Array.prototype.slice.call(document.querySelectorAll(SELECTOR));
        return loadRuntimeFont(true).then(function (result) {
            inners.forEach(function (inner) {
                forceRuntimeFont(inner);
                inner.setAttribute('data-manifesto-font-status', 'ready');
                inner.setAttribute('data-manifesto-font-source', result.type);
                inner.removeAttribute('data-manifesto-font-error');
                schedule(inner);
            });
            return result;
        });
     }
     }


178번째 줄: 302번째 줄:
     if (document.fonts && document.fonts.ready) {
     if (document.fonts && document.fonts.ready) {
         document.fonts.ready.then(recalculateAll).catch(function () {});
         document.fonts.ready.then(recalculateAll).catch(function () {});
        if (typeof document.fonts.load === 'function') {
            Promise.all([
                document.fonts.load('400 14px YiSunShinMain'),
                document.fonts.load('400 36px YiSunShinMain')
            ]).then(recalculateAll).catch(function () {});
        }
     }
     }


201번째 줄: 319번째 줄:
     window.MainPageManifesto = window.MainPageManifesto || {};
     window.MainPageManifesto = window.MainPageManifesto || {};
     window.MainPageManifesto.recalculate = recalculateAll;
     window.MainPageManifesto.recalculate = recalculateAll;
    window.MainPageManifesto.reloadFont = reloadFont;
    window.MainPageManifesto.fontStatus = function () {
        return {
            status: fontState.status,
            family: fontState.family,
            source: fontState.source,
            sourceType: fontState.sourceType,
            error: fontState.error
        };
    };
})(window, document, window.mw);
})(window, document, window.mw);

2026년 7월 12일 (일) 05:24 판

/* =========================================
   Main page manifesto optical centering
   =========================================
   New shared systems use unprefixed names. Existing prefixed page classes
   remain only because they are legacy layout contracts.

   The prose stays left-aligned. This script measures the actual rendered
   line fragments and shifts only the paragraphs so their visual mass is
   centered inside the 660px manifesto column. The title remains geometrically
   centered and the vertical -100px placement remains owned by MainPage.css.

   The title divider is the hard horizontal boundary. Optical correction is
   clamped so no rendered line can cross either end of that divider.

   The local typeface is loaded from bytes with the FontFace API instead of
   relying only on CSS @font-face. This bypasses an incorrect font MIME type
   and tries WOFF2 first, then the already uploaded TTF as a fallback.
*/
(function (window, document, mw) {
    'use strict';

    var SELECTOR = '.main-portal .main-manifesto-inner';
    var SHIFT_PROPERTY = '--manifesto-optical-shift-x';
    var MAX_SHIFT = 48;
    var DIVIDER_SAFE_INSET = 1;
    var resizeTimer = 0;
    var observed = typeof WeakSet === 'function' ? new WeakSet() : null;
    var resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(function (entries) {
        entries.forEach(function (entry) {
            schedule(entry.target);
        });
    }) : null;

    var FONT_FAMILY = 'YiSunShinRuntime';
    var FONT_VERSION = '20260712-runtime-loader-001';
    var FONT_SOURCES = [
        { type: 'woff2', path: '/fonts/YiSunShinBold.woff2' },
        { type: 'truetype', path: '/fonts/YiSunShinBold.ttf' }
    ];
    var fontPromise = null;
    var loadedFontFace = null;
    var fontState = {
        status: 'idle',
        family: FONT_FAMILY,
        source: '',
        sourceType: '',
        error: ''
    };

    function versionedFontUrl(path) {
        var separator = path.indexOf('?') === -1 ? '?' : '&';
        return path + separator + 'v=' + encodeURIComponent(FONT_VERSION);
    }

    function loadFontSource(source) {
        var url = versionedFontUrl(source.path);

        return window.fetch(url, {
            credentials: 'same-origin',
            cache: 'no-store'
        }).then(function (response) {
            if (!response.ok) throw new Error(source.type + ' HTTP ' + response.status + ' at ' + url);
            return response.arrayBuffer();
        }).then(function (buffer) {
            var face = new window.FontFace(FONT_FAMILY, buffer, {
                style: 'normal',
                weight: '400'
            });

            return face.load().then(function (loaded) {
                document.fonts.add(loaded);
                loadedFontFace = loaded;
                return {
                    type: source.type,
                    url: url
                };
            });
        });
    }

    function tryFontSource(index, errors) {
        var source;
        if (index >= FONT_SOURCES.length) {
            throw new Error(errors.join(' | ') || 'No usable YiSunShin font source');
        }

        source = FONT_SOURCES[index];
        return loadFontSource(source).catch(function (error) {
            errors.push(error && error.message ? error.message : String(error));
            return tryFontSource(index + 1, errors);
        });
    }

    function loadRuntimeFont(forceReload) {
        if (forceReload && loadedFontFace && document.fonts && typeof document.fonts.delete === 'function') {
            try { document.fonts.delete(loadedFontFace); } catch (error) {}
            loadedFontFace = null;
            fontPromise = null;
        }

        if (fontPromise) return fontPromise;

        if (!window.FontFace || !document.fonts || typeof window.fetch !== 'function') {
            fontState.status = 'unsupported';
            fontState.error = 'FontFace, document.fonts, or fetch is unavailable';
            return Promise.reject(new Error(fontState.error));
        }

        fontState.status = 'loading';
        fontState.source = '';
        fontState.sourceType = '';
        fontState.error = '';

        fontPromise = tryFontSource(0, []).then(function (result) {
            fontState.status = 'ready';
            fontState.source = result.url;
            fontState.sourceType = result.type;
            fontState.error = '';
            return result;
        }).catch(function (error) {
            fontState.status = 'failed';
            fontState.error = error && error.message ? error.message : String(error);
            fontPromise = null;
            throw error;
        });

        return fontPromise;
    }

    function forceRuntimeFont(inner) {
        if (!inner) return;

        inner.style.setProperty('font-family', "'" + FONT_FAMILY + "', serif", 'important');
        inner.style.setProperty('font-weight', '400', 'important');
        inner.style.setProperty('font-style', 'normal', 'important');
        inner.style.setProperty('font-synthesis', 'none', 'important');

        Array.prototype.forEach.call(inner.querySelectorAll('*'), function (node) {
            node.style.setProperty('font-family', "'" + FONT_FAMILY + "', serif", 'important');
            node.style.setProperty('font-weight', '400', 'important');
            node.style.setProperty('font-style', 'normal', 'important');
            node.style.setProperty('font-synthesis', 'none', 'important');
        });
    }

    function applyRuntimeFont(inner) {
        if (!inner) return;

        loadRuntimeFont(false).then(function (result) {
            forceRuntimeFont(inner);
            inner.setAttribute('data-manifesto-font-status', 'ready');
            inner.setAttribute('data-manifesto-font-source', result.type);
            inner.removeAttribute('data-manifesto-font-error');
            schedule(inner);
        }).catch(function (error) {
            inner.setAttribute('data-manifesto-font-status', 'failed');
            inner.setAttribute('data-manifesto-font-error', error && error.message ? error.message : String(error));
            schedule(inner);
            if (window.console && typeof window.console.error === 'function') {
                window.console.error('[MainPageManifesto] YiSunShin font load failed:', error);
            }
        });
    }

    function isVisible(element) {
        var rect;
        if (!element || !element.isConnected) return false;
        rect = element.getBoundingClientRect();
        return rect.width > 0 && rect.height > 0;
    }

    function getLineRects(paragraph) {
        var range;
        var rects;

        if (!paragraph || !paragraph.textContent || !paragraph.textContent.trim()) return [];

        range = document.createRange();
        range.selectNodeContents(paragraph);
        rects = Array.prototype.slice.call(range.getClientRects());
        if (typeof range.detach === 'function') range.detach();

        return rects.filter(function (rect) {
            return rect.width > 1 && rect.height > 1;
        });
    }

    function measureAndApply(inner) {
        var innerRect;
        var weightedCenter = 0;
        var totalWeight = 0;
        var desiredCenter;
        var minimumLeft = Infinity;
        var maximumRight = -Infinity;
        var minimumShift;
        var maximumShift;
        var shift;

        if (!isVisible(inner)) return;

        /* Remove the previous correction before reading unshifted line geometry. */
        inner.style.setProperty(SHIFT_PROPERTY, '0px');

        window.requestAnimationFrame(function () {
            if (!isVisible(inner)) return;

            innerRect = inner.getBoundingClientRect();
            desiredCenter = innerRect.left + (innerRect.width / 2);

            Array.prototype.forEach.call(inner.querySelectorAll('p'), function (paragraph) {
                getLineRects(paragraph).forEach(function (rect) {
                    /* Area weighting approximates the visible typographic mass. */
                    var weight = rect.width * rect.height;
                    weightedCenter += (rect.left + (rect.width / 2)) * weight;
                    totalWeight += weight;
                    minimumLeft = Math.min(minimumLeft, rect.left);
                    maximumRight = Math.max(maximumRight, rect.right);
                });
            });

            if (!totalWeight || !isFinite(minimumLeft) || !isFinite(maximumRight)) {
                inner.style.setProperty(SHIFT_PROPERTY, '0px');
                inner.removeAttribute('data-optical-shift-x');
                return;
            }

            shift = desiredCenter - (weightedCenter / totalWeight);
            shift = Math.max(-MAX_SHIFT, Math.min(MAX_SHIFT, shift));

            /*
             * The title divider spans the full inner box. Keep every rendered
             * line inside that same horizontal range. A long line that already
             * reaches the divider therefore limits the optical correction rather
             * than being pushed beyond it.
             */
            minimumShift = Math.ceil((innerRect.left + DIVIDER_SAFE_INSET) - minimumLeft);
            maximumShift = Math.floor((innerRect.right - DIVIDER_SAFE_INSET) - maximumRight);

            if (minimumShift <= maximumShift) {
                shift = Math.max(minimumShift, Math.min(maximumShift, shift));
            } else {
                /* The text itself is wider than the divider; do not compound it. */
                shift = 0;
            }

            shift = Math.round(shift);
            inner.style.setProperty(SHIFT_PROPERTY, shift + 'px');
            inner.setAttribute('data-optical-shift-x', String(shift));
            inner.setAttribute('data-optical-shift-min', String(minimumShift));
            inner.setAttribute('data-optical-shift-max', String(maximumShift));
        });
    }

    function schedule(inner) {
        if (!inner) return;
        window.requestAnimationFrame(function () {
            measureAndApply(inner);
        });
    }

    function mount(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var candidates = [];

        if (scope.matches && scope.matches(SELECTOR)) candidates.push(scope);
        candidates = candidates.concat(Array.prototype.slice.call(scope.querySelectorAll(SELECTOR)));

        candidates.forEach(function (inner) {
            if (resizeObserver && (!observed || !observed.has(inner))) {
                resizeObserver.observe(inner);
                if (observed) observed.add(inner);
            }
            applyRuntimeFont(inner);
            schedule(inner);
        });
    }

    function recalculateAll() {
        Array.prototype.forEach.call(document.querySelectorAll(SELECTOR), schedule);
    }

    function reloadFont() {
        var inners = Array.prototype.slice.call(document.querySelectorAll(SELECTOR));
        return loadRuntimeFont(true).then(function (result) {
            inners.forEach(function (inner) {
                forceRuntimeFont(inner);
                inner.setAttribute('data-manifesto-font-status', 'ready');
                inner.setAttribute('data-manifesto-font-source', result.type);
                inner.removeAttribute('data-manifesto-font-error');
                schedule(inner);
            });
            return result;
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () { mount(document); }, { once:true });
    } else {
        mount(document);
    }

    if (document.fonts && document.fonts.ready) {
        document.fonts.ready.then(recalculateAll).catch(function () {});
    }

    window.addEventListener('resize', function () {
        window.clearTimeout(resizeTimer);
        resizeTimer = window.setTimeout(recalculateAll, 120);
    }, { passive:true });

    try {
        if (mw && mw.hook) {
            mw.hook('wikipage.content').add(function (content) {
                mount(content && content[0] ? content[0] : document);
            });
        }
    } catch (err) {}

    window.MainPageManifesto = window.MainPageManifesto || {};
    window.MainPageManifesto.recalculate = recalculateAll;
    window.MainPageManifesto.reloadFont = reloadFont;
    window.MainPageManifesto.fontStatus = function () {
        return {
            status: fontState.status,
            family: fontState.family,
            source: fontState.source,
            sourceType: fontState.sourceType,
            error: fontState.error
        };
    };
})(window, document, window.mw);