GRO Index Search Helper

Adds additional functionality to the UK General Register Office (GRO) BMD index search

当前为 2016-11-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GRO Index Search Helper
  3. // @description Adds additional functionality to the UK General Register Office (GRO) BMD index search
  4. // @namespace cuffie81.scripts
  5. // @include https://www.gro.gov.uk/gro/content/certificates/indexes_search.asp
  6. // @version 1.6
  7. // @grant none
  8. // @require https://code.jquery.com/jquery-2.2.4.min.js
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js
  10. // ==/UserScript==
  11.  
  12. /*
  13. ======================INLINE_RESOURCE_BEGIN======================
  14. ***********RESOURCE_START=CSS*************
  15. <style type="text/css">
  16. body
  17. {
  18. min-height: 1200px;
  19. }
  20. .groish_ButtonContainer
  21. {
  22. padding-bottom: 10px;
  23. }
  24. .groish_ButtonContainer input[type='submit'],
  25. .groish_ButtonContainer input[type='button']
  26. {
  27. margin-right: 20px;
  28. min-width: 100px;
  29. font-size: 13px;
  30. padding: 4px 10px;
  31. }
  32. .groish_ButtonContainer input[type='submit']
  33. {
  34. margin-right: 0px;
  35. }
  36. #groish_ResultsSelector,
  37. #groish_ViewSwitcher
  38. {
  39. display:inline-block;
  40. position: absolute;
  41. bottom: 0px;
  42. color: #993333;
  43. font-weight: bold;
  44. cursor: pointer;
  45. }
  46. #groish_ResultsSelector
  47. {
  48. right: 120px;
  49. }
  50. #groish_ViewSwitcher
  51. {
  52. right: 10px;
  53. }
  54.  
  55. </style>
  56. *************RESOURCE_END*************
  57.  
  58. ***********RESOURCE_START=Template-EW_Birth*************
  59. <style type="text/css">
  60. div[results-view='EW_Birth'] td
  61. {
  62. padding: 5px 3px;
  63. font-size: 75%;
  64. color: #663333;
  65. vertical-align: top;
  66. }
  67. div[results-view='EW_Birth'] thead td
  68. {
  69. font-weight: bold;
  70. }
  71. div[results-view='EW_Birth'] tbody tr:nth-child(4n+1),
  72. div[results-view='EW_Birth'] tbody tr:nth-child(4n+2)
  73. {
  74. background-color: #F9E8A5;
  75. }
  76. div[results-view='EW_Birth'] tr.rec-actions a
  77. {
  78. padding: 0px 5px;
  79. font-size: 90%;
  80. color: #663333;
  81. text-decoration: none;
  82. }
  83. </style>
  84. <div results-view='EW_Birth' style='display: none; margin-bottom: 25px'>
  85. <table style='width: 100%; border-collapse: collapse'>
  86. <thead>
  87. <tr>
  88. <td style='width: 12%; padding: 5px 3px; font-weight: bold;'>Date</td>
  89. <td style='width: 30%'>Name</td>
  90. <td style='width: 15%'>Mother</td>
  91. <td style='width: 27%'>District</td>
  92. <td style='width: 8%'>Vol</td>
  93. <td style='width: 8%'>Page</td>
  94. </tr>
  95. </thead>
  96. <tbody>
  97. {{#each items}}
  98. <tr class='rec'>
  99. <td>{{year}} Q{{quarter}}</td>
  100. <td><span class='forenames'>{{forenames}}</span> <span class='surname'>{{surname}}</span>{{#if noForenames}} ({{gender}}){{/if}}</td>
  101. <td>{{mother}}</td>
  102. <td>{{district}}</td>
  103. <td>{{volume}}</td>
  104. <td>{{page}}</td>
  105. </tr>
  106. <tr class='rec-actions' style='display: none'>
  107. <td colspan='6' style='text-align: right'>
  108. {{#actions}}
  109. <a href='{{url}}' {{#if title}}title='{{title}}'{{/if}}>{{text}}</a>
  110. {{/actions}}
  111. </td>
  112. </tr>
  113. {{/each}}
  114. </tbody>
  115. </table>
  116. {{#if failures}}
  117. <p class='main_text' style='color: Red'>WARNING: Failed to parse {{failures.length}} records. See default view for full list.</p>
  118. <!--
  119. {{#each failures}}record parse exception ({{index}}): exception: {{ex.message}}{{/each}}
  120. -->
  121. {{/if}}
  122. </div>
  123. *************RESOURCE_END*************
  124.  
  125. ***********RESOURCE_START=Template-EW_Death*************
  126. <style type="text/css">
  127. div[results-view='EW_Death'] td
  128. {
  129. padding: 5px 3px;
  130. font-size: 75%;
  131. color: #663333;
  132. vertical-align: top;
  133. }
  134. div[results-view='EW_Death'] thead td
  135. {
  136. font-weight: bold;
  137. }
  138. div[results-view='EW_Death'] tbody tr:nth-child(4n+1),
  139. div[results-view='EW_Death'] tbody tr:nth-child(4n+2)
  140. {
  141. background-color: #F9E8A5;
  142. }
  143. div[results-view='EW_Death'] tr.rec-actions a
  144. {
  145. padding: 0px 5px;
  146. font-size: 90%;
  147. color: #663333;
  148. text-decoration: none;
  149. }
  150. </style>
  151. <div results-view='EW_Death' style='display: none; margin-bottom: 25px'>
  152. <table style='width: 100%; border-collapse: collapse'>
  153. <thead>
  154. <tr>
  155. <td style='width: 12%'>Date</td>
  156. <td style='width: 26%'>Name</td>
  157. <td style='width: 8%'>Age{{#if ageCautionThreshold}}*{{/if}}</td>
  158. <td style='width: 8%'>Birth</td>
  159. <td style='width: 30%'>District</td>
  160. <td style='width: 8%'>Vol</td>
  161. <td style='width: 8%'>Page</td>
  162. </tr>
  163. </thead>
  164. <tbody>
  165. {{#each items}}
  166. <tr class='rec'>
  167. <td>{{year}} Q{{quarter}}</td>
  168. <td><span class='forenames'>{{forenames}}</span> <span class='surname'>{{surname}}</span>{{#if noForenames}} ({{gender}}){{/if}}</td>
  169. <td>{{age}}{{#if ageCaution}}*{{/if}}</td>
  170. <td>{{birth}}
  171. <td>{{district}}</td>
  172. <td>{{volume}}</td>
  173. <td>{{page}}</td>
  174. </tr>
  175. <tr class='rec-actions' style='display: none'>
  176. <td colspan='7' style='text-align: right'>
  177. {{#actions}}
  178. <a href='{{url}}' {{#if title}}title='{{title}}'{{/if}}>{{text}}</a>
  179. {{/actions}}
  180. </td>
  181. </tr>
  182. {{/each}}
  183. </tbody>
  184. </table>
  185. {{#if failures}}
  186. <p class='main_text' style='color: Red'>WARNING: Failed to parse {{failures.length}} records. See default view for full list.</p>
  187. <!--
  188. {{#each failures}}record parse exception ({{index}}): exception: {{ex.message}}{{/each}}
  189. -->
  190. {{/if}}
  191. <p class='main_text'>
  192. * Age is presumed to be years but <i>may</i> be months.
  193. {{#if ageCautionThreshold}}An age below {{ageCautionThreshold}} <i>may</i> be a child, treat with caution.{{/if}}
  194. An age of zero <i>may</i> have be used when a child was aged less than 12 months.
  195. </p>
  196. </div>
  197. *************RESOURCE_END*************
  198.  
  199. ======================INLINE_RESOURCE_END======================
  200. */
  201.  
  202. this.$ = this.jQuery = jQuery.noConflict(true);
  203.  
  204. $(function()
  205. {
  206. var resources, recordType;
  207. var main = function()
  208. {
  209. resources = getInlineResources();
  210. recordType = getRecordType();
  211. //console.log("resources:\r\n%s", JSON.stringify(resources));
  212. // Load the general css
  213. var cssBlock = resources["CSS"].toString();
  214. $("body").append($(cssBlock));
  215.  
  216. initialiseSearchForm();
  217. initialiseResultViews(recordType, resources);
  218. // Scroll down to the form. Do this last as we may add/remove/chnage elements in the previous calls.
  219. $("h1:contains('Search the GRO Online Index')")[0].scrollIntoView();
  220. // Wire up accesskeys to clicks, to avoid having to use the full accesskey combo (eg ALT+SHFT+#)
  221. $(document).on("keypress", function(e)
  222. {
  223. if (!document.activeElement || document.activeElement.tagName.toLowerCase() !== "input")
  224. {
  225. var char = String.fromCharCode(e.which);
  226. //console.log("keypress: %s", char);
  227. if ($("*[id^='groish'][accesskey='" + char + "']").length)
  228. $("*[id^='groish'][accesskey='" + char + "']").click();
  229. else if (char == "{")
  230. adjustSearchYear(-10);
  231. else if (char == "}")
  232. adjustSearchYear(10);
  233. else if (char == "?")
  234. $("form[name='SearchIndexes'] input[type='submit']").click();
  235. else if (char == '@')
  236. switchRecordType();
  237. }
  238. });
  239. }
  240. var initialiseSearchForm = function()
  241. {
  242. // Hide superfluous spacing, text and buttons
  243. $("body > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(2)").hide();
  244. $("h1:contains('Search the GRO Online Index')").closest("tr").next().hide();
  245. $("strong:contains('Which index would you like to search?')").closest("tr").hide();
  246. $("table[summary*='contains the search form fields'] > tbody > tr:nth-of-type(2)").hide();
  247. $("table[summary*='contains the search form fields'] > tbody > tr:nth-of-type(3) td.main_text[colspan='5']").parent().hide();
  248. $("form[name='SearchIndexes'] input[type='submit'][value='Reset']").hide();
  249. // Change text
  250. $("form[name='SearchIndexes'] td span.main_text:contains('year(s)')").text("yrs");
  251. $("form[name='SearchIndexes'] td.main_text:contains('First Forename at Death:')").text("Forename 1:");
  252. $("form[name='SearchIndexes'] td.main_text:contains('Second Forename at Death:')").text("Forename 2:");
  253. $("form[name='SearchIndexes'] td.main_text:contains('District of Death:')").text("District:");
  254. $("form[name='SearchIndexes'] td.main_text:contains('First Forename:')").text("Forename 1:");
  255. $("form[name='SearchIndexes'] td.main_text:contains('Second Forename:')").text("Forename 2:");
  256. $("form[name='SearchIndexes'] td.main_text:contains('Maiden Surname:')").text("Mother:");
  257. $("form[name='SearchIndexes'] td.main_text:contains('District of Birth:')").text("District:");
  258.  
  259. // Add gender and year navigation buttons, and style them
  260. var searchButton = $("form[name='SearchIndexes'] input[type='submit'][value='Search']");
  261. $(searchButton).attr("accesskey", "?");
  262. $(searchButton).parent().find("br").remove();
  263.  
  264. $("<input type='button' class='formButton' accesskey='#' id='groish_BtnToggleGender' value='Gender' />").insertBefore($(searchButton));
  265. $("<input type='button' class='formButton' accesskey='[' id='groish_BtnYearsPrev' value='&lt; Years' />").insertBefore($(searchButton));
  266. $("<input type='button' class='formButton' accesskey=']' id='groish_BtnYearsNext' value='Years &gt;' />").insertBefore($(searchButton));
  267. var buttonContainer = $("form[name='SearchIndexes'] input[type='submit'][value='Search']").closest("td").addClass("groish_ButtonContainer");
  268. // Add button event handlers
  269. $("input#groish_BtnYearsPrev").click(function() { navigateYears(false); });
  270. $("input#groish_BtnYearsNext").click(function() { navigateYears(true); });
  271. $("input#groish_BtnToggleGender").click(function() { toggleGender(); });
  272. }
  273. var initialiseResultViews = function(recordType, resources)
  274. {
  275. // Move default results table into a view container
  276. var defaultTable = $("form[name='SearchIndexes'] h3:contains('Results:')").closest("table").css("width", "100%").addClass("groish_ResultsTable");
  277. $(defaultTable).before($("<div results-view='default' />"));
  278. var defaultView = $("div[results-view='default']");
  279. $(defaultView).append($("table.groish_ResultsTable"));
  280.  
  281. // Move header row to before default view
  282. $(defaultView).before($("<div class='groish_ResultsHeader' style='margin: 10px 0px; position: relative' />"));
  283. $(".groish_ResultsHeader").append($("table.groish_ResultsTable h3:contains('Results:')"));
  284.  
  285. // Move pager row contents to after default view
  286. $(defaultView).after($("table.groish_ResultsTable > tbody > tr:last table:first"));
  287. $("div[results-view='default'] + table").css("width", "100%").addClass("groish_ResultsInfo");
  288.  
  289. // Add alternate view(s)
  290. if (recordType)
  291. {
  292. var results = getResults(recordType);
  293. //console.log(results);
  294. if (results != null && recordType && results.items != null && results.items.length > 0)
  295. {
  296. // Get template and add alternate view
  297. var template = resources["Template-" + recordType].toString();
  298. var compiledTemplate = Handlebars.compile(template);
  299. var html = compiledTemplate(results);
  300. $(defaultView).after($(html));
  301.  
  302. // Add event handler to hide/show actions row
  303. // TODO: Make adding view event handlers more dynamic, so they can be specific to the view
  304. $("div[results-view][results-view!='default'] tbody tr.rec").click(function(index)
  305. {
  306. $(this).next("tr.rec-actions:not(:empty)").toggle();
  307. });
  308.  
  309.  
  310. // Add view switcher
  311. $(".groish_ResultsHeader").append($("<a href='#' id='groish_ViewSwitcher' class='main_text' accesskey='~'>Switch view</a>"));
  312. $("#groish_ViewSwitcher").on("click", function() { switchResultsView(); return false; });
  313. // Add results selector (if supported)
  314. if (window.getSelection && document.createRange)
  315. {
  316. $(".groish_ResultsHeader").append($("<a href='#' id='groish_ResultsSelector' class='main_text' accesskey='|'>Select results</a>"));
  317. $("#groish_ResultsSelector").on("click", function()
  318. {
  319. var resultsBody = $("div[results-view]:visible tbody")[0];
  320. if (resultsBody)
  321. {
  322. var selection = window.getSelection();
  323. var range = document.createRange();
  324. range.selectNodeContents(resultsBody);
  325. selection.removeAllRanges();
  326. selection.addRange(range);
  327. }
  328. return false;
  329. });
  330. }
  331.  
  332. // Show the last used view
  333. var viewName = sessionStorage.getItem("groish_view." + recordType);
  334. //console.log("initialising view: %s", viewName);
  335. if (viewName && $("div[results-view='" + viewName + "']:hidden").length == 1)
  336. {
  337. //console.log("setting active view: %s", viewName);
  338. $("div[results-view][results-view!='" + viewName + "']").hide();
  339. $("div[results-view][results-view='" + viewName + "']").show();
  340. }
  341. }
  342. }
  343. }
  344. var switchResultsView = function()
  345. {
  346. var recordType = getRecordType();
  347. var views = $("div[results-view]");
  348. if (views.length > 1)
  349. {
  350. var curIndex = -1;
  351. $(views).each(function(index)
  352. {
  353. if ($(this).css("display") != "none")
  354. curIndex = index;
  355. });
  356.  
  357. //console.log("current view index: %s", curIndex);
  358. if (curIndex !== -1)
  359. {
  360. var newIndex = ((curIndex == (views.length-1)) ? 0 : curIndex+1);
  361. $(views).hide();
  362. $("div[results-view]:eq(" + newIndex + ")").show();
  363.  
  364. // Get the name and save it
  365. var viewName = $("div[results-view]:eq(" + newIndex + ")").attr("results-view")
  366. sessionStorage.setItem("groish_view." + recordType, viewName); //save it
  367. //console.log("new view: %s", viewName);
  368. }
  369. }
  370. }
  371. var getResults = function(recordType)
  372. {
  373. var results = { "ageCautionThreshold": 24, "items": [], "failures": [] };
  374. // Lookup record type - birth or death
  375. if (recordType !== null && (recordType === "EW_Birth" || recordType === "EW_Death"))
  376. {
  377. var gender = $("form[name='SearchIndexes'] select#Gender").val();
  378. $("div[results-view='default'] > table > tbody > tr")
  379. .has("img[src='./graphics/order_certificate_button.gif']")
  380. .each(function(index)
  381. {
  382. try
  383. {
  384. //console.log("Parsing record (%d)...", index);
  385. // Get names and reference
  386. var names = $(this).find("td:eq(0)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ').trim();
  387. var ref = $(this).next().find("td:eq(0)").text();
  388.  
  389. // Clean up reference
  390. ref = ref.replace(/\u00a0/g, " ");
  391. ref = ref.replace(/\s\s+/g, ' ');
  392. ref = ref.replace(/GRO Reference: /g, "");
  393. ref = ref.replace(/M Quarter in/g, "Q1");
  394. ref = ref.replace(/J Quarter in/g, "Q2");
  395. ref = ref.replace(/S Quarter in/g, "Q3");
  396. ref = ref.replace(/D Quarter in/g, "Q4");
  397.  
  398. var age = 0;
  399. if (recordType === "EW_Death")
  400. {
  401. var ageArr = /^([0-9]{1,3})$/.exec($(this).find("td:eq(1)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ').trim());
  402. if (ageArr)
  403. age = parseInt(ageArr[1], 10);
  404. }
  405.  
  406. var mother = null;
  407. if (recordType === "EW_Birth")
  408. mother = toTitleCase($(this).find("td:eq(1)").text().replace(/\u00a0/g, " ").replace(/\s\s+/g, ' ')).trim();
  409.  
  410. var actions = [];
  411. var orderCertUrl = $(this).find("a[href^='indexes_order.asp']:eq(0)").prop("href");
  412. var orderPdfUrl = $(this).next().find("a[href^='indexes_order.asp']:eq(0)").prop("href");
  413.  
  414. if (orderCertUrl) actions.push( {"text": "Order Certificate", "url": orderCertUrl });
  415. if (orderPdfUrl) actions.push( {"text": "Order Research Copy", "title": "PDF", "url": orderPdfUrl });
  416.  
  417. // Parse forenames, surname, year, quarter, district, vol, page
  418. var namesArr = /([a-z' -]+),([a-z' -]*)/gi.exec(names);
  419. var refArr = /([0-9]{4}) Q([1-4]) ([a-z\.\-,\(\)0-9\&' ]*)Volume ([a-z0-9]+) Page ([0-9]+)/gi.exec(ref); // NB: the district may not be set in some cases
  420.  
  421. //console.log("index: %d, namesArr: %s, refArr: %s", index, namesArr, refArr);
  422.  
  423. var record =
  424. {
  425. "gender": gender,
  426. "forenames": toTitleCase(namesArr[2]).trim(),
  427. "surname": toTitleCase(namesArr[1]).trim(),
  428. "age": age,
  429. "mother": mother,
  430. "year": parseInt(refArr[1], 10),
  431. "quarter": parseInt(refArr[2], 10),
  432. "district": toTitleCase(refArr[3]).trim(),
  433. "volume": refArr[4].toLowerCase(),
  434. "page": refArr[5],
  435. "actions": actions
  436. };
  437. record.noForenames = (!record.forenames || record.forenames == "-");
  438. record.ageCaution = (age != null && age > 0 && age <= results.ageCautionThreshold);
  439. record.birth = (age != null ? record.year - age : null);
  440. //console.log(record);
  441. results.items.push(record);
  442. }
  443. catch (e)
  444. {
  445. //console.log("Failed to parse record (%d): %s", index, e.message);
  446. results.failures.push({ "index": index, "ex": e });
  447. }
  448. });
  449. }
  450.  
  451. // Sort records
  452. if (results.items.length > 0)
  453. {
  454. results.items.sort(function(a, b)
  455. {
  456. if (a.year == b.year && a.quarter == b.quarter)
  457. return 0;
  458. else if ((a.year > b.year) || (a.year == b.year && a.quarter > b.quarter))
  459. return 1;
  460. else
  461. return -1;
  462. });
  463. }
  464. return results;
  465. }
  466.  
  467.  
  468. var toTitleCase = function(str)
  469. {
  470. return str.replace(/([^\W_]+[^\s-]*) */g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
  471. }
  472. var switchRecordType = function()
  473. {
  474. var recordTypes = $("form[name='SearchIndexes'] input[type='Radio'][name='index']");
  475.  
  476. var curIndex = -1;
  477. for (var i = 0; i < recordTypes.length; i++)
  478. {
  479. if ($(recordTypes).eq(i).prop("checked"))
  480. {
  481. curIndex = i;
  482. break;
  483. }
  484. }
  485. //console.log("current record type: %d", curIndex);
  486.  
  487. if (curIndex >= 0)
  488. {
  489. var nextIndex = (curIndex == (recordTypes.length-1)) ? 0 : curIndex + 1;
  490.  
  491. if (nextIndex != curIndex)
  492. $(recordTypes).eq(nextIndex).prop("checked", true).click();
  493. //console.log("next record type: %d", nextIndex);
  494. }
  495. }
  496.  
  497. var toggleGender = function()
  498. {
  499. var curGender = $("form[name='SearchIndexes'] select#Gender").val();
  500. $("form[name='SearchIndexes'] select#Gender").val((curGender === "F" ? "M" : "F"));
  501. $("form[name='SearchIndexes'] input[type='submit'][value='Search']").click();
  502. }
  503. var adjustSearchYear = function(step)
  504. {
  505. var adjusted = false;
  506. // Get min and max years
  507. var minYear = parseInt($("form[name='SearchIndexes'] select#Year option:eq(2)").val(), 10);
  508. var maxYear = parseInt($("form[name='SearchIndexes'] select#Year option:last").val(), 10);
  509.  
  510. //console.log("Year range: %s - %s", minYear, maxYear);
  511.  
  512. if (!isNaN(step) && !isNaN(minYear) && !isNaN(maxYear))
  513. {
  514. // Read current year and range
  515. var curYear = parseInt($("form[name='SearchIndexes'] select#Year").val(), 10);
  516. var curRange = parseInt($("form[name='SearchIndexes'] select#Range").val(), 10);
  517.  
  518. if (!isNaN(curYear) && !isNaN(curRange))
  519. {
  520. // Calculate the new year
  521. var newYear = curYear+step;
  522. newYear = Math.min(Math.max(newYear, minYear), maxYear);
  523. if (newYear != curYear)
  524. {
  525. $("form[name='SearchIndexes'] select#Year").val(newYear);
  526. adjusted = true;
  527. }
  528. }
  529.  
  530. //console.log("Current year: %d +-%d (%d-%d), New year: %d (%d-%d)", curYear, curRange, curYear-curRange, curYear+curRange, newYear, newYear-curRange, newYear+curRange);
  531. }
  532.  
  533. return adjusted;
  534. }
  535.  
  536. var navigateYears = function(forward)
  537. {
  538. var curRange = parseInt($("form[name='SearchIndexes'] select#Range").val(), 10);
  539. if (!isNaN(curRange))
  540. {
  541. // Calculate the new year
  542. var step = (curRange * 2) + 1;
  543. if (!forward) step = -step;
  544. if (adjustSearchYear(step))
  545. {
  546. $("form[name='SearchIndexes'] input[type='submit'][value='Search']").click();
  547. }
  548. }
  549. }
  550. var getRecordType = function()
  551. {
  552. return $("form[name='SearchIndexes'] input[type='radio'][name='index']:checked").val();
  553. }
  554.  
  555. // https://gist.github.com/aidanhs/5534196
  556. var getInlineResources = function()
  557. {
  558. var resource = {}, len, match, resourceBlocks, inlineResourcesMatch = (/^=+INLINE_RESOURCE_BEGIN=+$([\s\S]*?)^=+INLINE_RESOURCE_END=+$/m).exec(GM_info.scriptSource);
  559. resourceBlocks = (inlineResourcesMatch && inlineResourcesMatch[1].match(/^\**RESOURCE_START[\s\S]*?^\**RESOURCE_END\**$/mg)) || null;
  560. len = (resourceBlocks && resourceBlocks.length) || 0;
  561.  
  562. for (var i = 0; i < len; i++)
  563. {
  564. match = (/^\**RESOURCE_START=(.*?)\**$\s*^([\s\S]*)^\**RESOURCE_END\**$/m).exec(resourceBlocks[i]);
  565. resource[match[1]] = match[2];
  566. }
  567.  
  568. return resource;
  569. }
  570.  
  571. //Get the ball rolling...
  572. main();
  573. });
  574.  
  575.