MFC Tagger+

A set of tools to help MyFigureCollection contributors with tagging items

  1. // ==UserScript==
  2. // @name MFC Tagger+
  3. // @description A set of tools to help MyFigureCollection contributors with tagging items
  4. // @namespace https://takkkane.tumblr.com/scripts/mfcTagCounter
  5. // @version 0.2.1
  6. // @author Nefere
  7. // @homepage https://github.com/Nefere256/mfc-tagger-plus/tree/main
  8. // @supportURL https://github.com/Nefere256/mfc-tagger-plus/tree/main
  9. // @match https://myfigurecollection.net/entry/*
  10. // @match https://myfigurecollection.net/browse.v4.php*
  11. // @match https://myfigurecollection.net/browse/calendar/*
  12. // @match https://myfigurecollection.net/*
  13. // @match https://myfigurecollection.net/item/browse/figure/
  14. // @match https://myfigurecollection.net/item/browse/goods/
  15. // @match https://myfigurecollection.net/item/browse/media/
  16. // @match https://myfigurecollection.net/item/browse/calendar/*
  17. // @icon https://www.google.com/s2/favicons?sz=64&domain=myfigurecollection.net
  18. // @license MIT
  19. // @connect github.com
  20. // @run-at document-idle
  21. // @grant GM_getValue
  22. // @grant GM_setValue
  23. // @grant GM_addStyle
  24. // @grant GM_getResourceText
  25. // @grant GM_xmlhttpRequest
  26. // ==/UserScript==
  27. GM_addStyle(GM_getResourceText('css'));
  28. var app = (function () {
  29. 'use strict';
  30. function noop() { }
  31. function run(fn) {
  32. return fn();
  33. }
  34. function blank_object() {
  35. return Object.create(null);
  36. }
  37. function run_all(fns) {
  38. fns.forEach(run);
  39. }
  40. function is_function(thing) {
  41. return typeof thing === 'function';
  42. }
  43. function safe_not_equal(a, b) {
  44. return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
  45. }
  46. function is_empty(obj) {
  47. return Object.keys(obj).length === 0;
  48. }
  49. const globals = (typeof window !== 'undefined'
  50. ? window
  51. : typeof globalThis !== 'undefined'
  52. ? globalThis
  53. : global);
  54. function detach(node) {
  55. if (node.parentNode) {
  56. node.parentNode.removeChild(node);
  57. }
  58. }
  59. function children(element) {
  60. return Array.from(element.childNodes);
  61. }
  62. function custom_event(type, detail, { bubbles = false, cancelable = false } = {}) {
  63. const e = document.createEvent('CustomEvent');
  64. e.initCustomEvent(type, bubbles, cancelable, detail);
  65. return e;
  66. }
  67. let current_component;
  68. function set_current_component(component) {
  69. current_component = component;
  70. }
  71. const dirty_components = [];
  72. const binding_callbacks = [];
  73. let render_callbacks = [];
  74. const flush_callbacks = [];
  75. const resolved_promise = /* @__PURE__ */ Promise.resolve();
  76. let update_scheduled = false;
  77. function schedule_update() {
  78. if (!update_scheduled) {
  79. update_scheduled = true;
  80. resolved_promise.then(flush);
  81. }
  82. }
  83. function add_render_callback(fn) {
  84. render_callbacks.push(fn);
  85. }
  86. // flush() calls callbacks in this order:
  87. // 1. All beforeUpdate callbacks, in order: parents before children
  88. // 2. All bind:this callbacks, in reverse order: children before parents.
  89. // 3. All afterUpdate callbacks, in order: parents before children. EXCEPT
  90. // for afterUpdates called during the initial onMount, which are called in
  91. // reverse order: children before parents.
  92. // Since callbacks might update component values, which could trigger another
  93. // call to flush(), the following steps guard against this:
  94. // 1. During beforeUpdate, any updated components will be added to the
  95. // dirty_components array and will cause a reentrant call to flush(). Because
  96. // the flush index is kept outside the function, the reentrant call will pick
  97. // up where the earlier call left off and go through all dirty components. The
  98. // current_component value is saved and restored so that the reentrant call will
  99. // not interfere with the "parent" flush() call.
  100. // 2. bind:this callbacks cannot trigger new flush() calls.
  101. // 3. During afterUpdate, any updated components will NOT have their afterUpdate
  102. // callback called a second time; the seen_callbacks set, outside the flush()
  103. // function, guarantees this behavior.
  104. const seen_callbacks = new Set();
  105. let flushidx = 0; // Do *not* move this inside the flush() function
  106. function flush() {
  107. // Do not reenter flush while dirty components are updated, as this can
  108. // result in an infinite loop. Instead, let the inner flush handle it.
  109. // Reentrancy is ok afterwards for bindings etc.
  110. if (flushidx !== 0) {
  111. return;
  112. }
  113. const saved_component = current_component;
  114. do {
  115. // first, call beforeUpdate functions
  116. // and update components
  117. try {
  118. while (flushidx < dirty_components.length) {
  119. const component = dirty_components[flushidx];
  120. flushidx++;
  121. set_current_component(component);
  122. update(component.$$);
  123. }
  124. }
  125. catch (e) {
  126. // reset dirty state to not end up in a deadlocked state and then rethrow
  127. dirty_components.length = 0;
  128. flushidx = 0;
  129. throw e;
  130. }
  131. set_current_component(null);
  132. dirty_components.length = 0;
  133. flushidx = 0;
  134. while (binding_callbacks.length)
  135. binding_callbacks.pop()();
  136. // then, once components are updated, call
  137. // afterUpdate functions. This may cause
  138. // subsequent updates...
  139. for (let i = 0; i < render_callbacks.length; i += 1) {
  140. const callback = render_callbacks[i];
  141. if (!seen_callbacks.has(callback)) {
  142. // ...so guard against infinite loops
  143. seen_callbacks.add(callback);
  144. callback();
  145. }
  146. }
  147. render_callbacks.length = 0;
  148. } while (dirty_components.length);
  149. while (flush_callbacks.length) {
  150. flush_callbacks.pop()();
  151. }
  152. update_scheduled = false;
  153. seen_callbacks.clear();
  154. set_current_component(saved_component);
  155. }
  156. function update($$) {
  157. if ($$.fragment !== null) {
  158. $$.update();
  159. run_all($$.before_update);
  160. const dirty = $$.dirty;
  161. $$.dirty = [-1];
  162. $$.fragment && $$.fragment.p($$.ctx, dirty);
  163. $$.after_update.forEach(add_render_callback);
  164. }
  165. }
  166. /**
  167. * Useful for example to execute remaining `afterUpdate` callbacks before executing `destroy`.
  168. */
  169. function flush_render_callbacks(fns) {
  170. const filtered = [];
  171. const targets = [];
  172. render_callbacks.forEach((c) => fns.indexOf(c) === -1 ? filtered.push(c) : targets.push(c));
  173. targets.forEach((c) => c());
  174. render_callbacks = filtered;
  175. }
  176. const outroing = new Set();
  177. function transition_in(block, local) {
  178. if (block && block.i) {
  179. outroing.delete(block);
  180. block.i(local);
  181. }
  182. }
  183. function mount_component(component, target, anchor, customElement) {
  184. const { fragment, after_update } = component.$$;
  185. fragment && fragment.m(target, anchor);
  186. if (!customElement) {
  187. // onMount happens before the initial afterUpdate
  188. add_render_callback(() => {
  189. const new_on_destroy = component.$$.on_mount.map(run).filter(is_function);
  190. // if the component was destroyed immediately
  191. // it will update the `$$.on_destroy` reference to `null`.
  192. // the destructured on_destroy may still reference to the old array
  193. if (component.$$.on_destroy) {
  194. component.$$.on_destroy.push(...new_on_destroy);
  195. }
  196. else {
  197. // Edge case - component was destroyed immediately,
  198. // most likely as a result of a binding initialising
  199. run_all(new_on_destroy);
  200. }
  201. component.$$.on_mount = [];
  202. });
  203. }
  204. after_update.forEach(add_render_callback);
  205. }
  206. function destroy_component(component, detaching) {
  207. const $$ = component.$$;
  208. if ($$.fragment !== null) {
  209. flush_render_callbacks($$.after_update);
  210. run_all($$.on_destroy);
  211. $$.fragment && $$.fragment.d(detaching);
  212. // TODO null out other refs, including component.$$ (but need to
  213. // preserve final state?)
  214. $$.on_destroy = $$.fragment = null;
  215. $$.ctx = [];
  216. }
  217. }
  218. function make_dirty(component, i) {
  219. if (component.$$.dirty[0] === -1) {
  220. dirty_components.push(component);
  221. schedule_update();
  222. component.$$.dirty.fill(0);
  223. }
  224. component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
  225. }
  226. function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) {
  227. const parent_component = current_component;
  228. set_current_component(component);
  229. const $$ = component.$$ = {
  230. fragment: null,
  231. ctx: [],
  232. // state
  233. props,
  234. update: noop,
  235. not_equal,
  236. bound: blank_object(),
  237. // lifecycle
  238. on_mount: [],
  239. on_destroy: [],
  240. on_disconnect: [],
  241. before_update: [],
  242. after_update: [],
  243. context: new Map(options.context || (parent_component ? parent_component.$$.context : [])),
  244. // everything else
  245. callbacks: blank_object(),
  246. dirty,
  247. skip_bound: false,
  248. root: options.target || parent_component.$$.root
  249. };
  250. append_styles && append_styles($$.root);
  251. let ready = false;
  252. $$.ctx = instance
  253. ? instance(component, options.props || {}, (i, ret, ...rest) => {
  254. const value = rest.length ? rest[0] : ret;
  255. if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
  256. if (!$$.skip_bound && $$.bound[i])
  257. $$.bound[i](value);
  258. if (ready)
  259. make_dirty(component, i);
  260. }
  261. return ret;
  262. })
  263. : [];
  264. $$.update();
  265. ready = true;
  266. run_all($$.before_update);
  267. // `false` as a special case of no DOM component
  268. $$.fragment = create_fragment ? create_fragment($$.ctx) : false;
  269. if (options.target) {
  270. if (options.hydrate) {
  271. const nodes = children(options.target);
  272. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  273. $$.fragment && $$.fragment.l(nodes);
  274. nodes.forEach(detach);
  275. }
  276. else {
  277. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  278. $$.fragment && $$.fragment.c();
  279. }
  280. if (options.intro)
  281. transition_in(component.$$.fragment);
  282. mount_component(component, options.target, options.anchor, options.customElement);
  283. flush();
  284. }
  285. set_current_component(parent_component);
  286. }
  287. /**
  288. * Base class for Svelte components. Used when dev=false.
  289. */
  290. class SvelteComponent {
  291. $destroy() {
  292. destroy_component(this, 1);
  293. this.$destroy = noop;
  294. }
  295. $on(type, callback) {
  296. if (!is_function(callback)) {
  297. return noop;
  298. }
  299. const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
  300. callbacks.push(callback);
  301. return () => {
  302. const index = callbacks.indexOf(callback);
  303. if (index !== -1)
  304. callbacks.splice(index, 1);
  305. };
  306. }
  307. $set($$props) {
  308. if (this.$$set && !is_empty($$props)) {
  309. this.$$.skip_bound = true;
  310. this.$$set($$props);
  311. this.$$.skip_bound = false;
  312. }
  313. }
  314. }
  315. function dispatch_dev(type, detail) {
  316. document.dispatchEvent(custom_event(type, Object.assign({ version: '3.59.2' }, detail), { bubbles: true }));
  317. }
  318. function validate_slots(name, slot, keys) {
  319. for (const slot_key of Object.keys(slot)) {
  320. if (!~keys.indexOf(slot_key)) {
  321. console.warn(`<${name}> received an unexpected slot "${slot_key}".`);
  322. }
  323. }
  324. }
  325. /**
  326. * Base class for Svelte components with some minor dev-enhancements. Used when dev=true.
  327. */
  328. class SvelteComponentDev extends SvelteComponent {
  329. constructor(options) {
  330. if (!options || (!options.target && !options.$$inline)) {
  331. throw new Error("'target' is a required option");
  332. }
  333. super();
  334. }
  335. $destroy() {
  336. super.$destroy();
  337. this.$destroy = () => {
  338. console.warn('Component was already destroyed'); // eslint-disable-line no-console
  339. };
  340. }
  341. $capture_state() { }
  342. $inject_state() { }
  343. }
  344. /* src\App.svelte generated by Svelte v3.59.2 */
  345. const { Object: Object_1, console: console_1 } = globals;
  346. function create_fragment(ctx) {
  347. const block = {
  348. c: noop,
  349. l: function claim(nodes) {
  350. throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");
  351. },
  352. m: noop,
  353. p: noop,
  354. i: noop,
  355. o: noop,
  356. d: noop
  357. };
  358. dispatch_dev("SvelteRegisterBlock", {
  359. block,
  360. id: create_fragment.name,
  361. type: "component",
  362. source: "",
  363. ctx
  364. });
  365. return block;
  366. }
  367. function instance($$self, $$props) {
  368. let { $$slots: slots = {}, $$scope } = $$props;
  369. validate_slots('App', slots, []);
  370. (async function () {
  371. /**
  372. * Name of the class used for a tag counter container.
  373. * It should be not used on the page it's inserted to.
  374. **/
  375. var TAG_CLASSNAME = "us-tag";
  376. /**
  377. * Name of the class absent on the page.
  378. * Used to return empty collection of nodes from a function.
  379. **/
  380. var FAKE_CLASS_PLACEHOLDER = "fake-class-placeholder";
  381. /**
  382. * A time in miliseconds to wait between requests to MFC.
  383. * Too short time may results in "429 - Too many requests" error responses.
  384. * Can be increased with REQUEST_DELAY_MULTIPLIER.
  385. **/
  386. var REQUEST_DELAY = 1000;
  387. /**
  388. * A multipler that is used on REQUEST_DELAY when 429 response error is obtained.
  389. * Should be over 1 to work properly.
  390. **/
  391. var REQUEST_DELAY_MULTIPLIER = 1.1;
  392. /**
  393. * A time in seconds for how long the entry data saved in a cache is considered "fresh" and up to date.
  394. * After the entry data is "stale", it is removed from cache and may be replaced with new data.
  395. **/
  396. var CACHE_FRESH_SECONDS = 10 * 60;
  397. /**
  398. * Map entries for tagCounterCache that are yet to be persisted in the extension storage.
  399. * The contents: see tagCounterCache
  400. **/
  401. var CACHE_SAVE_ENTRIES = [];
  402. /**
  403. * How many entries have to be added to the cache so the cache can be persisted in the extension storage.
  404. * That way if the user gets into another page, some of the data gathered will not be lost.
  405. * It requires using GM.getValue() and GM.setValue()
  406. **/
  407. var CACHE_SAVE_AFTER_SETTING_VALUES_ORDER = 5;
  408. /**
  409. * A cache for tag count indicated in the entry page.
  410. * It's a Map() consisted of:
  411. * * keys: pathname of an entry page ("/entry/39")
  412. * * values: object with fields:
  413. * ** number: integer with number of tags on the entry page (39)
  414. * ** updatedTime: timestamp of when the map was updated.
  415. * Map entries may be deleted after time indicated in CACHE_FRESH_SECONDS.
  416. **/
  417. var tagCounterCache;
  418. /**
  419. * Util method. It let the thread sleep for ms (miliseconds) between calls to MFC website.
  420. **/
  421. function sleep(ms) {
  422. return new Promise(resolve => setTimeout(resolve, ms));
  423. }
  424. /**
  425. * Get tagCounterCache from a persistent storage.
  426. **/
  427. async function getTagCounterCache() {
  428. return new Map(Object.entries(JSON.parse(await GM.getValue('tagCounterCache', '{}'))));
  429. }
  430. /**
  431. * Save tagCounterCache with new CACHE_SAVE_ENTRIES to a persistent storage.
  432. * CACHE_SAVE_ENTRIES will be cleared after succesful save.
  433. **/
  434. async function saveTagCounterCache() {
  435. var newTagCounterCache = await getTagCounterCache();
  436. for (var entry of CACHE_SAVE_ENTRIES) {
  437. newTagCounterCache.set(entry.key, entry.value);
  438. }
  439. GM.setValue('tagCounterCache', JSON.stringify(Object.fromEntries(newTagCounterCache)));
  440. tagCounterCache = newTagCounterCache;
  441. newTagCounterCache.length = 0; /* clear new data as they are persisted */
  442. }
  443. /**
  444. * Save an url and count of tags to both tagCounterCache and CACHE_SAVE_ENTRIES.
  445. * If CACHE_SAVE_ENTRIES will have CACHE_SAVE_AFTER_SETTING_VALUES_ORDER entries,
  446. * the persistent storage will be updated.
  447. **/
  448. async function pushToTagCounterCache(url, tagCounter) {
  449. if (tagCounter) {
  450. var time = Date.now();
  451. var entry = {
  452. key: url,
  453. value: {
  454. 'number': tagCounter,
  455. 'updatedTime': time
  456. }
  457. };
  458. tagCounterCache.set(entry.key, entry.value);
  459. CACHE_SAVE_ENTRIES.push(entry);
  460. if (CACHE_SAVE_ENTRIES.length % CACHE_SAVE_AFTER_SETTING_VALUES_ORDER == 0) {
  461. saveTagCounterCache();
  462. }
  463. }
  464. }
  465. /**
  466. * Get a number of tags for a specified url from cache.
  467. * if the info is stale after CACHE_FRESH_SECONDS since last update of entry,
  468. * the info would be deleted, and the functon will return 0.
  469. * Otherwise, return number of tags.
  470. **/
  471. function getTagCounterFromTagCounterCache(url) {
  472. var tagCounterPair = tagCounterCache.get(url);
  473. if (tagCounterPair == null) {
  474. return 0;
  475. }
  476. var stalePairDate = new Date(tagCounterPair.updatedTime);
  477. stalePairDate.setSeconds(stalePairDate.getSeconds() + CACHE_FRESH_SECONDS);
  478. if (stalePairDate < Date.now()) {
  479. tagCounterCache.delete(url);
  480. return 0;
  481. }
  482. return tagCounterPair.number;
  483. }
  484. /**
  485. * Add a style for tag counter container (with a TAG_CLASSNAME class).
  486. * It's done only once the page is loaded.
  487. **/
  488. function addStyles() {
  489. let style = document.createElement('style');
  490. style.type = 'text/css';
  491. style.innerHTML = "\
  492. .item-icon ." + TAG_CLASSNAME + " {\
  493. position: absolute;\
  494. display: block;\
  495. right: -4px;\
  496. top: -4px;\
  497. padding: 4px;\
  498. border-radius: 3px;\
  499. text-align: center;\
  500. vertical-align: middle;\
  501. min-width: 12px;\
  502. font-weight: 700;\
  503. font-size: 11px;\
  504. color: gold;\
  505. background-color: darkgreen\
  506. }";
  507. document.getElementsByTagName('head')[0].appendChild(style);
  508. }
  509. function getEntryContainers() {
  510. var pathname = window.location.pathname;
  511. var search = window.location.search;
  512. var searchParams = new URLSearchParams(search);
  513. var tbParam = searchParams.get("_tb");
  514. if (pathname.includes("/entry/") || pathname.includes("/browse.v4.php") || pathname.includes("/browse/calendar/") || pathname.includes("/item/browse/calendar/") || pathname.includes("/item/browse/figure/") || pathname.includes("/item/browse/goods/") || pathname.includes("/item/browse/media/") || tbParam !== null) {
  515. var result = document.querySelectorAll("#wide .result:not(.hidden)"); /* encyclopedia entry */ /* search results with filters */ /* calendar page */ /* new calendar page */ /* new figures page */ /* new goods page */ /* new media page */
  516. return result;
  517. }
  518. console.log("unsupported getEntryContainers");
  519. return document.querySelectorAll(FAKE_CLASS_PLACEHOLDER);
  520. }
  521. /**
  522. * Check if the current page (intended to be one with search results)
  523. * is detailed list.
  524. * The info is taken from GET/query params instead from the page contents.
  525. **/
  526. function isDetailedList() {
  527. var search = window.location.search;
  528. var searchParams = new URLSearchParams(search);
  529. var outputParam = searchParams.get("output"); /* 0 - detailedList, 1,2 - grid, 3 - diaporama */
  530. return outputParam == 0;
  531. }
  532. function getItemsFromContainer(entryContainer) {
  533. var icons = entryContainer.querySelectorAll(".item-icons .item-icon");
  534. if (icons.length > 0) {
  535. return icons;
  536. }
  537. var pathname = window.location.pathname;
  538. if (pathname.includes("/browse.v4.php") && isDetailedList()) {
  539. return document.querySelectorAll(FAKE_CLASS_PLACEHOLDER); /* search page, detailed list view */
  540. }
  541. console.log("unsupported getItemsFromContainer");
  542. return document.querySelectorAll(FAKE_CLASS_PLACEHOLDER);
  543. }
  544. function getTagCounterFromHtml(html) {
  545. var parser = new DOMParser();
  546. var doc = parser.parseFromString(html, 'text/html');
  547. var tagCounterNode = doc.querySelector("div.tbx-target-TAGS .actions > .meta");
  548. if (tagCounterNode == null) console.log("No tag counter element on downloaded html.");
  549. return tagCounterNode.textContent;
  550. }
  551. function addTagCounterToSearchResult(itemLinkElement, countOfTags) {
  552. var tagElement = document.createElement("span");
  553. tagElement.setAttribute("class", TAG_CLASSNAME);
  554. tagElement.textContent = countOfTags;
  555. itemLinkElement.appendChild(tagElement);
  556. }
  557. async function fetchAndHandle(queue) {
  558. var resultQueue = [];
  559. for (var itemElement of queue) {
  560. var itemLinkElement = itemElement.firstChild;
  561. var entryLink = itemLinkElement.getAttribute("href");
  562. fetch(entryLink, {
  563. headers: {
  564. "User-Agent": GM.info.script.name + " " + GM.info.script.version
  565. }
  566. }).then(function (response) {
  567. if (response.ok) {
  568. response.text().then(function (html) {
  569. var countOfTags = getTagCounterFromHtml(html);
  570. addTagCounterToSearchResult(itemLinkElement, countOfTags);
  571. pushToTagCounterCache(entryLink, countOfTags);
  572. });
  573. }
  574. return Promise.reject(response);
  575. }).catch(function (err) {
  576. if (err.status == 429) {
  577. console.warn('Too many requests. Added the request to fetch later', err.url);
  578. resultQueue.push(itemElement);
  579. REQUEST_DELAY = REQUEST_DELAY * REQUEST_DELAY_MULTIPLIER;
  580. console.info('Increased delay to ' + REQUEST_DELAY);
  581. }
  582. });
  583. await sleep(REQUEST_DELAY);
  584. }
  585. return resultQueue;
  586. }
  587. async function main() {
  588. var cacheQueue = [];
  589. var entryContainers = getEntryContainers();
  590. entryContainers.forEach(entryContainer => {
  591. var itemsElements = getItemsFromContainer(entryContainer);
  592. itemsElements.forEach(itemElement => {
  593. cacheQueue.push(itemElement);
  594. });
  595. });
  596. var queue = [];
  597. tagCounterCache = await getTagCounterCache();
  598. for (var itemElement of cacheQueue) {
  599. var itemLinkElement = itemElement.firstChild;
  600. var entryLink = itemLinkElement.getAttribute("href");
  601. var cache = getTagCounterFromTagCounterCache(entryLink);
  602. if (cache > 0) {
  603. addTagCounterToSearchResult(itemLinkElement, cache);
  604. } else {
  605. queue.push(itemElement);
  606. }
  607. }
  608. while (queue.length) {
  609. queue = await fetchAndHandle(queue);
  610. }
  611. saveTagCounterCache();
  612. }
  613. /**
  614. * All variables and methods are set.
  615. * Enjoy the show.
  616. **/
  617. addStyles();
  618. main();
  619. })();
  620. const writable_props = [];
  621. Object_1.keys($$props).forEach(key => {
  622. if (!~writable_props.indexOf(key) && key.slice(0, 2) !== '$$' && key !== 'slot') console_1.warn(`<App> was created with unknown prop '${key}'`);
  623. });
  624. return [];
  625. }
  626. class App extends SvelteComponentDev {
  627. constructor(options) {
  628. super(options);
  629. init(this, options, instance, create_fragment, safe_not_equal, {});
  630. dispatch_dev("SvelteRegisterComponent", {
  631. component: this,
  632. tagName: "App",
  633. options,
  634. id: create_fragment.name
  635. });
  636. }
  637. }
  638. const app = new App({
  639. target: document.body,
  640. props: {
  641. name: "World"
  642. }
  643. });
  644. return app;
  645. })();