1/** 2 * Reasonator V2 â ES6 module entry point. 3 * 4 * Loads runtime config + shared deps in parallel, registers components, then 5 * mounts the Vue app. The HTML shell renders a skeleton immediately so the 6 * user sees structure even while config and the first item batch are in flight. 7 */ 8 9import { WikiData } from '../resources/js_es6/wikidata.js'; 10import { initToolTranslate } from '../resources/vue_es6/tool-translate.js'; 11import { registerAll } from '../resources/vue_es6/index.js'; 12import { 13 setWd, setWikidataApiUrl, setConfig, setSiteInfo, 14} from './state.js'; 15import { setWidarApiUrl, ttMixin } from '../resources/vue_es6/state.js'; 16import { parseLegacyParams, routeForLegacy } from './util/url-params.js'; 17import { applyDefaults } from './i18n-defaults.js'; 18 19import MainPage from './components/main-page.js'; 20import Classifier from './components/classifier.js'; 21import Sidebar from './components/sidebar.js'; 22import PropertyList from './components/property-list.js'; 23import Claim from './components/claim.js'; 24import Snak from './components/snak.js'; 25import Coordinates from './components/coordinates.js'; 26import Hierarchy from './components/hierarchy.js'; 27import WdHovercard from './components/wd-hovercard.js'; 28import ReasonatorLink from './components/reasonator-link.js'; 29import MiniMap from './components/mini-map.js'; 30import CommonsVideo from './components/commons-video.js'; 31import SearchView from './components/search-view.js'; 32import CalendarView from './components/calendar-view.js'; 33import Backlinks from './components/backlinks.js'; 34import WikiHovercard from './components/wiki-hovercard.js'; 35import PersonRelations from './components/person-relations.js'; 36import TaxonInfo from './components/taxon-info.js'; 37import LocationTools from './components/location-tools.js'; 38import RelatedMedia from './components/related-media.js'; 39import Timeline from './components/timeline.js'; 40import NameInfo from './components/name-info.js'; 41import ListOf from './components/list-of.js'; 42 43// V1's index.php owns the OAuth handshake for both versions: it is the 44// registered consumer callback, and `?path=` says where to return to. 45// Resolves to /index.php whether V2 is served from /v2/ or from the root. 46const WIDAR_ENTRY = '../index.php'; 47 48async function loadConfig() { 49 // config.json is deployment-specific and gitignored, so a fresh checkout / 50 // git-pull deploy won't have it; fall back to the committed 51 // config_template.json (byte-identical defaults) so V2 boots out of the box. 52 // Resolve against this module's URL (not the document URL) so it loads 53 // whether V2 is served from /v2/ or proxied at the site root via index.php. 54 for (const name of ['config.json', 'config_template.json']) { 55 try { 56 const res = await fetch(new URL('./' + name, import.meta.url), { cache: 'force-cache' }); 57 if (res.ok) return res.json(); 58 } catch (_) { /* try the next candidate */ } 59 } 60 throw new Error('config.json / config_template.json: not found'); 61} 62 63// Mirror tt.js's detection (URL â cookie â navigator â 'en'), then substitute 64// 'mul' â 'en' since the tooltranslate server has no 'mul' translation file. 65// `?lang=` (V1's content-language param) takes precedence so a single URL 66// switch flips both interface and content language, matching V1's 67// initializeFromParameters â tt.setLanguage(self.getMainLang()) behaviour. 68function pickInterfaceLanguage(legacyLang) { 69 let lang; 70 if (legacyLang) { 71 lang = String(legacyLang).split(',')[0]; 72 } else { 73 const urlMatch = window.location.href.match(/[#&?]interface_language=([a-z_-]+)/); 74 if (urlMatch) { 75 lang = urlMatch[1]; 76 } else { 77 const cookieMatch = document.cookie.match(/(?:^|;\s*)interface_language=([^;]+)/); 78 if (cookieMatch) { 79 lang = decodeURIComponent(cookieMatch[1]); 80 } else { 81 lang = navigator.userLanguage || navigator.language || 'en'; 82 } 83 } 84 } 85 lang = (lang || 'en').replace(/-.+$/, ''); 86 return lang === 'mul' ? 'en' : lang; 87} 88 89async function loadSiteInfo(apiUrl) { 90 const u = new URL(apiUrl); 91 u.searchParams.set('action', 'query'); 92 u.searchParams.set('meta', 'siteinfo'); 93 u.searchParams.set('siprop', 'general|namespaces|namespacealiases|statistics'); 94 u.searchParams.set('format', 'json'); 95 u.searchParams.set('origin', '*'); 96 const res = await fetch(u); 97 if (!res.ok) throw new Error(`siteinfo: HTTP ${res.status}`); 98 const data = await res.json(); 99 return data.query; 100} 101 102function legacyRedirectIfNeeded() { 103 const params = parseLegacyParams(window.location.search); 104 if (params.q || params.find || params.date) { 105 const target = routeForLegacy(params); 106 if (target === '/') return; 107 const hash = '#' + target; 108 if (window.location.hash !== hash) { 109 const cleanUrl = window.location.pathname + hash; 110 window.history.replaceState(null, '', cleanUrl); 111 } 112 } 113 return params; 114} 115 116const RTL_LANGS = new Set(['fa', 'ar', 'ur', 'dv', 'he', 'arc', 'yi', 'ckb']); 117 118function applyLanguagePreference(wd, lang) { 119 // Mirror V1 main.js initializeFromParameters: push requested languages 120 // onto wd.main_languages so getLabel() / getDesc() prefer them. We use 121 // unshift in reverse so the first-listed language ends up at the front. 122 if (typeof lang !== 'string' || lang.trim() === '') return; 123 const codes = lang.split(',').map(s =>
123 s.trim()).filter(Boolean).reverse(); 124 for (const code of codes) wd.main_languages.unshift(code); 125} 126 127function applyDirectionFor(wd) { 128 // Mirror V1 setRTL (main.js:1026): if the active main language is RTL, 129 // flip <html dir>. Browsers and Bootstrap utilities use the attribute 130 // for layout mirroring; setting it once at boot time is enough. 131 const first = wd?.main_languages?.[0]; 132 document.documentElement.setAttribute('dir', RTL_LANGS.has(first) ? 'rtl' : 'ltr'); 133 document.documentElement.setAttribute('lang', first || 'en'); 134} 135 136function preferredLanguagesFromCookie() { 137 const m = document.cookie.match(/(?:^|;\s*)preferred_languages=([^;]+)/); 138 return m ? decodeURIComponent(m[1]) : ''; 139} 140 141async function bootstrap() { 142 const legacyParams = legacyRedirectIfNeeded() || {}; 143 144 const config = await loadConfig(); 145 setConfig(config); 146 if (config.wikibase_api) setWikidataApiUrl(config.wikibase_api); 147 148 const wd = new WikiData(); 149 if (config.sparql_server_url) wd.sparql_url = config.sparql_server_url; 150 151 // Language preference order: ?lang= URL param first, otherwise the 152 // preferred_languages cookie set by V1's personal-settings dialog. V1 153 // applied these to wd.main_languages before any item load, and we do the 154 // same so labels/descriptions render in the user's preferred language. 155 applyLanguagePreference(wd, legacyParams.lang || preferredLanguagesFromCookie()); 156 applyDirectionFor(wd); 157 158 setWd(wd); 159 160 setWidarApiUrl(WIDAR_ENTRY); 161 162 // Fire-and-forget: site info populates state.siteInfo whenever it lands; 163 // nothing on the critical-path render depends on it. 164 loadSiteInfo(config.wikibase_api).then(setSiteInfo).catch(() => {}); 165 166 // Translations are awaited briefly so the first paint is in the user's 167 // language when fast, but never blocks the app for more than 1.5 s. 168 const ttPromise = initToolTranslate('reasonator_v2', { language: pickInterfaceLanguage(legacyParams.lang) }) 169 .then((tt) => { 170 // Keys not yet present on tooltranslate would otherwise render 171 // as a red key name; fill them with English. See i18n-defaults.js. 172 applyDefaults(tt); 173 return tt; 174 }) 175 .catch((err) => { 176 console.warn('tt init failed:', err); 177 return null; 178 }); 179 180 registerAll(Vue); 181 Vue.component('main-page', MainPage); 182 Vue.component('classifier', Classifier); 183 Vue.component('sidebar', Sidebar); 184 Vue.component('property-list', PropertyList); 185 Vue.component('claim', Claim); 186 Vue.component('snak', Snak); 187 Vue.component('coordinates', Coordinates); 188 Vue.component('hierarchy', Hierarchy); 189 Vue.component('wd-hovercard', WdHovercard); 190 Vue.component('reasonator-link', ReasonatorLink); 191 Vue.component('mini-map', MiniMap); 192 Vue.component('commons-video', CommonsVideo); 193 Vue.component('search-view', SearchView); 194 Vue.component('calendar-view', CalendarView); 195 Vue.component('backlinks', Backlinks); 196 Vue.component('wiki-hovercard', WikiHovercard); 197 Vue.component('person-relations', PersonRelations); 198 Vue.component('taxon-info', TaxonInfo); 199 Vue.component('location-tools', LocationTools); 200 Vue.component('related-media', RelatedMedia); 201 Vue.component('timeline', Timeline); 202 Vue.component('name-info', NameInfo); 203 Vue.component('list-of', ListOf); 204 205 const router = new VueRouter({ 206 mode: 'hash', 207 routes: [ 208 { path: '/', component: MainPage, props: true }, 209 { path: '/search/:term', component: SearchView, props: true }, 210 { path: '/date/:date', component: CalendarView, props: true }, 211 { path: '/:q', component: MainPage, props: true }, 212 ], 213 }); 214 215 await Promise.race([ 216 ttPromise, 217 new Promise((resolve) => setTimeout(resolve, 1500)), 218 ]); 219 220 // Layout root: navbar + search box + the active route's view. Hoisting 221 // the navbar here keeps it visible on /search and /date routes too. 222 const RootView = { 223 mixins: [ttMixin], 224 data() { return { searchTerm: '' }; }, 225 computed: { 226 // Return the reader to the page they were on after the OAuth 227 // round-trip; index.php forwards to "/" + path. 228 widarLoginUrl() { 229 const back = '#' + (this.$route?.fullPath || '/'); 230 return WIDAR_ENTRY + '?action=authorize&path=' + encodeURIComponent(back); 231 }, 232 }, 233 methods: { 234 onSubmitSearch() { 235 const t = (this.searchTerm || '').trim(); 236 if (t === '') return; 237 this.$router.push('/search/' + encodeURIComponent(t)); 238 }, 239 }, 240 template: ` 241<div> 242 <tool-navbar> 243 <template #right> 244 <form class="form-inline ms-auto" @submit.prevent="onSubmitSearch"> 245 <input 246 v-model="searchTerm" 247 type="search" 248 class="form-control form-control-sm" 249 placeholder="Search Wikidata" 250 aria-label="Search" 251 style="min-width:14rem" 252 /> 253 <button class="btn btn-sm btn-outline-secondary ms-1" type="submit">
254 <span tt="find">Find</span> 255 </button> 256 </form> 257 <!-- <widar> registers state.widar, which the label editors read. 258 Logged-out readers get a login link: without one there is no 259 way into the inline label editing V1 offered. --> 260 <span class="ms-3"> 261 <widar> 262 <template #loggedout> 263 <a :href="widarLoginUrl" tt="log_into_widar">Log in to edit</a> 264 </template> 265 </widar> 266 </span> 267 </template> 268 </tool-navbar> 269 <router-view :key="$route.path" /> 270</div>`, 271 }; 272 new Vue({ router, render: (h) => h(RootView) }).$mount('#app'); 273 274 document.getElementById('reasonator-skeleton')?.remove(); 275} 276 277bootstrap().catch((err) => { 278 console.error('Reasonator V2 bootstrap failed:', err); 279 const root = document.getElementById('app'); 280 if (root) { 281 root.innerHTML = 282 '<div class="container mt-3">' + 283 '<div class="alert alert-danger">Reasonator could not start. ' + 284 'Check the browser console for details.</div></div>'; 285 } 286});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.