PageSourceSearch

https://reasonator.toolforge.org/v2/app.js

js reasonator.toolforge.org collected 2026-10-10 02:21:00 UTC 12,053 bytes, 286 lines download raw bytes

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.