├── client ├── robots.txt ├── favicon.ico ├── images │ ├── favicon.png │ └── scroll_210.png ├── fonts │ ├── BF_Hebrew.woff │ ├── TeXGyrePagella-Bold.woff │ ├── BitstreamVeraSans-Bold.woff │ ├── TeXGyrePagella-Italic.woff │ ├── TeXGyrePagella-Regular.woff │ ├── BitstreamVeraSans-Oblique.woff │ ├── BitstreamVeraSans-Roman.woff │ ├── TeXGyrePagella-BoldItalic.woff │ └── BitstreamVeraSans-BoldOblique.woff ├── index.html ├── styles │ ├── night.css │ └── base.css └── js │ └── misc │ └── zh_segment.js ├── readme ├── copyright.txt ├── credits.txt ├── mit.txt ├── gust_font_license.txt ├── ofl-1.1.txt └── bitstream_font_license.txt ├── readme.md ├── .gitignore └── server ├── modules ├── email.js └── db.js ├── config.sample.js ├── index_non-js.html └── bibleforge.js /client/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/favicon.ico -------------------------------------------------------------------------------- /client/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/images/favicon.png -------------------------------------------------------------------------------- /client/fonts/BF_Hebrew.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/fonts/BF_Hebrew.woff -------------------------------------------------------------------------------- /client/images/scroll_210.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/images/scroll_210.png -------------------------------------------------------------------------------- /client/fonts/TeXGyrePagella-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/fonts/TeXGyrePagella-Bold.woff -------------------------------------------------------------------------------- /client/fonts/BitstreamVeraSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/fonts/BitstreamVeraSans-Bold.woff -------------------------------------------------------------------------------- /client/fonts/TeXGyrePagella-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/fonts/TeXGyrePagella-Italic.woff -------------------------------------------------------------------------------- /client/fonts/TeXGyrePagella-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/fonts/TeXGyrePagella-Regular.woff -------------------------------------------------------------------------------- /client/fonts/BitstreamVeraSans-Oblique.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/fonts/BitstreamVeraSans-Oblique.woff -------------------------------------------------------------------------------- /client/fonts/BitstreamVeraSans-Roman.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/fonts/BitstreamVeraSans-Roman.woff -------------------------------------------------------------------------------- /client/fonts/TeXGyrePagella-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/fonts/TeXGyrePagella-BoldItalic.woff -------------------------------------------------------------------------------- /client/fonts/BitstreamVeraSans-BoldOblique.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bibleforge/BibleForge/HEAD/client/fonts/BitstreamVeraSans-BoldOblique.woff -------------------------------------------------------------------------------- /readme/copyright.txt: -------------------------------------------------------------------------------- 1 | BibleForge follows the teachings of Jesus, namely 2 | 3 | "all things whatsoever ye would that men should do to you, do ye even so to them." 4 | —Jesus (Matthew 7:12) 5 | 6 | Therefore, BibleForge is free, open source software. BibleForge is licensed under the MIT license. 7 | This basically means that you can copy it, change it, and even sell it. See mit.txt for details. 8 | 9 | Also, see credits.txt for details about specific files. 10 | 11 | Font licenses: 12 | 13 | The Bitstream Vera Fonts fonts are copyrighted under an open source license as found 14 | in bitstream_font_license.txt. The Tex Gyre Pagella fonts are also copyrighted under an 15 | open source copyright as found in gust_font_license.txt. The BF Hebrew and IM Fell DW Pica 16 | fonts are licensed under the SIL Open Font License as found in ofl-1.1.txt. 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

BibleForge

2 | Bible study done right 3 | 4 |

About

5 | 6 | BibleForge is a free, open source Bible Study web app. It aspires to be easy to use, pleasing to use, and useful. It is currently in the early stages of software development. If you would like to help, please dive in. 7 | 8 | To use BibleForge, all you have to do is go to bibleforge.com; however, unlike most web apps, BibleForge can be downloaded and installed. See the instructions on the wiki for details. 9 | 10 |

License

11 | 12 | Most of the BibleForge code is licensed under the MIT. See the readme directory for more details. 13 | 14 |

Contact

15 | 16 | * Blog: blog.bibleforge.com 17 | * Email: info@bibleforge.com 18 | -------------------------------------------------------------------------------- /readme/credits.txt: -------------------------------------------------------------------------------- 1 | The wrench graphic was derived from Justin Force's Wrench.svg (http://wiki.education.ucsb.edu/File:Wrench.svg). 2 | This graphic is under the cc-by-sa 1.0 copyright. 3 | 4 | The favicon and/or touch icon contains one or more elements derived from Alexandre Moore's nuoveXT2 icon set 5 | (http://nuovext.pwsp.net/) and Lawrie Cate's picture Torah 6 | (https://secure.flickr.com/photos/lawriecate/3370859327/sizes/o/in/photostream/). 7 | The nuoveXT2 icon set is licensed under the LGPL 2.1 license. 8 | 9 | The magnifying glass graphic was derived from Derferman's public domain Magnifying glass icon.svg 10 | (http://commons.wikimedia.org/wiki/File:Magnifying_glass_icon.svg). 11 | 12 | The Chinese dictionaries (client/js/misc/zh_s_dict and client/js/misc/zh_t_dict) are licensed under the 13 | CC by-sa 3.0 Unported license and were created with content from the following projects: 14 | 15 | * scim-googlepinyin (https://github.com/tchaikov/scim-googlepinyin) by tchaikov under the Apache License 2 license. 16 | * VimIM Data Project (https://code.google.com/p/vimim-data/) MIT license. 17 | * Wiktionary (https://zh.wiktionary.org) CC by-sa 3.0 Unported. 18 | -------------------------------------------------------------------------------- /readme/mit.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2013 BibleForge , http://bibleforge.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | “Software”), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # BibleForge 3 | # 4 | # @date 09-01-09 5 | # @version alpha (α) 6 | # @link http://BibleForge.com 7 | # @license MIT 8 | # 9 | 10 | # 11 | # The BibleForge motto: 12 | # 13 | # "all things whatsoever ye would that men should do to you, do ye even so to them." 14 | # —Jesus (Matthew 7:12) 15 | # 16 | 17 | # 18 | # Copyright (C) 2022 19 | # 20 | # Permission is hereby granted, free of charge, to any person obtaining 21 | # a copy of this software and associated documentation files (the 22 | # “Software”), to deal in the Software without restriction, including 23 | # without limitation the rights to use, copy, modify, merge, publish, 24 | # distribute, sublicense, and/or sell copies of the Software, and to 25 | # permit persons to whom the Software is furnished to do so. 26 | # 27 | # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 28 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 30 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 31 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 32 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 33 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | # 35 | 36 | client/.htaccess 37 | client/google* 38 | client/sitemap.xml 39 | server/node_modules 40 | server/config.js 41 | -------------------------------------------------------------------------------- /readme/gust_font_license.txt: -------------------------------------------------------------------------------- 1 | NOTE: The GUST font license (GFL) is legally identical to the 2 | LaTeX Project Public License (LPPL), version 1.3c or later, 3 | but please observe its additional but not legally binding clause. 4 | 5 | % This is version 1.0, dated 22 June 2009, of the GUST Font License. 6 | % (GUST is the Polish TeX Users Group, http://www.gust.org.pl) 7 | % 8 | % For the most recent version of this license see 9 | % http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt 10 | % or 11 | % http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt 12 | % 13 | % This work may be distributed and/or modified under the conditions 14 | % of the LaTeX Project Public License, either version 1.3c of this 15 | % license or (at your option) any later version. 16 | % 17 | % Please also observe the following clause: 18 | % 1) it is requested, but not legally required, that derived works be 19 | % distributed only after changing the names of the fonts comprising this 20 | % work and given in an accompanying "manifest", and that the 21 | % files comprising the Work, as listed in the manifest, also be given 22 | % new names. Any exceptions to this request are also given in the 23 | % manifest. 24 | % 25 | % We recommend the manifest be given in a separate file named 26 | % MANIFEST-.txt, where is some unique identification 27 | % of the font family. If a separate "readme" file accompanies the Work, 28 | % we recommend a name of the form README-.txt. 29 | % 30 | % The latest version of the LaTeX Project Public License is in 31 | % http://www.latex-project.org/lppl.txt and version 1.3c or later 32 | % is part of all distributions of LaTeX version 2006/05/20 or later. -------------------------------------------------------------------------------- /server/modules/email.js: -------------------------------------------------------------------------------- 1 | /// Set JSHint options. 2 | // jshint bitwise:true, curly:true, eqeqeq:true, forin:true, immed:true, latedef:true, newcap:true, noarg:true, noempty:true, nonew:true, onevar:true, plusplus:true, quotmark:double, strict:true, undef:true, unused:strict, node:true 3 | 4 | "use strict"; 5 | 6 | exports.init = function (config) 7 | { 8 | var server; 9 | 10 | function init() 11 | { 12 | server = require("emailjs/email").server.connect({ 13 | user: config.user, 14 | password: config.pass, 15 | host: config.host, 16 | ssl: config.ssl, 17 | tls: config.tls, 18 | domain: config.domain, 19 | port: config.port, 20 | }); 21 | } 22 | 23 | init(); 24 | 25 | return { 26 | /** 27 | * Send an email from a user. 28 | * 29 | * @param data (object) An object describing the message 30 | * Object structure: 31 | * message: "The message to send; only plain text is allowed" 32 | * submitter_name: "(optional) The name to use for the reply-to address" 33 | * submitter_email: "(optional) The address to use in the reply-to header" 34 | * @param callback (function) (optional) The function to call after an email is sent 35 | * The function will be sent TRUE on success and FALSE if the email fails to send. 36 | */ 37 | send_user_message: function (data, callback) 38 | { 39 | var message_data = { 40 | text: data.message, 41 | from: config.from, 42 | to: config.to, 43 | subject: "BibleForge User Message: " + (data.submitter_name || "anonymous") + " <" + (data.submitter_email || "NOEMAIL") + ">", 44 | }; 45 | 46 | /// If the user submitted an email address, use that address in the reply-to header. 47 | if (data.submitter_email) { 48 | /// The reply-to header should be the submitter's name and address, if any, a reply can be made to him. 49 | /// Email format is "[NAME ]" 50 | message_data["reply-to"] = (data.submitter_name ? data.submitter_name + " " : "") + "<" + data.submitter_email + ">"; 51 | } 52 | 53 | server.send(message_data, function server_response(err, message) 54 | { 55 | var res; 56 | 57 | if (err) { 58 | ///NOTE: If an error occurs, it seems to keep an open connection (the script will not terminate). 59 | console.log("Error sending an email!"); 60 | console.log(err); 61 | console.log(message); 62 | res = false; 63 | /// Reinitialize the client since when there is an error, it gets messed up. 64 | init(); 65 | } else { 66 | res = true; 67 | } 68 | 69 | if (callback) { 70 | callback(res); 71 | } 72 | }); 73 | } 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /server/config.sample.js: -------------------------------------------------------------------------------- 1 | /** 2 | * BibleForge 3 | * 4 | * @date 05-15-12 5 | * @version alpha (α) 6 | * @link http://BibleForge.com 7 | * @license The MIT License (MIT) 8 | */ 9 | 10 | /*! 11 | * The BibleForge motto: 12 | * 13 | * "all things whatsoever ye would that men should do to you, do ye even so to them." 14 | * —Jesus (Matthew 7:12) 15 | */ 16 | 17 | /*! 18 | * Copyright (C) 2022 19 | * 20 | * Permission is hereby granted, free of charge, to any person obtaining 21 | * a copy of this software and associated documentation files (the 22 | * “Software”), to deal in the Software without restriction, including 23 | * without limitation the rights to use, copy, modify, merge, publish, 24 | * distribute, sublicense, and/or sell copies of the Software, and to 25 | * permit persons to whom the Software is furnished to do so. 26 | * 27 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 28 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 30 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 31 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 32 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 33 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | */ 35 | 36 | /// Set JSHint options. 37 | // jshint bitwise:true, curly:true, eqeqeq:true, forin:true, immed:true, latedef:true, newcap:true, noarg:true, noempty:true, nonew:true, onevar:true, plusplus:true, quotmark:double, strict:true, undef:true, unused:strict, node:true 38 | 39 | "use strict"; 40 | 41 | exports.config = { 42 | cache_simple_html: true, /// Whether or not to cache the contents of index_non-js.html. Should be TRUE on production servers. 43 | /// The database options 44 | db: [ 45 | /// Database server #1 46 | { 47 | base: "bf", /// The database name 48 | ///NOTE: If connecting via a network host, remove the "sock" property. 49 | host: "127.0.0.1", /// The hostname to connect to 50 | user: "user", /// The database username 51 | pass: "password", /// The user's password 52 | port: 3306, /// The port to connect to (must be an integer) (optional: default 3306) 53 | ///NOTE: If connecting via a file socket, remove the "host" and "port" properties. 54 | /// If not using a Unix socket file, remove the following line. 55 | sock: "mysqld.sock" /// The Unix socket file 56 | }, 57 | ], 58 | use_ssl: false, /// Whether or not to use SSL (partially implemented) 59 | port: 7777, /// The port for the BibleForge server to listen to. This is the port that HTTP server forwards requests to, not the port of the HTTP server. 60 | /// The SMTP info for sending emails 61 | smtp: { 62 | user: "email@address.com", /// SMTP username 63 | pass: "password", /// SMTP password 64 | host: "smtp.address.com", /// SMTP server 65 | port: null, /// SMTP port (if NULL, a standard port will be used) 66 | from: "sender ", /// The address displayed in the FROM header (does not need to be the same as user) 67 | to: "recipient ", /// The address to send user generated emails to 68 | ssl: true, /// Whether or not to use SSL when connecting to the SMTP server 69 | tls: false, /// Whether or not to use TLS when connecting to the SMTP server 70 | domain: "bibleforge.com", /// The domain to claim when connecting (optional) 71 | }, 72 | static_path: "../client/" /// The folder containing files sent to the client. The server also includes some of them. 73 | }; 74 | -------------------------------------------------------------------------------- /server/index_non-js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 50 | {TITLE} 51 | 52 | {LANG_CSS} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 67 | 68 | 69 | 74 | 75 |
{LANG_SELECT}
{UNSUPPORTED_WARNING}
{CONTENT}
101 | -------------------------------------------------------------------------------- /readme/ofl-1.1.txt: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 (26 February 2007) 2 | 3 | PREAMBLE 4 | 5 | The goals of the Open Font License (OFL) are to stimulate worldwide 6 | development of collaborative font projects, to support the font creation 7 | efforts of academic and linguistic communities, and to provide a free 8 | and open framework in which fonts may be shared and improved in 9 | partnership with others. 10 | 11 | The OFL allows the licensed fonts to be used, studied, modified and 12 | redistributed freely as long as they are not sold by themselves. The 13 | fonts, including any derivative works, can be bundled, embedded, 14 | redistributed and/or sold with any software provided that any reserved 15 | names are not used by derivative works. The fonts and derivatives, 16 | however, cannot be released under any other type of license. The 17 | requirement for fonts to remain under this license does not apply to 18 | any document created using the fonts or their derivatives. 19 | 20 | DEFINITIONS 21 | 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components 30 | as distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software 35 | to a new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical writer 38 | or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | 42 | Permission is hereby granted, free of charge, to any person obtaining a 43 | copy of the Font Software, to use, study, copy, merge, embed, modify, 44 | redistribute, and sell modified and unmodified copies of the Font 45 | Software, subject to the following conditions: 46 | 47 | 1) Neither the Font Software nor any of its individual components, in 48 | Original or Modified Versions, may be sold by itself. 49 | 50 | 2) Original or Modified Versions of the Font Software may be bundled, 51 | redistributed and/or sold with any software, provided that each copy 52 | contains the above copyright notice and this license. These can be 53 | included either as stand-alone text files, human-readable headers or in 54 | the appropriate machine-readable metadata fields within text or binary 55 | files as long as those fields can be easily viewed by the user. 56 | 57 | 3) No Modified Version of the Font Software may use the Reserved Font 58 | Name(s) unless explicit written permission is granted by the 59 | corresponding Copyright Holder. This restriction only applies to the 60 | primary font name as presented to the users. 61 | 62 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 63 | Software shall not be used to promote, endorse or advertise any 64 | Modified Version, except to acknowledge the contribution(s) of the 65 | Copyright Holder(s) and the Author(s) or with their explicit written 66 | permission. 67 | 68 | 5) The Font Software, modified or unmodified, in part or in whole, 69 | must be distributed entirely under this license, and must not be 70 | distributed under any other license. The requirement for fonts to 71 | remain under this license does not apply to any document created 72 | using the Font Software. 73 | 74 | TERMINATION 75 | 76 | This license becomes null and void if any of the above conditions are 77 | not met. 78 | 79 | DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", 80 | WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 81 | INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 82 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 83 | NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR 84 | OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE 85 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 86 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR 87 | CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF 88 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE 89 | USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 90 | OTHER DEALINGS IN THE FONT SOFTWARE. 91 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 67 | 68 | BibleForge 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 81 | 82 | 85 | 92 | 93 | 94 | 95 | 96 |
122 | -------------------------------------------------------------------------------- /readme/bitstream_font_license.txt: -------------------------------------------------------------------------------- 1 | Bitstream Vera Fonts Copyright 2 | 3 | The fonts have a generous copyright, allowing derivative works (as 4 | long as "Bitstream" or "Vera" are not in the names), and full 5 | redistribution (so long as they are not *sold* by themselves). They 6 | can be be bundled, redistributed and sold with any software. 7 | 8 | The fonts are distributed under the following copyright: 9 | 10 | Copyright 11 | ========= 12 | 13 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream 14 | Vera is a trademark of Bitstream, Inc. 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining 17 | a copy of the fonts accompanying this license ("Fonts") and associated 18 | documentation files (the "Font Software"), to reproduce and distribute 19 | the Font Software, including without limitation the rights to use, 20 | copy, merge, publish, distribute, and/or sell copies of the Font 21 | Software, and to permit persons to whom the Font Software is furnished 22 | to do so, subject to the following conditions: 23 | 24 | The above copyright and trademark notices and this permission notice 25 | shall be included in all copies of one or more of the Font Software 26 | typefaces. 27 | 28 | The Font Software may be modified, altered, or added to, and in 29 | particular the designs of glyphs or characters in the Fonts may be 30 | modified and additional glyphs or characters may be added to the 31 | Fonts, only if the fonts are renamed to names not containing either 32 | the words "Bitstream" or the word "Vera". 33 | 34 | This License becomes null and void to the extent applicable to Fonts 35 | or Font Software that has been modified and is distributed under the 36 | "Bitstream Vera" names. 37 | 38 | The Font Software may be sold as part of a larger software package but 39 | no copy of one or more of the Font Software typefaces may be sold by 40 | itself. 41 | 42 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 43 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 44 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 45 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL 46 | BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR 47 | OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, 48 | OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR 49 | OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT 50 | SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. 51 | 52 | Except as contained in this notice, the names of Gnome, the Gnome 53 | Foundation, and Bitstream Inc., shall not be used in advertising or 54 | otherwise to promote the sale, use or other dealings in this Font 55 | Software without prior written authorization from the Gnome Foundation 56 | or Bitstream Inc., respectively. For further information, contact: 57 | fonts at gnome dot org. 58 | 59 | Copyright FAQ 60 | ============= 61 | 62 | 1. I don't understand the resale restriction... What gives? 63 | 64 | Bitstream is giving away these fonts, but wishes to ensure its 65 | competitors can't just drop the fonts as is into a font sale system 66 | and sell them as is. It seems fair that if Bitstream can't make money 67 | from the Bitstream Vera fonts, their competitors should not be able to 68 | do so either. You can sell the fonts as part of any software package, 69 | however. 70 | 71 | 2. I want to package these fonts separately for distribution and 72 | sale as part of a larger software package or system. Can I do so? 73 | 74 | Yes. A RPM or Debian package is a "larger software package" to begin 75 | with, and you aren't selling them independently by themselves. 76 | See 1. above. 77 | 78 | 3. Are derivative works allowed? 79 | Yes! 80 | 81 | 4. Can I change or add to the font(s)? 82 | Yes, but you must change the name(s) of the font(s). 83 | 84 | 5. Under what terms are derivative works allowed? 85 | 86 | You must change the name(s) of the fonts. This is to ensure the 87 | quality of the fonts, both to protect Bitstream and Gnome. We want to 88 | ensure that if an application has opened a font specifically of these 89 | names, it gets what it expects (though of course, using fontconfig, 90 | substitutions could still could have occurred during font 91 | opening). You must include the Bitstream copyright. Additional 92 | copyrights can be added, as per copyright law. Happy Font Hacking! 93 | 94 | 6. If I have improvements for Bitstream Vera, is it possible they might get 95 | adopted in future versions? 96 | 97 | Yes. The contract between the Gnome Foundation and Bitstream has 98 | provisions for working with Bitstream to ensure quality additions to 99 | the Bitstream Vera font family. Please contact us if you have such 100 | additions. Note, that in general, we will want such additions for the 101 | entire family, not just a single font, and that you'll have to keep 102 | both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add 103 | glyphs to the font, they must be stylistically in keeping with Vera's 104 | design. Vera cannot become a "ransom note" font. Jim Lyles will be 105 | providing a document describing the design elements used in Vera, as a 106 | guide and aid for people interested in contributing to Vera. 107 | 108 | 7. I want to sell a software package that uses these fonts: Can I do so? 109 | 110 | Sure. Bundle the fonts with your software and sell your software 111 | with the fonts. That is the intent of the copyright. 112 | 113 | 8. If applications have built the names "Bitstream Vera" into them, 114 | can I override this somehow to use fonts of my choosing? 115 | 116 | This depends on exact details of the software. Most open source 117 | systems and software (e.g., Gnome, KDE, etc.) are now converting to 118 | use fontconfig (see www.fontconfig.org) to handle font configuration, 119 | selection and substitution; it has provisions for overriding font 120 | names and subsituting alternatives. An example is provided by the 121 | supplied local.conf file, which chooses the family Bitstream Vera for 122 | "sans", "serif" and "monospace". Other software (e.g., the XFree86 123 | core server) has other mechanisms for font substitution. 124 | 125 | -------------------------------------------------------------------------------- /client/styles/night.css: -------------------------------------------------------------------------------- 1 | /** 2 | * BibleForge 3 | * 4 | * @date 11-30-12 5 | * @version alpha (α) 6 | * @link http://BibleForge.com 7 | * @license The MIT License (MIT) 8 | */ 9 | 10 | /*! 11 | * The BibleForge motto: 12 | * 13 | * "all things whatsoever ye would that men should do to you, do ye even so to them." 14 | * —Jesus (Matthew 7:12) 15 | */ 16 | 17 | /*! 18 | * Copyright (C) 2022 19 | * 20 | * Permission is hereby granted, free of charge, to any person obtaining 21 | * a copy of this software and associated documentation files (the 22 | * “Software”), to deal in the Software without restriction, including 23 | * without limitation the rights to use, copy, modify, merge, publish, 24 | * distribute, sublicense, and/or sell copies of the Software, and to 25 | * permit persons to whom the Software is furnished to do so. 26 | * 27 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 28 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 30 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 31 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 32 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 33 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | */ 35 | 36 | /* NOTE: It might be nice to apply transitions to all of these elements (without the .night) in order to fade in and out of Night Mode; */ 37 | /* however, currently gradients do not transition (although the spec says they should), so it doesn't really work right now. */ 38 | 39 | /* NOTE: Both the and tags have a background color. */ 40 | /* Also, if the tag's background is not set, parts of the viewport may appear white. */ 41 | /* NOTE: Since the tag has the class, the class name must come after its tag name. */ 42 | .night body, html.night { 43 | background: #121212; 44 | color: #a7a7a3; 45 | } 46 | 47 | .night .q { 48 | color: #F33; 49 | } 50 | .night .black_letter .q { 51 | color: #828282; 52 | } 53 | 54 | .night .searchBars { 55 | /* Transparent Black background for Firefox 3.5-/Opera 10 */ 56 | background: rgba(0,0,0,.79); 57 | /* Chrome/Safari 5.1+ */ 58 | background: -webkit-linear-gradient(rgba(0,0,0,.93), rgba(0,0,0,.86) 25%, rgba(0,0,0,.7)); 59 | /* IE 10, Firefox 16+, Opera 12.10+ */ 60 | background: linear-gradient(rgba(0,0,0,.93), rgba(0,0,0,.86) 25%, rgba(0,0,0,.7)); 61 | } 62 | 63 | .night .infoBars { 64 | background: rgba(0,0,0,0.65); 65 | border-color: #000; 66 | } 67 | 68 | .night .infoLeft { 69 | /* Add a slight radial blur to make sure the text is always readable.*/ 70 | /* Chrome/Safari 5.1+ */ 71 | background: -webkit-radial-gradient(ellipse farthest-corner, #000 70%, rgba(0,0,0,0) 100%); 72 | /* IE 10, Firefox 16+, Opera 12.10+ */ 73 | background: radial-gradient(ellipse farthest-corner, #000 70%, rgba(0,0,0,0) 100%); 74 | } 75 | 76 | .night .queryInput { 77 | background: rgba(0,0,0,.2); 78 | color: rgba(130,130,130,.7); 79 | border-color: rgba(130,130,130,.5); 80 | } 81 | 82 | .night .queryInput:hover { 83 | border-color: rgba(130,130,130,.85); 84 | } 85 | 86 | .night .queryInput:focus { 87 | background: #000; 88 | color: #828282; 89 | border-color: #828282; 90 | } 91 | 92 | .night .lang { 93 | color: rgba(130,130,130,.7); 94 | border-right-color: rgba(130,130,130,.55); 95 | } 96 | 97 | /* NOTE: The activeLang class is used to keep the language button highlighted while the context menu is open. */ 98 | .night .lang:hover, .night .activeLang { 99 | color: #828282; 100 | border-right-color: #828282; 101 | } 102 | 103 | 104 | .night .callout, .night .panel { 105 | background: rgba(18,18,18,.93); 106 | border-color: #828282; 107 | } 108 | 109 | .night .callout .pointer-down { 110 | /* Chrome/Safari 5.1+ */ 111 | background: -webkit-linear-gradient(left top, rgba(0,0,0,0) 41%, rgba(18,18,18,.97) 41%); 112 | /* IE 10, Firefox 16+, Opera 12.10+ */ 113 | background: linear-gradient(to right bottom, rgba(0,0,0,0) 41%, rgba(18,18,18,.97) 41%); 114 | border-color: #828282; 115 | } 116 | 117 | .night .callout .pointer-up { 118 | /* Chrome/Safari 5.1+ */ 119 | background: -webkit-linear-gradient(right bottom, rgba(0,0,0,0) 45%, rgba(18,18,18,.97) 45%); 120 | /* IE 10, Firefox 16+, Opera 12.10+ */ 121 | background: linear-gradient(to left top, rgba(0,0,0,0) 45%, rgba(18,18,18,.97) 45%); 122 | border-color: #828282; 123 | } 124 | 125 | .night .transparent_el { 126 | background: #000; 127 | } 128 | 129 | .night .more-button { 130 | border-color: rgba(130,130,130,.4); 131 | color: rgba(130,130,130,.6); 132 | background: rgba(0,0,0,0); 133 | } 134 | 135 | .night .button { 136 | border-color: rgba(130,130,130,.55); 137 | color: rgba(130,130,130,.7); 138 | background: rgba(0,0,0,0); 139 | } 140 | 141 | .night .more-button:hover, .night .button:hover { 142 | border-color: #828282; 143 | color: #828282; 144 | background: rgba(0,0,0,1); 145 | } 146 | 147 | .night .button { 148 | box-shadow: none; 149 | text-shadow: none; 150 | } 151 | 152 | .night .emailForm input, .night .emailForm textarea { 153 | background: #121212; 154 | color: #828282; 155 | border-color: #828282; 156 | } 157 | 158 | 159 | .night .contextMenu { 160 | background: rgba(18,18,18,.94); 161 | } 162 | 163 | .night .menu_item_selected { 164 | background: #828282; 165 | color: #000; 166 | } 167 | 168 | .night .panel a { 169 | color: #EEE; 170 | } 171 | 172 | .night .inputIcon { 173 | background-image: url(data:image/gif;base64,R0lGODdhEAAQAMIIAAEBAS0tLWFhYYyMjKWlpcnJyeLi4vv7+ywAAAAAEAAQAAADQwi6EHTHDEbFISGIcgIFlscMx3cIH1BMTJkKBhO430zRlN2KFEH0OMWMJ4TwLCjZiQSBJBdOWapJnBpTDMMVu+BUsQkAOw==); 174 | } 175 | 176 | .night .loaders { 177 | background-image: url(data:image/gif;base64,R0lGODlhIAAgAOMMABISEhsbGycnJzk5OUNDQ0lJSWVlZXt7e6mpqcnJyeHh4fv7+////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQECgD/ACwAAAAAIAAgAAAE5xDISalZpurNp1pKJQSdVhzVolJDUZRUoCgIpS5T4BIwNSsvyW1CcAl6k8MsMRkCBDskJTFDAZyuAkkqKfxIQ2hhQBFvAYVEIjNBVDW6XNE4MagTiOBAwe60smQUCHd4Rz1ZBQRnFAWDd0hihh12CUE9kjABVlycXIg7AQMGB6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YBvpJivxNaGmLHT0VnOgWYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ/V/nmOM82XiHQLYKhKP1oZmADdEAAAh+QQFCgAPACwAAAAAGAAXAAAEcvDJSasqNeuZVKqHsYmUYlLJsmjJV56SoSpsS0wmLSnqoRktBA52mG0eiBYp9xCoFqRNy8VEqFzNw+GGwlJkgwlBqxVIBAnMkVIghwLrjcDti2/Gh7D9qN774wEEBYOEfw+ChIV/gYmDho+QkZKTR3p7EQAh+QQFCgAPACwBAAAAHQAOAAAEcvDJSWdJNev9UEJZwU0CUSUohSjKKB2HOKGYVLD1CB/CnEoJlktC2PlyuKHEADM9aI8ASyGbBDYC4zN1YIEmh8XCECgQehNmTNNaKsQLXmFOuEYPg9EgAV/UBnNzeUp9VBQBBIFOLmFxWHNoQwiRWEocEQAh+QQFCgAPACwHAAAAGQARAAAEafDJ+cYZNOttjsnf9gRCdpxZkohFgU3nQani0xYBjEqIGmqCFkEnexhUiNqD0Co9YhLVShm4SaAPWkahKFAGTU0h4RxzFWJnDXFWJJWb9pTihRu5dvghl+/7NQuBggo/fYKHC38PiAuEEQAh+QQFCgAPACwOAAAAEgAYAAAEZlAY8aq9uBj8COWPIV7GcYDhWAnmCYpb1RIoXBEtmsbt944FU6xiSCRQg0PAgjAmhiBB06mzOBMIWrVihG6/4K9iTD5WyWjFOZ0ohMMH3/ewWLi+9brCCxok8gtmWwYKeXdbdAtuEQAh+QQFCgAPACwOAAAAEgAeAAAEgDCQ8Kq9mBSCB75FCBrGV4XFRaYmahWk14rPQJbm4z53foo2A6tCOBwEJsFQYDRyfAFD84DzTY8+y8CZxQy74GxiTEZ8P+R0QqxGhN8WytugUMDr9cQ5icAr3CYHCzIFCXgmCAsLaxeGJgGKC1Vdggt2bwqKB28FkXAJinJhVCYRACH5BAUKAA8ALA8AAQARAB8AAAR88Mn5RKAYj1KyDxzhZQQnjJQQohRXXGzFDSkHU/cTlCa7uTSUq0DIeVSFU0yiXDo9h6jUEIRKr6hrlPrsOguJhMEZDiOe5cTIoKAgymPKQaE4TwxhyWIvoSs6HnsLE3RqgXwSCXQjghR+h4MTCIsZjRiAGAoLbU4GC3FLEQAh+QQFCgAPACwIAA4AGAASAAAEbPDJSesrOJNg+8wg4Xkgto1oihYqOhyH2FpwLcxUYNSHMReJ20PAkz0Gi0MnkagQYMJHYrFAPBRYCbM5MlAXHKxCYmBaPQqq8pqVIJg+GnUsEVO2nfQizqZPmB1UXHVtE3wVOxUJCoM4H34qEQAh+QQFCgAPACwCABIAHQAOAAAEePDJSat94lKBtBVFkV3HshweRYQFEVCFYpopNbDFICXzkuieweH1CKxCgpJJYZAImJbC4dCcCFYBgwk1QSgUqIRYMj2MLMRJ7LsbPwjl2iTBbicmhilB/vje7VZxNXQKBXNuEnlcKV8dh38TBGcehhUICY58cpA1EQAh+QQFCgAPACwAAA8AGQARAAAEZ9CoReu7OOtb+9pgJnlfaJ7oGRwpqihGCybvK2dIrSCoUGy1xO8i3PgKg0zBpkkkLofopUANoJzPR5T1OBJOCGdMK70QqIKQwcmDlh8B6nCWdXMvA2rIqdlqBFZqGgYHYzcaAwdJGxEAIfkEBQoADwAsAQAIABEAGAAABF2wrHWevfieuZTJ4JNwSzKEmKGQaLZRbSZUcW3feK7vqJEkNgLi96sREwhBy/dDXAKHE+j3uRgOh8KjwEXNsBauNoQ9EMJdEKF8EZOxSvTYlcW4QYK5hVA43wIEAREAIfkEBQoADwAsAAACAA4AHQAABHLwyfnMoXiulTLeS+FNx6aMk7JdqAEKaUclG5IqSiI+w8JKCBwOAfMUEsIT6oEzLB+B57KASFivmatWRqFuudLwDkU4HAYjgXntsawNUUzZbEBLCvGH+kCgDAoFRR4BgGMeBICCGQKAfWSAeUYFdigBihEAOw==); 178 | } 179 | 180 | .night .crown_loader { 181 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAQAAAAngNWGAAAAAXNSR0IArs4c6QAAAXZJREFUKM+F0rtLFQAUB+CfXnWQCIMeQxQEYQ8IJcqgpTIiC4cM2/oferuIDU5tRkUNikWBNBT0IEWEmtKgLtSW4GSbWVLpZNevIeLSvYJnO4fvwDmHk1SF7R5lrVDnnWVM27Q6OGOzLh0+Gteq2bAlHbrtqoRjSn5ZwJD6RJ+SRV/xxtUy22rZlcSAaRPggzGjiU7sK8MuS4kNOJ54rjHRhJbEmN4yPOm7AbxO1GlIEqeN4L6ii+VNP+GzE85WTL7TJOZt/ptOGTdkIlH3H6tJ3FZ0z6zGuKGkVQG1VUd77Kf2BPtjjxXNCUarYI05bQmeRcG0YX3eq7euio54oceChiQ2WlQyan0FOqbWIGj7V+qw6BVuOqWQJGoT1zFpxqVy7zlziRbsThK3bEtwPvFUfxnusKIz8cCUO77goUFvE3uVtJdht28YV0TRUYe8xIwnfvtRud8BvS6YdzdJ9OCyfoddW/0vt5gFCw6u9eMFA45oqqz/ATzLTtJ03pdKAAAAAElFTkSuQmCC); 182 | opacity: .4; 183 | } 184 | 185 | .night .dropdown, .night .lang:after, .night .expandable_summary:before { 186 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAGCAQAAAC/O6xzAAAAAXNSR0IArs4c6QAAAF9JREFUCNdj6OLqSu163/UfCU7uEmPoYuvS7lrY9RUueLTLp4uLoYsJqN6oawlU4nhXYBd/FzMDA0iCGyixuOsLUGVgl0AXCwMEgHXodZV0uQNVwgShEmxAIQ6wdiAAAAUwNSUbjgKTAAAAAElFTkSuQmCC); 187 | } 188 | 189 | .night .wrenchIcon { 190 | background-image: url(data:image/gif;base64,R0lGODdhEAAQAMIIAAMDAyoqKk9PT3R0dJeXl7Gxsc7Ozv39/SwAAAAAEAAQAAADQwi6S3eGsBnMe2My+0zQi3Adn5JNw1gAQXFo6UhwFDc+61LdY6m4vIeAAbwQChEfoIgBLW8njUPmBNwk1ekBW31yqwkAOw==); 191 | opacity: .25; 192 | } 193 | 194 | .lang:active, .activeLang, .more-button:active { 195 | background: rgba(0,0,0,.2); 196 | } 197 | 198 | .night .lang:after { 199 | opacity: .5; 200 | } 201 | .night .lang:hover:after, .activeLang:after { 202 | opacity: .9!important; 203 | } 204 | 205 | .night .menu_item_line div { 206 | border-top: 1px solid #828282; 207 | } 208 | 209 | /* Remove shadows to speed up painting. */ 210 | .night .infoBars, .night .queryInput, .night .contextMenu, .night .panel, .night .button, .night .callout, .night .pointer-down, .night .pointer-up, .night .more-button { 211 | box-shadow: none; 212 | text-shadow: none; 213 | } 214 | 215 | /* WebKit's default font color is too bright, so it needs to be lightened. */ 216 | .night input::-webkit-input-placeholder, .night textarea::-webkit-input-placeholder { 217 | color: rgba(130,130,130,.65); 218 | } 219 | 220 | .night ::-moz-selection { 221 | background: #828282; 222 | color: #000; 223 | } 224 | .night ::selection { 225 | /* Because WebKit handles selection background color differently, it needs to be lighter. */ 226 | background: rgba(130,130,130,.5); 227 | color: #000; 228 | } 229 | 230 | .night .q::-moz-selection { 231 | color: #A00; 232 | } 233 | .night .q::selection { 234 | color: #A00; 235 | } 236 | 237 | .night .menu_item_selected ::-moz-selection, .night .panel a::-moz-selection { 238 | color: #EEE; 239 | } 240 | .night .menu_item_selected ::selection, .night .panel a::selection { 241 | color: #EEE; 242 | } 243 | 244 | 245 | /***********************/ 246 | /* WebKit specific CSS */ 247 | /***********************/ 248 | 249 | /* Set the style for the main scrollbar. */ 250 | /* NOTE: Other scrollbars (like in
tags) do not need to be styled because they are transparent and the background color shows through. */ 251 | html.night > ::-webkit-scrollbar { 252 | background: #000; 253 | } 254 | -------------------------------------------------------------------------------- /server/modules/db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * BibleForge 3 | * 4 | * @date 05-28-12 5 | * @version alpha (α) 6 | * @link http://BibleForge.com 7 | * @license The MIT License (MIT) 8 | */ 9 | 10 | /*! 11 | * The BibleForge motto: 12 | * 13 | * "all things whatsoever ye would that men should do to you, do ye even so to them." 14 | * —Jesus (Matthew 7:12) 15 | */ 16 | 17 | /*! 18 | * Copyright (C) 2022 19 | * 20 | * Permission is hereby granted, free of charge, to any person obtaining 21 | * a copy of this software and associated documentation files (the 22 | * “Software”), to deal in the Software without restriction, including 23 | * without limitation the rights to use, copy, modify, merge, publish, 24 | * distribute, sublicense, and/or sell copies of the Software, and to 25 | * permit persons to whom the Software is furnished to do so. 26 | * 27 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 28 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 30 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 31 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 32 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 33 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | */ 35 | 36 | /// Set JSHint options. 37 | // jshint bitwise:true, curly:true, eqeqeq:true, forin:true, immed:true, latedef:true, newcap:true, noarg:true, noempty:true, nonew:true, onevar:true, plusplus:true, quotmark:double, strict:true, undef:true, unused:strict, node:true 38 | 39 | "use strict"; 40 | 41 | /// How long to wait after an error 42 | var reconnect_delay = 1000; 43 | 44 | /** 45 | * Check if a Node.js module exists. 46 | * 47 | * @param name (string) The name of the module to check for 48 | * @return TRUE if the module was found and FALSE if not 49 | */ 50 | function module_exists(name) 51 | { 52 | try { 53 | require.resolve(name); 54 | return true; 55 | } catch (err) { 56 | return false; 57 | } 58 | } 59 | 60 | /** 61 | * Escape special symbols used in Sphinx and the db connectors. 62 | * 63 | * @param str (string) The string to escape. 64 | * @return An escaped string 65 | */ 66 | function sphinx_escape(str) 67 | { 68 | /// Because question marks (?) are a special symbol to the database connectors, they must be escaped too. 69 | /// Semicolons are special symbols in SphinxQL and need to be escaped. 70 | return str.replace(/\?/g, "\\?").replace(/;/g, "\\\\;"); 71 | } 72 | 73 | /** 74 | * Create an abstraction around the non-blocking Maria/MySQL connector. 75 | * 76 | * @param config (object) A BibleForge DB config object (see init() for details) 77 | */ 78 | function create_connection_non_blocking(config, set_status) 79 | { 80 | var client = new (require("mariasql"))(), 81 | connecting, 82 | connected; 83 | 84 | /** 85 | * Open a connection to the database if one does not already exist. 86 | */ 87 | function connect() 88 | { 89 | /// Make sure an open connection has not already been established or in the process of being established; otherwise, an infinite connection loop could occur. 90 | if (!connected && !connecting) { 91 | connecting = true; 92 | client.connect({ 93 | host: config.host, 94 | user: config.user, 95 | port: config.port, 96 | password: config.pass, 97 | db: config.base, 98 | }); 99 | } 100 | } 101 | 102 | /** 103 | * Reconnect on disconnections. 104 | * 105 | * @param err (object) (optional) An error message from the server 106 | */ 107 | function handle_disconnect(err) 108 | { 109 | connecting = false; 110 | ///TODO: Log disconnection. 111 | connected = false; 112 | set_status(0); 113 | setTimeout(connect, reconnect_delay); 114 | 115 | if (err) { 116 | console.log(err); 117 | } 118 | } 119 | 120 | /// Open a connection. 121 | connect(); 122 | 123 | /// Attach functions to the connect, error, and close events. 124 | client.on("connect", function () 125 | { 126 | connecting = false; 127 | connected = true; 128 | set_status(1); 129 | }).on("error", handle_disconnect).on("close", handle_disconnect); 130 | 131 | return { 132 | /** 133 | * Escape a sphinx query argument. 134 | * 135 | * @example var sql = "SELECT * FROM `table` WHERE query = \"" + db_client.escape_sphinx("fake query;limit=99999") + ";ranker=none\""; 136 | * @param str (string) The string to escape. 137 | * @return An escaped string. 138 | * @note This requires an open connection to a database. 139 | */ 140 | escape_sphinx: function (str) 141 | { 142 | return sphinx_escape(client.escape(str)); 143 | }, 144 | /** 145 | * Send a simple SQL query and get any and all results. 146 | * 147 | * @param sql (string) The SQL string to execute. 148 | * @param callback (function) (optional) The function to call after all of the data is returned or an error occurred 149 | * The callback() function will receive one variable if the query succeeds 150 | * and two (the first will be undefined) on errors. 151 | */ 152 | query: function (sql, callback) 153 | { 154 | /// Send the query. 155 | client.query(sql).on("result", function (res) 156 | { 157 | var data = []; 158 | 159 | res.on("row", function (row) 160 | { 161 | /// Store each row in an array. 162 | data[data.length] = row; 163 | }).on("end", function () 164 | { 165 | /// Once the query has finished, send the results to the callback, if any. 166 | if (callback) { 167 | callback(data); 168 | } 169 | }); 170 | }).on("error", function (err) 171 | { 172 | /// Catch errors. 173 | ///TODO: Log errors. 174 | if (callback) { 175 | callback(undefined, err); 176 | } 177 | }); 178 | }, 179 | }; 180 | } 181 | 182 | /** 183 | * Create an abstraction around the pure JavaScript Maria/MySQL connector. 184 | * 185 | * @param config (object) A BibleForge DB config object (see init() for details) 186 | */ 187 | function create_connection_js(config, set_status) 188 | { 189 | var client, 190 | connecting, 191 | connected; 192 | 193 | /** 194 | * Create the connection object and make the connection. 195 | */ 196 | function create_connection() 197 | { 198 | client = require("mysql").createConnection({ 199 | host: config.host, 200 | user: config.user, 201 | port: config.port, 202 | password: config.pass, 203 | database: config.base, 204 | socketPath: config.sock, 205 | }); 206 | } 207 | 208 | /** 209 | * Open and prepare a connection to the database if one does not already exist. 210 | */ 211 | function connect() 212 | { 213 | if (!connected && !connecting) { 214 | connecting = true; 215 | /// In this module, a connection can only be used once (it cannot be used to reconnect to a server), so a new client object has to be created each time. 216 | create_connection(); 217 | /// Attach functions to the connect, error, and close events. 218 | ///NOTE: This has to be done each time because this connector cannot reuse a connection object. 219 | client.on("error", handle_disconnect); 220 | client.connect(function on_connect(err) 221 | { 222 | /// Was there an error connecting? 223 | if (err) { 224 | handle_disconnect(err); 225 | } else { 226 | /// If no error was reported then the connection apparently opened correctly. 227 | connecting = false; 228 | connected = true; 229 | set_status(1); 230 | } 231 | }); 232 | } 233 | } 234 | 235 | /** 236 | * Reconnect on disconnections. 237 | * 238 | * @param err (object) (optional) An error message from the server 239 | */ 240 | function handle_disconnect(err) 241 | { 242 | connecting = false; 243 | ///TODO: Log disconnection. 244 | connected = false; 245 | set_status(0); 246 | setTimeout(connect, reconnect_delay); 247 | 248 | if (err) { 249 | console.log(err); 250 | } 251 | } 252 | 253 | /// Open a connection. 254 | connect(); 255 | 256 | return { 257 | /** 258 | * Escape a sphinx query argument. 259 | * 260 | * @example var sql = "SELECT * FROM `table` WHERE query = \"" + db_client.escape_sphinx("fake query;limit=99999") + ";ranker=none\""; 261 | * @param str (string) The string to escape. 262 | * @return An escaped string. 263 | * @note This requires an open connection to a database. 264 | */ 265 | escape_sphinx: function (str) 266 | { 267 | /// This module adds quotations around strings, but those will cause issues with Sphinx. 268 | return sphinx_escape(client.escape(str)).slice(1, -1); 269 | }, 270 | /** 271 | * Send a simple SQL query and get the results. 272 | * 273 | * @param sql (string) The SQL string to execute. 274 | * @param callback (function) (optional) The function to call after all of the data is returned or an error occurred 275 | * The callback() function will receive one variable if the query succeeds 276 | * and two (the first will be undefined) on errors. 277 | */ 278 | query: function (sql, callback) 279 | { 280 | /// Send the query. 281 | client.query(sql, function (err, data) 282 | { 283 | if (callback) { 284 | callback(data, err); 285 | } 286 | }); 287 | }, 288 | }; 289 | } 290 | 291 | /** 292 | * Create the database abstraction layer. 293 | * 294 | * @param db_config (array || object) An object or array of objects describing how to configure the database (see config.sample.js) 295 | * Object structure: 296 | * { 297 | * host: "The host's address", 298 | * port: "The port to listen to", 299 | * sock: "The Unix file socket if not listening to a port", 300 | * user: "The database username", 301 | * user: "The database user's password", 302 | * base: "The database to connect to", 303 | * } 304 | */ 305 | exports.db = function init(db_config) 306 | { 307 | var create_connection, 308 | request_a_client, 309 | servers = [], 310 | servers_count; 311 | 312 | /// If the mariasql module exists use that. 313 | ///NOTE: The mariasql module is faster and non-blocking, but it is a C module, so it is not as portable and may not be installed. 314 | if (module_exists("mariasql")) { 315 | create_connection = create_connection_non_blocking; 316 | } else { 317 | /// As a fallback, use the pure JavaScript client. 318 | create_connection = create_connection_js; 319 | } 320 | 321 | /// If only an object was sent, turn it into an array for convenience’s sake. 322 | if (!Array.isArray(db_config)) { 323 | db_config = [db_config]; 324 | } 325 | 326 | servers_count = db_config.length - 1; 327 | 328 | /// Create a connection to each server. 329 | db_config.forEach(function (config) 330 | { 331 | var which = servers.length; 332 | servers[which] = { 333 | connection: create_connection(config, function set_status(status) 334 | { 335 | servers[which].online = status === 1; 336 | }) 337 | }; 338 | }); 339 | 340 | /** 341 | * Get a free database client 342 | * 343 | * @return A client object or FALSE if none could be found 344 | * @note This uses a round robin method. 345 | * @note It would be nice if we could make a server as a backup server and only query it when the other servers are down. 346 | */ 347 | request_a_client = (function () 348 | { 349 | /// Since it adds 1 at the beginning of the loop, we start with -1 to go to 0 (i.e., the first server). 350 | var which_server = -1; 351 | 352 | return function () 353 | { 354 | var tries = 0; 355 | 356 | /// Loop through all available servers (from where we left off) and try to find one online. 357 | for (;;) { 358 | /// Try the next server. 359 | which_server += 1; 360 | 361 | if (which_server > servers_count) { 362 | which_server = 0; 363 | } 364 | 365 | if (servers[which_server].online) { 366 | /// If a server is online, Stop looping and use that server. 367 | break; 368 | } 369 | 370 | /// If that server is offline, keep track of how many we've tried and try the next one. This way we know when we've tried them all. 371 | tries += 1; 372 | 373 | /// If we have tried all of the clients, give up. 374 | if (tries > servers_count) { 375 | return false; 376 | } 377 | } 378 | 379 | ///TODO: Connection pooling. 380 | return servers[which_server].connection; 381 | }; 382 | }()); 383 | 384 | return { 385 | /** 386 | * Execute a query. 387 | * 388 | * @param sql (string) The SQL query to execute 389 | * @param callback (function) The function to call after the query returns 390 | * @note If the database has not yet been connected to, the query will be queued. 391 | */ 392 | query: function query(sql, callback) 393 | { 394 | var client = request_a_client(); 395 | 396 | /// Were no clients available? 397 | if (!client) { 398 | /// If we cannot get a client, wait and try again later. 399 | setTimeout(function () 400 | { 401 | query(sql, callback); 402 | }, 100); 403 | return; 404 | } 405 | 406 | /// Hand off the query to the client. 407 | client.query(sql, callback); 408 | }, 409 | /// For more complex queries (like ones involving escaping) a client object and be requested directly. 410 | /// Client's have the following functions: 411 | /// .escape_sphinx() 412 | /// .query() 413 | request_a_client: request_a_client, 414 | }; 415 | }; 416 | -------------------------------------------------------------------------------- /client/js/misc/zh_segment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * BibleForge 3 | * 4 | * @date 03-19-13 5 | * @version alpha (α) 6 | * @link http://BibleForge.com 7 | * @license The MIT License (MIT) 8 | */ 9 | 10 | /*! 11 | * The BibleForge motto: 12 | * 13 | * "all things whatsoever ye would that men should do to you, do ye even so to them." 14 | * —Jesus (Matthew 7:12) 15 | */ 16 | 17 | /*! 18 | * Copyright (C) 2022 19 | * 20 | * Permission is hereby granted, free of charge, to any person obtaining 21 | * a copy of this software and associated documentation files (the 22 | * “Software”), to deal in the Software without restriction, including 23 | * without limitation the rights to use, copy, modify, merge, publish, 24 | * distribute, sublicense, and/or sell copies of the Software, and to 25 | * permit persons to whom the Software is furnished to do so. 26 | * 27 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 28 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 30 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 31 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 32 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 33 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | */ 35 | 36 | /// Set JSHint options. 37 | // jshint bitwise:true, curly:true, eqeqeq:true, forin:true, immed:true, latedef:true, newcap:true, noarg:true, noempty:true, nonew:true, onevar:true, plusplus:true, quotmark:double, strict:true, undef:true, unused:strict, browser:true, node:true 38 | 39 | (function () 40 | { 41 | "use strict"; 42 | 43 | var analysis, 44 | dict, 45 | p; 46 | 47 | function get_significance(x) 48 | { 49 | var i; 50 | 51 | /// Find which p element to use. 52 | for (i = p.length - 2; i > 0; i -= 1) { 53 | if (x >= p[i].x) { 54 | break; 55 | } 56 | } 57 | 58 | /// If the forumal has not been solved yet for these two points (i, i + 1), solve it now. 59 | if (typeof p[i].a === "undefined") { 60 | p[i].a = Math.pow((p[i + 1].y / p[i].y), (1 / ((p[i + 1].x - p[i].x)))); 61 | p[i].yint = p[i].y / (Math.pow(p[i].a, p[i].x)); 62 | } 63 | 64 | return p[i].yint * Math.pow(p[i].a, x); 65 | } 66 | 67 | function copy(mixed) 68 | { 69 | return JSON.parse(JSON.stringify(mixed)); 70 | } 71 | 72 | function get_probabilities(str) 73 | { 74 | var i, 75 | len = str.length, 76 | prob, 77 | probs = []; 78 | 79 | for (i = 0; i < len; i += 1) { 80 | prob = analysis[str[i]]; 81 | if (!prob) { 82 | /// If the character is unknown, just assign it a simple probability. 83 | prob = {L: 50, M: 1, R: 50, guess: true}; 84 | } 85 | 86 | /// Expand short hand. 87 | if (prob.R === 1 && typeof prob.L === "undefined") { 88 | prob.R = 100; 89 | } else if (prob.L === 1 && typeof prob.R === "undefined") { 90 | prob.L = 100; 91 | } 92 | 93 | probs[probs.length] = prob; 94 | } 95 | 96 | return probs; 97 | } 98 | 99 | function isolate_chinese_characters(str) 100 | { 101 | ///NOTE: This would include punctuation too (\u3000-\u3020\u2014-\u2027\u4e36\ufe4f-\uffe5) 102 | return { 103 | /// Divide the string at non-Chinese parts. 104 | cn: str.split(/[^\u4e00-\u4e35\u4e37-\u9fff\u3400-\u4dff]+/), 105 | /// Get an array of the non-Chinese parts to fill in later. 106 | ///NOTE: If there are no matches, return an empty array so that it has a valid length. 107 | non: str.match(/[^\u4e00-\u4e35\u4e37-\u9fff\u3400-\u4dff]+/g) || [] 108 | }; 109 | } 110 | 111 | function calculate_prob(str, prob, starting, len) 112 | { 113 | var i, 114 | /// Assuming at least a two letter word. 115 | sample_count = 2, 116 | tmp_prob, 117 | total_prob = 0, 118 | word_prob = { 119 | inner_valid: true, 120 | outter_valid: true, 121 | middle_valid: true 122 | }; 123 | 124 | /// Get the left probability. 125 | tmp_prob = prob[starting].L; 126 | if (isNaN(tmp_prob)) { 127 | word_prob.inner_valid = false; 128 | } 129 | total_prob += tmp_prob || 0; 130 | 131 | /// Get the right probability. 132 | tmp_prob = prob[starting + len - 1].R; 133 | if (isNaN(tmp_prob)) { 134 | word_prob.inner_valid = false; 135 | } 136 | total_prob += tmp_prob || 0; 137 | 138 | word_prob.inner = total_prob / sample_count; 139 | 140 | /// Get the outer probability (if any). 141 | if (starting > 0) { 142 | /// The previous character's right probability. 143 | tmp_prob = prob[starting - 1].R; 144 | if (isNaN(tmp_prob)) { 145 | word_prob.outter_valid = false; 146 | } 147 | total_prob += tmp_prob || 0; 148 | sample_count += 1; 149 | } 150 | if (len + starting < str.length - 1) { 151 | /// The next character's left probability. 152 | tmp_prob = prob[starting + len].L; 153 | if (isNaN(tmp_prob)) { 154 | word_prob.outter_valid = false; 155 | } 156 | total_prob += tmp_prob || 0; 157 | sample_count += 1; 158 | } 159 | 160 | word_prob.outter = total_prob / sample_count; 161 | 162 | word_prob.ave_prob = (word_prob.inner + word_prob.outter) / 2; 163 | 164 | /// Check for middle validity. 165 | for (i = starting + 1; i < len + starting - 1; i += 1) { 166 | if (isNaN(prob[i].M)) { 167 | word_prob.middle_valid = false; 168 | break; 169 | } 170 | } 171 | 172 | word_prob.invalid_count = 0; 173 | if (!word_prob.inner_valid) { 174 | word_prob.invalid_count += 1; 175 | } 176 | if (!word_prob.outter_valid) { 177 | word_prob.invalid_count += 1; 178 | } 179 | if (!word_prob.middle_valid) { 180 | word_prob.invalid_count += 1; 181 | } 182 | 183 | return word_prob; 184 | } 185 | 186 | function extract_word(str, prob, starting, len) 187 | { 188 | var dict_pos, 189 | word = {str: str.substr(starting, len)}; 190 | 191 | if ((dict_pos = dict.indexOf("\n" + word.str + "\n")) > -1) { 192 | word.in_dict = true; 193 | word.dict_pos = dict_pos; 194 | /// Check to see if the last character is very commonly found at the end (like 的 and 了). 195 | ///TODO: Measure words after a number. 196 | } else if (word.str.length > 2 && /[了子的儿着吗者呢啊得个过地兒著嗎個過]/.test(word.str.slice(-1)) && (dict_pos = dict.indexOf("\n" + word.str.slice(0, -1) + "\n")) > -1) { 197 | word.in_dict = true; 198 | word.dict_pos = dict_pos; 199 | /// Indicate that one character is not actually in the dictionary. 200 | word.not_in_dict = 1; 201 | /// Check for two character endings, like 的人 and 了吗. 202 | } else if (word.str.length > 3 && /(?:的人|了(?:吗|嗎))/.test(word.str.slice(-2)) && (dict_pos = dict.indexOf("\n" + word.str.slice(0, -2) + "\n")) > -1) { 203 | word.in_dict = true; 204 | word.dict_pos = dict_pos; 205 | /// Indicate that two characters are not actually in the dictionary. 206 | word.not_in_dict = 2; 207 | /// Check for short separable words: 吃了饭 208 | } else if (word.str.length === 3 && /[了着过著過]/.test(word.str.substr(1, 1)) && (dict_pos = dict.indexOf("\n" + word.str.substr(0, 1) + word.str.slice(-1) + "\n")) > -1) { 209 | word.in_dict = true; 210 | word.dict_pos = dict_pos; 211 | /// Indicate that one character is not actually in the dictionary. 212 | word.not_in_dict = 1; 213 | word.separable = true; 214 | } 215 | /// Misc matching that could be added: 216 | /// 在…… 217 | ///NOTE: Also match duplication, like 刚刚、高高兴兴. 218 | 219 | word.prob = calculate_prob(str, prob, starting, len); 220 | 221 | return word; 222 | } 223 | 224 | function examine_str(str, prob, starting) 225 | { 226 | var dictionary_max_len = 5, 227 | i, 228 | len = str.length - starting, 229 | word, 230 | likely_words = [], 231 | possible_words = []; 232 | 233 | if (len > dictionary_max_len) { 234 | len = dictionary_max_len; 235 | } 236 | 237 | for (i = 2; i <= len; i += 1) { 238 | word = extract_word(str, prob, starting, i); 239 | ///TODO: There should be a way to override this. 240 | if (word.in_dict || word.prob.invalid_count === 0 || (word.prob.invalid_count === 1 && !word.prob.middle_valid)) { 241 | possible_words[possible_words.length] = word; 242 | } 243 | 244 | ///TODO: Make a way to override this. 245 | if (!word.in_dict && (word.prob.invalid_count > 1 || (word.prob.invalid_count === 1 && !word.prob.middle_valid))) { 246 | break; 247 | } 248 | } 249 | 250 | possible_words.forEach(function (word) 251 | { 252 | /// The acceptable probability should probably be changeable. 253 | if (word.in_dict || word.prob.ave_prob > 60) { 254 | likely_words[likely_words.length] = word; 255 | } 256 | }); 257 | 258 | return likely_words; 259 | } 260 | 261 | function find_known_and_likely_words(str, prob) 262 | { 263 | var branch, 264 | branches = [], 265 | i, 266 | len = str.length; 267 | 268 | /// Subtract one because we want to examine at least 2 characters. 269 | for (i = 0; i < len - 1; i += 1) { 270 | branch = examine_str(str, prob, i); 271 | branches[i] = branch; 272 | } 273 | 274 | return branches; 275 | } 276 | 277 | function create_char_arr(branches) 278 | { 279 | var char_arr = []; 280 | 281 | branches.forEach(function (branch, x) 282 | { 283 | branch.forEach(function (word_obj) 284 | { 285 | var i; 286 | 287 | for (i = word_obj.str.length - 1; i >= 0; i -= 1) { 288 | if (!char_arr[x + i]) { 289 | char_arr[x + i] = []; 290 | } 291 | char_arr[x + i].push({ 292 | x: x + i, 293 | origin_x: x, 294 | obj: word_obj, 295 | is_beginning: i === 0, 296 | is_end: i === word_obj.str.length - 1, 297 | total_len: word_obj.str.length, 298 | len_left: word_obj.str.length - 1 - i, 299 | }); 300 | } 301 | }); 302 | }); 303 | 304 | return char_arr; 305 | } 306 | 307 | function mark_resolved_branches(char_arr) 308 | { 309 | var currently_disputed, 310 | disputed_count = -1, 311 | resolved_branch = []; 312 | 313 | function at_ending(arr) 314 | { 315 | var i; 316 | 317 | for (i = arr.length - 1; i >= 0; i -= 1) { 318 | if (!arr[i].is_end) { 319 | return false; 320 | } 321 | } 322 | 323 | return true; 324 | } 325 | 326 | function tag_disputed(arr, disputed_count) 327 | { 328 | arr.forEach(function (el) 329 | { 330 | el.obj.disputed_group = disputed_count; 331 | }); 332 | } 333 | 334 | char_arr.forEach(function (arr) 335 | { 336 | /// If there is more than one word, it is disputed. 337 | if (arr.length > 1) { 338 | /// If it's not already known to be disputed, inc group to keep track of the groups. 339 | if (!currently_disputed) { 340 | disputed_count += 1; 341 | } 342 | currently_disputed = true; 343 | /// If it is not disputed and we are at the end, it's resolved! 344 | } else if (!currently_disputed && arr[0].is_end) { 345 | resolved_branch[arr[0].origin_x] = arr[0].obj; 346 | arr[0].obj.resolved = true; 347 | } 348 | 349 | /// Tag them all each time just to make sure no one is missed. 350 | if (currently_disputed) { 351 | tag_disputed(arr, disputed_count); 352 | } 353 | 354 | /// If they are all ending, it's the end of a group. 355 | if (currently_disputed && at_ending(arr)) { 356 | currently_disputed = false; 357 | } 358 | }); 359 | 360 | return disputed_count; 361 | } 362 | 363 | function walk_branches(branch_holder, branches, disputed_group, branch_num_obj, x, mark_callback) 364 | { 365 | var at_end_of_branch = true, 366 | i, 367 | stop_before = branches.length; 368 | 369 | function create_mark_func(obj, callback) 370 | { 371 | return function (branch_num) 372 | { 373 | /// Create new branch object to track branches. 374 | if (!obj.branch) { 375 | obj.branch = {}; 376 | } 377 | 378 | obj.branch[branch_num] = 1; 379 | 380 | /// Add this branch if it's not already known. 381 | if (!branch_holder[branch_num]) { 382 | branch_holder[branch_num] = []; 383 | } 384 | /// Add this word to the branch tracker. 385 | branch_holder[branch_num].push(obj); 386 | 387 | if (callback) { 388 | /// Loop back through the whole tree. 389 | callback(branch_num); 390 | } 391 | }; 392 | } 393 | 394 | for (; x < stop_before; x += 1) { 395 | /// Are there any words in this column? 396 | if (branches[x] && branches[x].length > 0) { 397 | /// Just look for the specific group. 398 | if (branches[x][0].resolved || branches[x][0].disputed_group < disputed_group) { 399 | continue; 400 | } 401 | if (branches[x][0].disputed_group > disputed_group) { 402 | break; 403 | } 404 | 405 | /// Could make it reverse later; just doing it more straightforwardly at first to make it easier to understand. 406 | for (i = 0; i < branches[x].length; i += 1) { 407 | /// stop_before needs to keep moving in based on the shortest distance from start to finish. 408 | if (x + branches[x][i].str.length < stop_before) { 409 | stop_before = x + branches[x][i].str.length; 410 | } 411 | 412 | /// Walk to the next step. 413 | walk_branches(branch_holder, branches, disputed_group, branch_num_obj, x + branches[x][i].str.length, create_mark_func(branches[x][i], mark_callback)); 414 | 415 | /// If it found another route, it's not at a branches end, so it does not need to marked the branches at the end. 416 | at_end_of_branch = false; 417 | } 418 | } 419 | } 420 | 421 | /// Trigger the mark cascade, if any. 422 | if (at_end_of_branch && mark_callback) { 423 | mark_callback(branch_num_obj.num); 424 | branch_num_obj.num += 1; 425 | } 426 | } 427 | 428 | function is_more_likely(sig1, sig2, prob1, prob2) 429 | { 430 | var i, 431 | index, 432 | sig_t1, 433 | sig_t2, 434 | calc_sig; 435 | 436 | function sum(a, b) 437 | { 438 | return a + b; 439 | } 440 | 441 | /// Remove words that are the same so that it doesn't overshadow the average of the significance. 442 | for (i = sig1.length; i >= 0; i -= 1) { 443 | index = sig2.indexOf(sig1[i]); 444 | if (index > -1) { 445 | sig1.splice(i, 1); 446 | sig2.splice(index, 1); 447 | } 448 | } 449 | 450 | if (sig1.length > 0) { 451 | sig_t1 = sig1.map(get_significance).reduce(sum); 452 | } else { 453 | sig_t1 = 0; 454 | } 455 | if (sig2.length > 0) { 456 | sig_t2 = sig2.map(get_significance).reduce(sum); 457 | calc_sig = sig_t1 / sig_t2; 458 | } else { 459 | sig_t2 = 0; 460 | calc_sig = 0; 461 | } 462 | 463 | if (prob2 > 0) { 464 | ///NOTE: Weigh significance a little more since it seems more reliable. 465 | calc_sig = (calc_sig * 2 + (prob1 / prob2)) / 3; 466 | } 467 | 468 | return calc_sig > 1; 469 | } 470 | 471 | function keep_best_branch(branches_lined_up, branches, disputed_group) 472 | { 473 | var best_branch = 0, 474 | best_words_in_dict = 0, 475 | best_prob = 0, 476 | best_sig = [], 477 | i, 478 | j, 479 | clear_branch; 480 | 481 | branches_lined_up.forEach(function (lined_up_branch, branch_num) 482 | { 483 | var this_words_in_dict = 0, 484 | this_prob_tmp = 0, 485 | this_prob, 486 | this_sig = []; 487 | 488 | lined_up_branch.forEach(function(word) 489 | { 490 | if (word.in_dict) { 491 | ///NOTE: If some parts of the word are not actually in the dictionary, weight them a little less. 492 | this_words_in_dict += word.str.length - (word.not_in_dict ? word.not_in_dict / 1.5 : 0); 493 | this_sig[this_sig.length] = word.dict_pos; 494 | } 495 | this_prob_tmp += word.prob.ave_prob; 496 | }); 497 | 498 | this_prob = this_prob_tmp / lined_up_branch.length; 499 | 500 | ///NOTE: Since the arrays are mutated, just send a copy. 501 | if (branch_num === 0 || this_words_in_dict > best_words_in_dict || (this_words_in_dict - 1 >= best_words_in_dict && is_more_likely(copy(this_sig), copy(best_sig), this_prob, best_prob))) { 502 | best_branch = branch_num; 503 | best_words_in_dict = this_words_in_dict; 504 | best_prob = this_prob; 505 | best_sig = this_sig; 506 | } 507 | }); 508 | 509 | /// Now, remove the others. 510 | for (i = 0; i < branches.length; i += 1) { 511 | for (j = branches[i].length - 1; j >= 0; j -= 1) { 512 | clear_branch = false; 513 | /// If it reached a later branch, we are done. 514 | if (branches[i][j].disputed_group > disputed_group) { 515 | return; 516 | } 517 | /// Skip if it is not the right branch. 518 | ///NOTE: If it finds a resolved branch after already finding the disputed branch, it could return there too. 519 | if (branches[i][j].resolved || branches[i][j].disputed_group < disputed_group) { 520 | break; 521 | } 522 | 523 | if (branches[i][j].branch[best_branch]) { 524 | branches[i] = [branches[i][j]]; 525 | /// Break since there's only one per column. 526 | break; 527 | } else { 528 | clear_branch = true; 529 | } 530 | } 531 | if (clear_branch) { 532 | ///NOTE: Deleting just sets the element to undefined, so it's fastest just to replace it all. 533 | branches[i] = []; 534 | } 535 | } 536 | } 537 | 538 | function resolve_multiple_branches(branches) 539 | { 540 | /// First, add known good branches (single branches) to a resolved variable 541 | /// while adding unresolved branches to an array of unresolved branches. 542 | /// Then, recursively loop through all of the branches in the array and total up 543 | /// how many known characters were found and the overall probability. 544 | var branches_lined_up = [], 545 | disputed_count, 546 | i; 547 | 548 | disputed_count = mark_resolved_branches(create_char_arr(branches)); 549 | 550 | for (i = 0; i <= disputed_count; i += 1) { 551 | branches_lined_up[i] = []; 552 | walk_branches(branches_lined_up[i], branches, i, {num: 0}, 0); 553 | } 554 | for (i = 0; i <= disputed_count; i += 1) { 555 | keep_best_branch(branches_lined_up[i], branches, i); 556 | } 557 | } 558 | 559 | function format_nicely(branches, str) 560 | { 561 | var res = [], 562 | x = 0; 563 | 564 | for (;;) { 565 | if (x >= str.length) { 566 | break; 567 | } 568 | 569 | /// If there are no words, use a single character. 570 | if (!branches[x] || branches[x].length === 0) { 571 | res[res.length] = str.substr(x, 1); 572 | x += 1; 573 | } else { 574 | /// Use a word if present. 575 | res[res.length] = branches[x][0].str; 576 | x += branches[x][0].str.length; 577 | } 578 | } 579 | 580 | return res; 581 | } 582 | 583 | /** 584 | * Segment a string of Chinese text. 585 | * 586 | * @param lang_analysis (object) The analyzed language data 587 | * @param lang_dict (string) A dictionary list of words 588 | * @param lang_p (object) The dictionary plot data 589 | * @param orig_str (string) The string to segment 590 | * @param return_raw (boolean) Whether or not to return the raw array of segmented strings (default) or a string with a space separating each segmentation 591 | * @note When compling the CKJV in the Forge, it needs the raw data. 592 | */ 593 | function segment(lang_analysis, lang_dict, lang_p, orig_str, return_raw) 594 | { 595 | var chunks = isolate_chinese_characters(orig_str), 596 | res = [], 597 | text; 598 | 599 | /// Set the closure variables for this language. 600 | analysis = lang_analysis; 601 | dict = lang_dict; 602 | p = lang_p; 603 | 604 | chunks.cn.forEach(function (str, i) 605 | { 606 | var branches; 607 | 608 | if (str !== "") { 609 | /// Step 1 610 | branches = find_known_and_likely_words(str, get_probabilities(str)); 611 | 612 | /// Step 2 613 | resolve_multiple_branches(branches); 614 | 615 | /// Step 3 616 | res[res.length] = format_nicely(branches, str); 617 | } 618 | 619 | /// Fill in with any non-Chinese parts. 620 | if (chunks.non.length > i) { 621 | res[res.length] = chunks.non[i]; 622 | } 623 | }); 624 | 625 | /// Should it convert the data into a string separated by spaces between segments? 626 | if (!return_raw) { 627 | text = ""; 628 | res.forEach(function (sec, i) 629 | { 630 | if (typeof sec === "string") { 631 | /// Adding a space, even before non-Chinese parts, helps to keep segments separate. 632 | /// But don't add a space before a space. 633 | if (i > 0 && sec !== " ") { 634 | text += " "; 635 | } 636 | text += sec; 637 | } else { 638 | sec.forEach(function (word, i) 639 | { 640 | /// Add a space to separate words. 641 | if (i > 0) { 642 | text += " "; 643 | } 644 | text += word; 645 | }); 646 | } 647 | }); 648 | return text; 649 | } 650 | 651 | return res; 652 | }; 653 | 654 | /// Is this Node.js? 655 | if (typeof exports !== "undefined") { 656 | exports.segment = segment; 657 | } else { 658 | /// If this is a browser, return a function that allows segment() to be attached to an object. 659 | return function init(context) 660 | { 661 | context.segment = segment; 662 | }; 663 | } 664 | }()); 665 | -------------------------------------------------------------------------------- /client/styles/base.css: -------------------------------------------------------------------------------- 1 | /** 2 | * BibleForge 3 | * 4 | * @date 10-30-08 5 | * @version alpha (α) 6 | * @link http://BibleForge.com 7 | * @license The MIT License (MIT) 8 | */ 9 | 10 | /*! 11 | * The BibleForge motto: 12 | * 13 | * "all things whatsoever ye would that men should do to you, do ye even so to them." 14 | * —Jesus (Matthew 7:12) 15 | */ 16 | 17 | /*! 18 | * Copyright (C) 2022 19 | * 20 | * Permission is hereby granted, free of charge, to any person obtaining 21 | * a copy of this software and associated documentation files (the 22 | * “Software”), to deal in the Software without restriction, including 23 | * without limitation the rights to use, copy, modify, merge, publish, 24 | * distribute, sublicense, and/or sell copies of the Software, and to 25 | * permit persons to whom the Software is furnished to do so. 26 | * 27 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 28 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 30 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 31 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 32 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 33 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | */ 35 | 36 | /*TODO: Completely document the CSS. */ 37 | /*TODO: Re-order (as well as maybe group) the styles alphabetically or in a logical order. */ 38 | 39 | /* Normalize Mozilla button styles. */ 40 | /* Also, remove the ugly dashed border on the text of buttons in Mozilla when clicked. */ 41 | button::-moz-focus-inner, input::-moz-focus-inner { 42 | border: 0; 43 | padding: 0; 44 | } 45 | 46 | input, textarea { 47 | /* This removes the awful, non-rounded selection rectangle that WebKit imposes. */ 48 | outline: 0; 49 | } 50 | 51 | 52 | html, body { 53 | margin: 0; 54 | padding: 0; 55 | border: 0; 56 | width: 100%; 57 | /* System/user settings are not be the same for every user, so the background and text color must be explicitly set. */ 58 | background: #FFF; 59 | color: #000; 60 | } 61 | 62 | html { 63 | overflow-y: scroll; 64 | overflow-x: hidden; 65 | font-family: Verdana, 'Bitstream Vera Sans', 'Bitstream Vera Sans Webfont', 'DejaVu Sans', sans-serif; 66 | font-size: 15px; 67 | 68 | /* Improve the text rendering and enable support for ligatures and kerning in Mozilla. */ 69 | /*NOTE: This may have a slight speed cost. */ 70 | /*NOTE: This does not effect Mozilla on Macs. */ 71 | text-rendering: optimizeLegibility; 72 | } 73 | 74 | input, .callout, .lang { 75 | /*NOTE: Input fields do not inherit from the HTML element. */ 76 | /* If a font-family is not specified, it will use a system default. */ 77 | /* On Windows, that font is "MS Shell Dlg," which is actually "Microsoft Sans Serif." */ 78 | /*TODO: Make/find a good replacement font for Arial. */ 79 | font-family: Arial, 'DejaVu Sans', sans-serif; 80 | } 81 | 82 | body { 83 | overflow: hidden; 84 | } 85 | 86 | 87 | .topBars { 88 | position: fixed; 89 | top: 0; 90 | left: 0; 91 | width: 100%; 92 | height: 30px; 93 | /* This needs to be above most floating elements (excluding the context menu). */ 94 | z-index: 999999; 95 | } 96 | 97 | .searchBars { 98 | /* A solid background for IE/Mozilla 2-/Opera 9.6- */ 99 | background: #FFF; 100 | /* Transparent white background for Firefox 3.5-/Opera 10 */ 101 | background: rgba(255,255,255,.9); 102 | /* Chrome/Safari 5.1+ */ 103 | background: -webkit-linear-gradient(rgba(255,255,255,.98), rgba(255,255,255,.93) 25%, rgba(255,255,255,.87)); 104 | /* IE 10, Firefox 16+, Opera 12.10+ */ 105 | background: linear-gradient(rgba(255,255,255,.98), rgba(255,255,255,.93) 25%, rgba(255,255,255,.87)); 106 | width: 100%; 107 | height: 100%; 108 | text-align: center; 109 | } 110 | 111 | .infoBars { 112 | /* A solid background for IE/Mozilla (Firefox 2-)/Opera 9.6- */ 113 | background: #FFD; 114 | /* Transparent yellow background */ 115 | background: rgba(255,255,221,.87); 116 | border-top: #EEE999 solid 1px; 117 | border-bottom: #EEE999 solid 1px; 118 | 119 | padding-left: 3px; 120 | 121 | width: 100%; 122 | height: 18px; 123 | 124 | /* Prevent a long search string from overflowing the info bar by cutting it short and adding an ellipsis. (Mozilla has no support for this yet.) */ 125 | /*FIXME: This does not work yet. */ 126 | text-overflow: ellipsis; 127 | /* Opera specific */ 128 | -o-text-overflow: ellipsis; 129 | /* This is what actually hides the text, currently. */ 130 | overflow: hidden; 131 | 132 | /* Prevent excess whitespace from collapsing when displaying user generated search terms. */ 133 | white-space: pre-wrap; 134 | 135 | font-size: 13px; 136 | 137 | box-shadow: 0 3px 6px rgba(0,0,0,.08); 138 | } 139 | 140 | .infoLeft { 141 | /* Make the
not take up the entire width of the infoBar. */ 142 | float: left; 143 | 144 | /* Add a slight radial blur to make sure the text is always readable.*/ 145 | /* Chrome/Safari 5.1+ */ 146 | background: -webkit-radial-gradient(ellipse farthest-corner, #FFFFE2 70%, rgba(255,255,226,0) 100%); 147 | /* IE 10, Firefox 16+, Opera 12.10+ */ 148 | background: radial-gradient(ellipse farthest-corner, #FFFFE2 70%, rgba(255,255,226,0) 100%); 149 | } 150 | 151 | .queryInput { 152 | background: rgba(255,255,255,.5); 153 | 154 | /* Padding is used to push the text away from the rounded corners. */ 155 | padding: 3px 25px 1px 7px; 156 | 157 | font-size: 15px; 158 | 159 | max-width: 480px; 160 | min-width: 30px; 161 | /*NOTE: 100% width cannot be used because it extends off the page */ 162 | /* 70% is used because it is a good balance when the page is wide or narrow. */ 163 | width: 70%; 164 | height: 18px; 165 | 166 | /* Dim the text when not in focus (or hovering) so that it does not distract from the text. */ 167 | color: #A7A7A7; 168 | 169 | border-style: solid; 170 | border-width: 1px; 171 | /* This gives the input box a slight 3D effect. */ 172 | border-color: #BBB #CACACA #C3C3C3 #A1A19B; 173 | border-radius: 15px; 174 | 175 | /* Add slight gradient shadow to the top of the input box. */ 176 | box-shadow: inset 0 2px 3px rgba(0,0,0,.16); 177 | 178 | /* The margin balances moves the input box over a little so as not to cover the wrench icon. */ 179 | margin: 2px 30px; 180 | 181 | /* Apply transitions to all changes (in particular, background-color, border-color, and color). */ 182 | /* Transition for Mozilla (Firefox 4.0+) */ 183 | -moz-transition: all .3s; 184 | /* Transition for WebKit */ 185 | -webkit-transition: all .3s; 186 | /* Transition for Opera 10.5+ */ 187 | transition: all .3s; 188 | } 189 | 190 | /** 191 | * Highlight the input box partially on hover. 192 | */ 193 | .queryInput:hover { 194 | border-color: #96968E; 195 | } 196 | 197 | /** 198 | * Highlight the input box. 199 | */ 200 | .queryInput:focus { 201 | border-color: #96968E; 202 | 203 | /* Darken the text so that it is more prominent. */ 204 | color: #555; 205 | background-color: #FFF; 206 | } 207 | 208 | /** 209 | * Keep the placeholder text grayed out. 210 | */ 211 | /* NOTE: WebKit does not need this because the placeholder text does not change colors on focus. */ 212 | /* However, in Night Mode, WebKit's font color is too bright, so it is lightened in night.css. 213 | /* NOTE: WebKit's placeholder property is "::-webkit-input-placeholder" (note the double colons as well as different text). */ 214 | /* NOTE: IE 10 might use ":-ms-input-placeholder". */ 215 | /* NOTE: Different browsers use different styles by default to style the placeholder text. Some set the opacity; some set a semi-opaque color. */ 216 | .queryInput:-moz-placeholder { 217 | color: #A7A7A7; 218 | } 219 | 220 | 221 | .queryPadding { 222 | padding: 8px; 223 | } 224 | 225 | .lang { 226 | background: rgba(0,0,200,.05); 227 | color: rgba(0,0,0,.2); 228 | font-size: 13px; 229 | 230 | cursor: pointer; 231 | 232 | /* Round just the left side to match queryInput. */ 233 | border-radius: 15px 0 0 15px; 234 | 235 | /* Just have a single border on the right to separate it from queryInput. */ 236 | border: none transparent; 237 | border-right: 1px solid rgba(0,0,0,.1); 238 | 239 | /* Align the element inside of queryInput. */ 240 | position: absolute; 241 | margin: 0 0 0 31px; 242 | top: 3px; 243 | height: 22px; 244 | 245 | /* The extra right padding is for the drop down arrow (see .lang:after). */ 246 | padding-right: 14px; 247 | /* Center the text vertically. */ 248 | padding-top: 2px; 249 | 250 | font-size: 13px; 251 | 252 | /* Hide by default so that the default language name can be added first. */ 253 | visibility: hidden; 254 | 255 | /* Apply transitions to all changes. */ 256 | /* Transition for Mozilla (Firefox 4.0+) */ 257 | -moz-transition: all .3s; 258 | /* Transition for WebKit */ 259 | -webkit-transition: all .3s; 260 | /* Transition for Opera 10.5+ */ 261 | transition: all .3s; 262 | } 263 | 264 | .lang:hover, .more-button:hover { 265 | color: rgba(0,0,0,.6); 266 | } 267 | 268 | /*BUG Opera does not properly render the arrow. It flickers when the mouse hovers over it. */ 269 | .lang:after { 270 | opacity: .15; 271 | 272 | width: 11px; 273 | height: 6px; 274 | 275 | /* This is necessary (for some reason) to make the element visible. */ 276 | content: ""; 277 | 278 | position: absolute; 279 | top: 45%; 280 | right: 1px; 281 | 282 | -moz-transition: opacity .3s; 283 | /*BUG This does not work at least in Chrome 22. */ 284 | -webkit-transition: opacity .3s; 285 | transition: opacity .3s; 286 | } 287 | 288 | .lang:hover:after, .lang:active:after, .activeLang:after { 289 | opacity: .4; 290 | } 291 | 292 | .lang:active, .activeLang, .more-button:active { 293 | background: rgba(0,0,255,.12); 294 | color: rgba(0,0,0,.6); 295 | } 296 | 297 | .more-button:hover { 298 | background: rgba(0,0,200,.08); 299 | } 300 | 301 | 302 | .crown_loader { 303 | width: 20px; 304 | height: 20px; 305 | /* The Crown of Thorns icon */ 306 | background: no-repeat url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAQAAAAngNWGAAABcUlEQVQoFQXB72uMARwA8M+ee1ArOsW80FxKNsSQ215IGGuje2E6L5Q/QuydvPFqr3aSvJkiSl6cInWXVqg1P+buhXdWU+KNZlzHSZlnX58PAABscx8AAACA1BsrwoLNAEACOKPHuDENbUV95nwypqwPAKAu80tLuG0Nrsp0fBNemABgqxWXUbFgRggNdTWUhL0AjPuNjcIJPNGNvDCAuisAjPqhIjxHai047YFwR9NFgNR74YMRZwEAO7wSlvWQYtZX7xTMSAFAl0VN68xr6GdSZr+ckAAAeOinYYSD7LJqJ0INAECXJYMIj3NazivYZ70R3f4CAA44qVfREGzSkanZAAA4LjEthEESLCv7I7Rdd0oOJDgis8dHl8wDnLOEAaEf3NCLcAGPXANgu1Ul3PXaTV+Ee6bNYbfMMABl34VnmkLTMUOeCouq/mkDKaiqOmRUR8FbLzGr5Ja8KYcBAIAtPguhpQgAAACQU3FUHgDgP8wYe6XaJYMMAAAAAElFTkSuQmCC); 307 | opacity: .2; 308 | 309 | /* Prepare to make the graphic spin. */ 310 | /*NOTE: Mozilla has some issues that are fixed by the Mozilla specific code at the bottom. */ 311 | -webkit-animation-name: rotate; 312 | -moz-animation-name: rotate; 313 | animation-name: rotate; 314 | -webkit-animation-duration: 2s; 315 | -moz-animation-duration: 2s; 316 | animation-duration: 2s; 317 | -webkit-animation-iteration-count: infinite; 318 | -moz-animation-iteration-count: infinite; 319 | animation-iteration-count: infinite; 320 | -webkit-animation-timing-function: linear; 321 | -moz-animation-timing-function: linear; 322 | animation-timing-function: linear; 323 | } 324 | 325 | /* Make the Crown of Thorns spin. */ 326 | @-webkit-keyframes rotate { 327 | from {-webkit-transform: rotate(0deg);} 328 | to {-webkit-transform: rotate(360deg);} 329 | } 330 | @-moz-keyframes rotate { 331 | from {-moz-transform: rotate(0deg);} 332 | to {-moz-transform: rotate(360deg);} 333 | } 334 | @keyframes rotate { 335 | from {transform: rotate(0deg);} 336 | to {transform: rotate(360deg);} 337 | } 338 | 339 | 340 | .inputIcon { 341 | /* Move the icon over the right end of the input box. */ 342 | margin-left: -55px; 343 | /* Aligns the image in a somewhat cross-browser way. */ 344 | vertical-align: bottom; 345 | 346 | /* Remove the hideous selection rectangle when clicked. */ 347 | outline: 0; 348 | 349 | /* Fade out slightly when the mouse cursor moves off. */ 350 | /* Transition for Mozilla (Firefox 4.0+) */ 351 | -moz-transition: opacity .3s ease; 352 | /* Transition for WebKit */ 353 | -webkit-transition: opacity .3s ease; 354 | /* Transition for Opera 10.5+ */ 355 | transition: opacity .3s ease; 356 | opacity: .3; 357 | 358 | position: absolute; 359 | 360 | /* Buttons do not automatically get a hand cursor. */ 361 | cursor: pointer; 362 | 363 | width: 25px; 364 | height: 26px; 365 | border: 0; 366 | background: no-repeat left 60% url(data:image/gif;base64,R0lGODlhEAAQAPIHAAIFARweHDU3NFlbWHJ0cZ2fnNDTz/3//CH5BAAKAAAALAAAAAAQABAAAANDeLpnAyAQRgsYxhQBDD2WxxDABxTfIUxMmRYBY7jfTNGU3YrUMPQ4xYwnhPAsKNmJBIEkF05ZqkmcGlOMwBW74FSxCQA7); 367 | } 368 | 369 | /** 370 | * Fade in icon on hover. 371 | */ 372 | .inputIcon:hover, .wrenchIcon:hover, .wrenchPadding:hover .wrenchIcon, .activeWrenchIcon { 373 | /* !important is used to enforce the opacity for .activeWrenchIcon when the context menu is open. */ 374 | opacity: .6!important; 375 | } 376 | 377 | /** 378 | * Emulate button pressing. 379 | * 380 | * @note This does not work in IE. 381 | */ 382 | .inputIcon:active { 383 | /* This is a simple way of moving the icon over slightly when clicked. */ 384 | border-left: 1px solid transparent; 385 | /* Keep the icon highlighted even in the mouse moves off. */ 386 | opacity: .6; 387 | } 388 | 389 | 390 | .wrenchPadding { 391 | position: absolute; 392 | /* Add padding around wrench with extra padding below and on the right. */ 393 | padding: 1px 6px 8px 1px; 394 | 395 | /* Use the hand cursor for the padded area, just like the button. */ 396 | cursor: pointer; 397 | 398 | /*TODO: Use fade class. */ 399 | /* Transition for Mozilla (Firefox 4.0+) */ 400 | -moz-transition: opacity 1s ease-in; 401 | /* Transition for WebKit */ 402 | -webkit-transition: opacity 1s ease-in; 403 | /* Transition for Opera 10.5+ */ 404 | transition: opacity 1s ease-in; 405 | } 406 | 407 | 408 | .wrenchIcon { 409 | /* Darken the text so that it is more prominent. */ 410 | /* Transition for Mozilla (Firefox 4.0+) */ 411 | -moz-transition: opacity .6s ease; 412 | /* Transition for WebKit */ 413 | -webkit-transition: opacity .6s ease; 414 | /* Transition for Opera 10.5+ */ 415 | transition: opacity .6s ease; 416 | 417 | /* Fade out slightly when the mouse cursor moves off. */ 418 | opacity: .2; 419 | 420 | padding: 4px; 421 | 422 | /* Remove the hideous selection rectangle when clicked. */ 423 | outline: 0; 424 | 425 | /* Buttons do not automatically get a hand cursor. */ 426 | cursor: pointer; 427 | 428 | border: 0; 429 | width: 24px; 430 | height: 24px; 431 | background: center no-repeat url(data:image/gif;base64,R0lGODdhEAAQAMIIAAEDADAyL05OSWlpYYyLg7GwqNjVyP/97iwAAAAAEAAQAAADQ3i6OwBhsGnCe2Qy+4LRS3EBn5JNxCgchgBo6ThwFDc+61LdY6m4vEeBAbwMBBHfoYgBLW8njUPmPNwk1SkAW31yqwkAOw==); 432 | } 433 | 434 | 435 | .contextMenu { 436 | opacity: 0; 437 | background: #FFF; 438 | background: rgba(255,255,255,.95); 439 | border: #A3A3A3 1px solid; 440 | padding: 3px; 441 | /* This element must be on top (because it is created later, it is also above the topBar too). */ 442 | z-index: 999999; 443 | 444 | /* Transition for Mozilla (Firefox 4.0+) */ 445 | -moz-transition: opacity .2s ease-in; 446 | /* Transition for WebKit */ 447 | -webkit-transition: opacity .2s ease-in; 448 | /* Transition for Opera 10.5+ */ 449 | transition: opacity .2s ease-in; 450 | 451 | /* Rounded corners for Opera 10.5+, IE 9+, WebKit 532.5+ (Chrome 4, Safari 5), Gecko 2 (Mozilla 4.0) */ 452 | border-radius: 3px; 453 | 454 | /* This shadow is slightly dimmer than the panel because it is supposed to look higher up. */ 455 | /*TODO: The direction of the shadow could be set dynamically based on where it is on the page. */ 456 | box-shadow: 2px 2px 3px rgba(0,0,0,.2); 457 | } 458 | 459 | .contextMenu div { 460 | /* Ensure that the cursor will look like a hand even if not hovering over the text. */ 461 | /*NOTE: Since Chromium fires the onmousemove event when the mouse cursor changes, we must set the cursor for the entire
, not just on hover */ 462 | /* because if the user uses the keyboard arrows while the mouse is over an element, the onmousemove event will keep firing. */ 463 | cursor: pointer; 464 | } 465 | 466 | /* Menu items */ 467 | .contextMenu div a { 468 | color: #555; 469 | /* Block table-row-group is needed to make the possible table cells to display correctly (e.g., the pronunciation selection menu). */ 470 | /* Any display type that is block (as opposed to inline) is needed to make the width become 100%. */ 471 | /* NOTE: table-row could also works except for the fact that in Chrome, the element is treated as inline (cannot click on the space to the right) even though the background extends all the way. */ 472 | display: table-row-group; 473 | text-decoration: none; 474 | /* Prevent the text from wrapping. */ 475 | white-space: nowrap; 476 | width: 100%; 477 | 478 | /* Make the background and font color fade when the mouse hovers/leaves. */ 479 | -moz-transition: background .2s, color .2s; 480 | -webkit-transition: background .2s, color .2s; 481 | transition: background .2s, color .2s; 482 | } 483 | 484 | /* The container for a (text) menu item. */ 485 | .contextMenu div a div { 486 | padding: 1px 0; 487 | } 488 | 489 | /* CSS table cell */ 490 | /* The pronunciation context menu uses this. */ 491 | .cell { 492 | display: table-cell; 493 | /* Add some space between the columns. */ 494 | padding-right: 8px; 495 | } 496 | 497 | /* The last column. */ 498 | .cell:last-of-type { 499 | /* Prevent the last column from displaying space on the right of it. */ 500 | padding-right: 0; 501 | } 502 | 503 | .menu_item_selected { 504 | /*TODO: Determine if it is better to use the color that is similar to the yellow before the transparency is applied (#DDF) or use transparency. */ 505 | background: #E1E1FF; 506 | color: #3985A8; 507 | } 508 | 509 | .menu_item_line div { 510 | border-top: 1px solid #A3A3A3; 511 | } 512 | 513 | 514 | .panel { 515 | display: none; 516 | position: fixed; 517 | /* A solid background for IE/Mozilla 2-/Opera 9.6- */ 518 | background: #FFF; 519 | /* Transparent white background for Mozilla/Opera 10/WebKit */ 520 | background: rgba(255,255,255,.97); 521 | /* Chrome/Safari 5.1+ */ 522 | background: -webkit-linear-gradient(bottom, rgba(240,240,240,.98), rgba(255,255,255,.98) 60px); 523 | /* IE 10, Firefox 16+, Opera 12.10+ */ 524 | background: linear-gradient(to top, rgba(240,240,240,.98), rgba(255,255,255,.98) 60px); 525 | border: #A3A3A3 1px solid; 526 | /* Add enough padding to the top to push the contents past the top bar. */ 527 | /* Add extra padding on the sides so that the optional scroll bars will not cover up any of the content. */ 528 | padding: 60px 16px 16px 16px; 529 | /* Add scroll bars when the contents extend past the height or width. */ 530 | overflow: auto; 531 | 532 | /* Rounded corners for Opera 10.5+, IE 9+, WebKit 532.5+ (Chrome 4, Safari 5), Gecko 2 (Mozilla 4.0) */ 533 | border-radius: 9px; 534 | 535 | /* Add both a drop shadow and an inset shadow to give a slightly rounded effect. */ 536 | box-shadow: 3px 3px 6px rgba(0,0,0,.2), inset -1px 0 2px rgba(0,0,0,.2); 537 | 538 | /* Just below the topBar (and context menu) but above callouts. */ 539 | z-index: 999990; 540 | 541 | /* Make sure that the panel does not get too large. */ 542 | max-width: 90%; 543 | } 544 | 545 | 546 | .panel a { 547 | color: #3985A8; 548 | 549 | text-decoration: none; 550 | } 551 | 552 | .panel a:hover { 553 | /*TODO: Do something better. */ 554 | text-decoration: underline; 555 | } 556 | 557 | 558 | .slide { 559 | /* Transition for Mozilla (Firefox 4.0+) */ 560 | -moz-transition: top .6s ease; 561 | /* Transition for WebKit */ 562 | -webkit-transition: top .6s ease; 563 | /* Transition for Opera 10.5+ */ 564 | transition: top .6s ease; 565 | } 566 | 567 | 568 | fieldset { 569 | border: 0; 570 | } 571 | 572 | legend { 573 | font-size: 20px; 574 | margin-left: -10px; 575 | } 576 | 577 | /* Make sure that the panel does not get too big. */ 578 | /*NOTE: It needs to be set here because the panel must have "max-width: 100%" to prevent the panel as a whole from getting too big.*/ 579 | .aboutBible { 580 | max-width: 700px; 581 | } 582 | 583 | /* Align the first button to the left. */ 584 | .button1of2 { 585 | float: left; 586 | } 587 | 588 | /* The done button and the second of two should be right aligned. */ 589 | .doneButton, .button2of2 { 590 | float: right; 591 | } 592 | 593 | .button { 594 | border: 1px solid #8EC1DA; 595 | 596 | border-radius: 4px; 597 | 598 | background-color: rgba(0,0,200,.05); 599 | 600 | box-shadow: inset 0 1px 3px rgba(255,255,255,.5), inset 0 -15px 3px rgba(0,0,200,.05); 601 | 602 | color: #3985A8; 603 | 604 | text-shadow: 0 1px #FFF; 605 | 606 | padding: 5px 14px; 607 | outline: 0; 608 | 609 | cursor: pointer; 610 | 611 | /* Fade the background color on mouse down. */ 612 | /* Transition for Mozilla (Firefox 4.0+) */ 613 | -moz-transition: background-color .2s; 614 | /* Transition for WebKit */ 615 | -webkit-transition: background-color .2s; 616 | /* Transition for Opera 10.5+ */ 617 | transition: background-color .2s; 618 | } 619 | 620 | .button:hover { 621 | background-color: rgba(0,0,200,.07); 622 | } 623 | 624 | /** 625 | * Emulate button pressing. 626 | * 627 | * @note This does not work in IE. 628 | */ 629 | .button:active { 630 | /*TODO: Determine if it would be better just to change the text shadow color. (Opera already moves the text.) */ 631 | padding: 6px 13px 4px 15px; 632 | 633 | background-color: rgba(0,0,200,.1); 634 | } 635 | 636 | .emailForm input, .emailForm textarea { 637 | display: block; 638 | background: #FFF; 639 | color: #000; 640 | border: 1px solid #CCC; 641 | border-radius: 3px; 642 | margin: 5px; 643 | width: 230px; 644 | max-width: 100%; 645 | font-size: 14px; 646 | padding: 2px; 647 | } 648 | 649 | .emailForm textarea { 650 | /* Override the width to make it larger than the input boxes. */ 651 | width: 500px; 652 | height: 120px; 653 | } 654 | 655 | .emailForm div { 656 | /* Line up the email address link with the input boxes. */ 657 | padding-left: 4px; 658 | } 659 | 660 | 661 | 662 | .scrolls { 663 | overflow: hidden; 664 | max-width: 800px; 665 | min-width: 200px; 666 | margin-left: auto; 667 | margin-right: auto; 668 | /* Fixes the clipping of italics p's at the beginning of a line (e.g., Acts 5:3 with a large window) or f's at the end of a line (Ex. 36:8). */ 669 | padding: 0 4px 0 4px; 670 | } 671 | 672 | /* Display a hand cursor for words that can be looked up. */ 673 | .linked a { 674 | cursor: pointer; 675 | } 676 | 677 | .scrolls { 678 | cursor: auto; 679 | } 680 | 681 | /* Hide the cursor when hovering between words on the page (.hidden_cursor), hovering over words on the page (.hidden_cursor a), and hovering over a search verse (.hidden_cursor .search_verse span). */ 682 | /* These all must be specified specifically because they may have a hand cursor style that needs to be directly overwitten. */ 683 | .hidden_cursor, .hidden_cursor a, .hidden_cursor .search_verse span { 684 | cursor: none!important; 685 | } 686 | 687 | 688 | .loaders { 689 | height: 50px; 690 | width: 100%; 691 | background: no-repeat center url(data:image/gif;base64,R0lGODlhIAAgAPMLAMbGxoSEhLa2tpqamjY2NlZWVtjY2OTk5Ly8vB4eHgQEBP///wAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQECgD/ACwAAAAAIAAgAAAE53DJSelQo+rNZ1JJZRydJgSVolKAIJTUkSQFpSrT4SIwNScvyW2CcBl6k8CMMBkuDDskhTBDLZwuAUkqEfxIQ6gAQBFvFwICITMpVDW6XNE4GagJhSAgwe60smQUBXd4Rz1ZAghnFAKDd0hihh12BEE9kjAHVlycXIg7BwADAaSlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YHvpJivxNaGmLHT0VnOgKYf0dZXS7APdpB309RnHOG5gvqXGLDaC457D1zZ/V/nmOM82XiHQ7YKhKP1oZmADdEAAAh+QQFCgALACwAAAAAGAAXAAAEcnDJSWsSNetJEqnBsIlUYlKEomjEV57SoCZsi0wmLSVqoA2tAg4WmG0WhRYptzCoFKRNy8UsqFzNQOCGwlJkgAlCqzVIDATMkSIghw7rjcHti2/GgbD9qN774wcIAoOEfwuChIV/gYmDho+QkZKTR3p7EQAh+QQFCgALACwBAAAAHQAOAAAEcnDJSacgNeu9CimZwE0GUhEoVSTJKAWBOKGYJLD1CAfGnEoElkuC2PlyuKFkADMtaIsDKyGbHDYG4zMVYIEmAYVicBAgehNmTNNaJsQKnmCOuEYDgBGAAFfUAHNzeUp9VBQHCIFOLmFxWHNoQwWRWEocEQAh+QQFCgALACwHAAAAGQARAAAEaXDJuUAANOs9wsjfthxGFpwZQYiCgE1nQKni0goHjEqFGmqGFkInWwxUhdoC0SotYhLVSnm4SaALWiaREFAATY2A4BxzE2JnrXBOJJWb9pTihRu5dnggl+/7NQqBggk/fYKHCn8LiAqEEQAh+QQFCgALACwOAAAAEgAYAAAEZtAMs6q9WAy8EOXLIF5DEIDhWBnmCYpb1SIoXCEtmsbt944CU6wyIBBQgMDBUjAShiBD06mzOAkFWrVihG6/4G9iTD5WyejEOU0QhMMB3zegULi+9XrCCwIQ8gpmWwMJeXdbdApuEQAh+QQFCgALACwOAAAAEgAeAAAEgPCgs6q9GAmEAb5CCA7DV4XCRaYmagmk14oLQJbm4i53foq2AauCCAQMJsPQYDRyfIdBM4DzTY8+C8CZxQy74CxhTC58P+Q0QawuhN8WynuQSMDrdcI5WcAn3CYBCjICBHgmBQoKaxeGJgeKClVdggp2bwmKAW8CkXAEinJhVCYRACH5BAUKAAsALA8AAQARAB8AAAR8cMm5zKEYAyGyPxziZQhnjJQRohQnXGzFASkHU/dylCa7uTSUS4DIeVSCU0yiXDo9gah0EIRKr6hrlPrsOgUEwsAZDheeZcJokKAUymNKIJE4TwZhiWIvoSc6HnsKE3RqgXwSBHQjghR+h4MTBYsZjRiAGAkKbU4DCnFLEQAh+QQFCgALACwIAA4AGAASAAAEbHDJSesSOKNj+8wg4nkgto1oigoqCgSB2FpwbczUMdTBMAuE28LAky0AikCHQKggYMIFQaEoLBJYCbM5GlAVHGxCMmBaPQmq8pqVFJg+GnUsEVO2nbQizqZPmB1UXHVtE3wVOxUECYM4H34qEQAh+QQFCgALACwCABIAHQAOAAAEeHDJSatd5lJTtDWCkF2BogQehYQCclBCYpopBbACIBGzQugeQOC1OKxChpIpMZAYmBZBINCcGFaHgQk1KSQSKIJYMg2MLMRJ7LsbLxDl2oTAbhMmgylCvvje7VZxNXQJAnNuEnlcKV8dh38TCGcehhUFBI58cpA1EQAh+QQFCgALACwAAA8AGQARAAAEZ3AkReu6OOtbu9pgJnlfaJ7oeQQpmiRDCxLvK2dFnRSoIWw1wu8i3PgEgIzApiEQLoHoRUA9oJzPRZS1OCJOBWdMK70gqIbQwMmDlhcH6nCWdXMvAGrIqdlqDFZqGgMBYzcaAAFJGxEAIfkEBQoACwAsAQAIABEAGAAABF1QKBWWvfiGqdLI4EJwCgGE2JCQaLZRbWZUcW3feK7v6EAQNkTh96sRCQVDy/crXA6BE+j3uQwCAcFCwEXNsBauNoQNIMJdEKJ8EZOxSvTYlcW4QYa5BSE43w4IBxEAIfkEBQoACwAsAAACAA4AHQAABHJwyblGoHgqRTLeiuBNwZaMU7Jd6AAaaUcRW5EmCSEugMJKBRyuAPMICMITaoEbLBeH51JQIFivmatWRqFuudLwDoUIBAAjg3ntsawHUUzZPEBLBPGFOoCgAAQCRR4HgGMeCICCGQaAfWSAeUYCdigHihEAOw==); 692 | visibility: hidden; 693 | opacity: .5; 694 | } 695 | 696 | 697 | /* Make labels have a hand cursor. */ 698 | label { 699 | cursor: pointer; 700 | } 701 | 702 | .callout { 703 | border-radius: 9px; 704 | box-shadow: 3px 3px 6px rgba(0,0,0,.2), inset -1px 0 2px rgba(0,0,0,.2); 705 | 706 | background: #FFF; 707 | /* Transparent background for Firefox 3.5- */ 708 | background: rgba(255, 255, 255, .95); 709 | /* Chrome/Safari 5.1+ */ 710 | background: -webkit-linear-gradient(bottom, rgba(240,240,240,.97), rgba(255,255,255,.97) 60px); 711 | /* IE 10, Firefox 16+, Opera 12.10+ */ 712 | background: linear-gradient(to top, rgba(240,240,240,.97), rgba(255,255,255,.97) 60px); 713 | 714 | position: absolute; 715 | border: #ccc solid 1px; 716 | /* There is no padding on the left so that the lexicon title's diacritics have enough room to be displayed. */ 717 | padding: 4px 4px 4px 0; 718 | font-size: 16px; 719 | min-width: 46px; 720 | max-width: 95%; 721 | max-height: 90%; 722 | width: 300px; 723 | height: 125px; 724 | } 725 | 726 | .callout .inside { 727 | overflow-x: hidden; 728 | height: 100%; 729 | } 730 | .pointer-down { 731 | box-shadow: 6px 4px 6px -3px rgba(0,0,0,.2); 732 | transform: rotate(45deg); 733 | -moz-transform: rotate(45deg); 734 | -webkit-transform: rotate(45deg); 735 | position: absolute; 736 | width: 20px; 737 | height: 20px; 738 | border-bottom: #ccc solid 1px; 739 | border-right: #ccc solid 1px; 740 | 741 | /* Chrome/Safari 5.1+ */ 742 | background: -webkit-linear-gradient(left top, rgba(255,255,255,0) 41%, rgba(240,240,240,.97) 41%); 743 | /* IE 10, Firefox 16+, Opera 12.10+ */ 744 | background: linear-gradient(to right bottom, rgba(255,255,255,0) 41%, rgba(240,240,240,.97) 41%); 745 | bottom: -11px; 746 | } 747 | .pointer-up { 748 | box-shadow: -1px -3px 6px -3px rgba(0,0,0,.2); 749 | transform: rotate(45deg); 750 | -moz-transform: rotate(45deg); 751 | -webkit-transform: rotate(45deg); 752 | 753 | position: absolute; 754 | top: -11px; 755 | 756 | width: 20px; 757 | height: 20px; 758 | 759 | border-top: #ccc solid 1px; 760 | border-left: #ccc solid 1px; 761 | 762 | /* Chrome/Safari 5.1+ */ 763 | background: -webkit-linear-gradient(right bottom, rgba(255,255,255,0) 45%, rgba(255,255,255,.95) 45%); 764 | /* IE 10, Firefox 16+, Opera 12.10+ */ 765 | background: linear-gradient(to left top, rgba(255,255,255,0) 45%, rgba(255,255,255,.95) 45%); 766 | } 767 | 768 | /* Hide certain callout content when showing details (such as the "more" button). */ 769 | .detailed_callout .simple_only { 770 | display: none; 771 | } 772 | 773 | /* Hide detailed callout content by default. */ 774 | .detailed_only { 775 | display: none; 776 | } 777 | 778 | /* Display detailed callout content when showing details. */ 779 | .detailed_callout .detailed_only { 780 | display: block; 781 | } 782 | 783 | 784 | .lex-title { 785 | /* Push the text over a little so that diacritics are not cut off. */ 786 | margin-left: 7px; 787 | } 788 | 789 | .lex-orig_word { 790 | /* Palatino Linotype (or similar fonts) are used for Greek and BF_Hebrew for Hebrew. */ 791 | /* If both fonts are installed on the computer, the Hebrew letters will fall back to BF_Hebrew, */ 792 | /* but if they are not installed, this will not happen; therefore, a separate class is needed for the Hebrew text. */ 793 | font-family: 'Palatino Linotype', Palatino, TexGyrePagella, 'TeX Gyre Pagella', 'TeX Gyre Pagella Webfont', 'URWPalladioL', 'Book Antiqua', 'Times new Roman', Times, serif; 794 | font-size: 30px; 795 | /* The vertical alignment is required simply to match up with .lex-pronun. */ 796 | vertical-align: super; 797 | 798 | /* Transition the font size when expanding the callout for more details. */ 799 | /* Transition for Mozilla (Firefox 4.0+) */ 800 | -moz-transition: font-size 150ms ease 150ms; 801 | /* Transition for WebKit */ 802 | /* NOTE: In the WebKit specific CSS below, the line-height property is required to fix some issues and this property also must transition with the font size. */ 803 | -webkit-transition: font-size 150ms ease 150ms, line-height 150ms ease 150ms; 804 | /* Transition for Opera 10.5+ */ 805 | transition: font-size 150ms ease 150ms; 806 | } 807 | 808 | /* The first letter of Psalm 119 titles is a Hebrew letter. */ 809 | .hebrew, .psalm_119_heading:first-letter { 810 | font-family: "Ezra SIL SR", BF_Hebrew; 811 | } 812 | 813 | /* Zoom the font in when transitioning to show more details. */ 814 | .large_callout .lex-orig_word { 815 | font-size: 55px; 816 | } 817 | 818 | 819 | .lex-pronun { 820 | font-family: Verdana, 'Bitstream Vera Sans', 'Bitstream Vera Sans Webfont', 'DejaVu Sans', sans-serif; 821 | font-size: 14px; 822 | white-space: nowrap; 823 | /* Because this element can wrap if the word is long and the line height is rather large due to the large original word one the line above, */ 824 | /* it needs to be centered above the baseline to look good. */ 825 | vertical-align: super; 826 | /* This pushes down the context menu and drop down arrow slightly. */ 827 | padding-bottom: 3px; 828 | 829 | /* Transition the font size when expanding the callout for more details. */ 830 | /* Transition for Mozilla (Firefox 4.0+) */ 831 | -moz-transition: font-size 200ms ease 10ms; 832 | /* Transition for WebKit */ 833 | -webkit-transition: font-size 200ms ease 10ms; 834 | /* Transition for Opera 10.5+ */ 835 | transition: font-size 200ms ease 10ms; 836 | } 837 | 838 | /* Zoom the font in when transitioning to show more details. */ 839 | .large_callout .lex-pronun { 840 | font-size: 16px; 841 | } 842 | 843 | .expandable { 844 | margin-top: 7px; 845 | /* Without this, the before pseudo element does not stay with the parent element when scrolling in WebKit. */ 846 | /* In Mozilla, the pseudo element sometimes stays with the element, but there is a long delay before it is redrawn. */ 847 | position: relative; 848 | } 849 | 850 | .expandable_summary { 851 | font-weight: bold; 852 | /* Push the text over to make way for the arrow. */ 853 | padding-left: 10px; 854 | } 855 | 856 | .dropdown, .lang:after, .expandable_summary:before { 857 | /* The drop down arrow (triangle) */ 858 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAGCAQAAAC/O6xzAAAAXklEQVQI12Ng4GJIZXjP8B8JTmYQY2BgY9BmWMjwFS54lMEHqJSBCUgYMSyBShxnCGTgZ2BmYABJcAMlFjN8AaoMZBBgYGGAApAOPYYSBnegSrggRIINKMQB0c7AAAAMyxgJkjHDSAAAAABJRU5ErkJggg==); 859 | background-repeat: no-repeat; 860 | } 861 | 862 | .expandable_summary:before { 863 | /* This is necessary (for some reason) to make the element visible. */ 864 | content: ""; 865 | 866 | /* This is needed to be able to change the width and height. */ 867 | position: absolute; 868 | 869 | width: 11px; 870 | height: 6px; 871 | 872 | /* Reposition the arrow to align with the left. */ 873 | margin-top: 8px; 874 | margin-left: -12px; 875 | 876 | /* The default position for the arrow is rotated to the right. */ 877 | transform: rotate(-90deg); 878 | -moz-transform: rotate(-90deg); 879 | -webkit-transform: rotate(-90deg); 880 | /* This is used to center the rotation of the arrow to make it rotate properly. */ 881 | transform-origin: 6px 1px; 882 | -moz-transform-origin: 6px 1px; 883 | -webkit-transform-origin: 6px 1px; 884 | 885 | transition: transform 250ms; 886 | -moz-transition: -moz-transform 250ms; 887 | -webkit-transition: -webkit-transition 250ms; 888 | } 889 | 890 | /* The expander's summary's pseudo element when clicked. */ 891 | .expandable_summary.expanded:before { 892 | /* Rotate the arrow to point down. */ 893 | /*NOTE: If 0 degrees is used, it will visually jump; therefore, 1 degree is used. */ 894 | transform: rotate(-1deg); 895 | -moz-transform: rotate(-1deg); 896 | -webkit-transform: rotate(-1deg); 897 | } 898 | 899 | /* Set the number styles for list elements (specifically for long definitions). */ 900 | ol { 901 | margin: 0; 902 | list-style-type: decimal; 903 | padding-left: 2em; 904 | } 905 | 906 | ol ol { 907 | list-style-type: lower-latin; 908 | } 909 | 910 | ol ol ol { 911 | list-style-type: lower-roman; 912 | } 913 | 914 | ol ol ol ol { 915 | list-style-type: upper-latin; 916 | } 917 | 918 | ol ol ol ol ol { 919 | list-style-type: upper-roman; 920 | } 921 | 922 | ol ol ol ol ol ol { 923 | list-style-type: decimal; 924 | } 925 | 926 | 927 | .dropdown { 928 | /* Move the arrow all the way to the right, and center it vertically. */ 929 | background-position: 100%; 930 | /* Add some extra space to the right for the drop down arrow. */ 931 | padding-right: 12px; 932 | cursor: pointer; 933 | } 934 | 935 | .lex-body { 936 | /* Push the text over a little to make it line up (more or less) with the title. */ 937 | margin-left: 4px; 938 | } 939 | 940 | /* The element that causes the text to fade out when a large callout is shown. */ 941 | .transparent_el { 942 | background: #FFF; 943 | } 944 | 945 | /*NOTE See above for .more-button-buffer:hover and .more-button-buffer:active. */ 946 | .more-button { 947 | position: absolute; 948 | bottom: 0; 949 | right: 0; 950 | 951 | border-top: 1px solid rgba(0,0,0,.1); 952 | border-left: 1px solid rgba(0,0,0,.1); 953 | border-radius: 9px 0; 954 | 955 | padding: 4px; 956 | 957 | font-size: 12px; 958 | /* This is the same as .lang. */ 959 | background: rgba(0,0,200,.05); 960 | color: rgba(0,0,0,.2); 961 | 962 | cursor: pointer; 963 | 964 | /* Prevent the user from selecting the text (though only when directly clicking it). */ 965 | /* This also prevent users from copying and text in Mozilla. */ 966 | user-select: none; 967 | -webkit-touch-callout: none; 968 | -webkit-user-select: none; 969 | -moz-user-select: none; 970 | -ms-user-select: none; 971 | 972 | /* Apply transitions to all changes. */ 973 | /* Transition for Mozilla (Firefox 4.0+) */ 974 | -moz-transition: background .2s, color .2s; 975 | /* Transition for WebKit */ 976 | -webkit-transition: background .2s, color .2s; 977 | /* Transition for Opera 10.5+ */ 978 | transition: background .2s, color .2s; 979 | } 980 | 981 | .more-button:active { 982 | /* Emulate button pressing */ 983 | /*NOTE: There is another .more-button:active above. */ 984 | padding: 5px 3px 3px 5px; 985 | /* The box shadow gives some depth to the button when it is pressed. */ 986 | box-shadow: inset 2px 2px 3px rgba(0,0,0,.2); 987 | } 988 | 989 | /* This is an invisible element that is used to prevent real text from going over the more button. */ 990 | .more-button-buffer { 991 | position: static; 992 | visibility: hidden; 993 | float: right; 994 | /* NOTE: Because this element is not positioned absolutely, it is moved over to the left slightly (due to padding in the parent element), */ 995 | /* but this extra padding adds extra buffer between the button and the text, so the padding attribute does not need to be changed here. */ 996 | } 997 | 998 | 999 | /* The class allows for simple fade ins and outs with JavaScript. */ 1000 | .fade { 1001 | /* Fade out slightly when the mouse cursor moves off. */ 1002 | /* Transition for Mozilla (Firefox 4.0+) */ 1003 | -moz-transition: opacity .3s ease; 1004 | /* Transition for WebKit */ 1005 | -webkit-transition: opacity .3s ease; 1006 | /* Transition for Opera 10.5+ */ 1007 | transition: opacity .3s ease; 1008 | } 1009 | 1010 | 1011 | /* Verses created from a search. */ 1012 | .search_verse { 1013 | text-indent: 2em; 1014 | width: 100%; 1015 | } 1016 | 1017 | .search_verse span { 1018 | cursor: pointer; 1019 | } 1020 | 1021 | /* Verses created from a verse lookup. */ 1022 | .verse { 1023 | text-indent: 1em; 1024 | width: 100%; 1025 | } 1026 | 1027 | /* Make all lines of text the same height. */ 1028 | .scrolls .verse, .scrolls .first_verse, .psalm_title, .search_verse, .paragraph, .subscription { 1029 | line-height: 19px; 1030 | } 1031 | 1032 | /***************************************/ 1033 | /* Verses created from a verse lookup. */ 1034 | /***************************************/ 1035 | 1036 | .psalm_title { 1037 | /* Adding padding to the sides and bottom to distinguish the Psalm title from the rest of the text. */ 1038 | /*NOTE: Padding should always be used for verses instead of margin because it can interfere with determining the verse range. */ 1039 | padding: 0 2em 19px 2em; 1040 | } 1041 | 1042 | /* Subscriptions added to the end of Pauline Epistles. */ 1043 | .subscription { 1044 | /* Add indentation on both sides and insert a single blank line between the text and the subscription. */ 1045 | /*NOTE: Padding should always be used for verses instead of margin because it can interfere with determining the verse range. */ 1046 | /*FIXME: Because of the top padding, when looking up a subscription, the text is scrolled to the blank line above the text. */ 1047 | padding: 19px 2em 0 2em; 1048 | } 1049 | 1050 | /* Drop caps for the first letter of a chapter */ 1051 | .first_verse:first-letter, .first_paragraph:first-letter { 1052 | /*FIXME: Capital J's can cover up part of characters in the third line of the first verse. (See 2 Kings 22:1 in a narrow window.) */ 1053 | font-size: 51px; 1054 | float: left; 1055 | 1056 | /*NOTE: Bold makes some letters better, especially P's. */ 1057 | /* This fixes the spacing problem with some letters, like W, but not quite capital J's (see Psalm 43). */ 1058 | margin-bottom: -9px; 1059 | 1060 | /* Ensure that the verse letter has the correct line height even if it is a divine word. (See Psalm 3.) */ 1061 | line-height: 19px!important; 1062 | } 1063 | 1064 | .first_verse { 1065 | /* This fixes a bug where the last word of the first verse sometimes wraps. */ 1066 | width: 100%; 1067 | /* This makes sure that the first line is at least two lines long to give the drop caps just enough space. */ 1068 | min-height: 38px; 1069 | } 1070 | 1071 | 1072 | .paragraph { 1073 | text-indent: 2em; 1074 | } 1075 | 1076 | .paragraph .first_verse { 1077 | display: inline; 1078 | } 1079 | 1080 | .first_paragraph { 1081 | text-indent: 0; 1082 | } 1083 | 1084 | .paragraph .verse { 1085 | display: inline; 1086 | } 1087 | 1088 | /* Verse numbers in paragraph form */ 1089 | .paragraph span { 1090 | /* Shrink the verse number. */ 1091 | font-size: 75%; 1092 | /* Raise it slightly. */ 1093 | vertical-align: 4px; 1094 | /* Remove extra line height created by raising the numbers. */ 1095 | line-height: 1px!important; 1096 | /* Move the verse numbers over a little closer to the text. */ 1097 | margin-right: -2px; 1098 | } 1099 | 1100 | /* Serif fonts */ 1101 | .book, .chapter, .short_book, .first_verse:first-letter, .first_paragraph:first-letter, .d, .psalm_119_heading { 1102 | font-family: 'TeX Gyre Pagella Webfont', 'Palatino Linotype', Palatino, TexGyrePagella, 'TeX Gyre Pagella', 'URWPalladioL', 'Book Antiqua', 'Times new Roman', Times, serif; 1103 | } 1104 | 1105 | .book, .chapter, .short_book { 1106 | text-align: center; 1107 | width: 100%; 1108 | font-size: 23px; 1109 | } 1110 | 1111 | .chapter, .short_book, .psalm_119_heading { 1112 | padding: 13px 0 13px 0; 1113 | margin: 0; 1114 | line-height: 31px; 1115 | } 1116 | 1117 | /* Long book titles */ 1118 | div.book { 1119 | padding: 57px 0 19px 0; 1120 | } 1121 | 1122 | 1123 | /* Main name for long book titles */ 1124 | h1 { 1125 | margin: 0; 1126 | font-size: 69px; 1127 | display: block; 1128 | font-weight: bold; 1129 | line-height: 95px; 1130 | } 1131 | 1132 | /* Pre- and post-titles for long book titles */ 1133 | h2 { 1134 | font-size: 23px; 1135 | display: block; 1136 | font-weight: normal; 1137 | margin: 0; 1138 | line-height: 19px; 1139 | } 1140 | 1141 | 1142 | /* The divine name of God */ 1143 | .d { 1144 | /* Firefox 4.0+ and recent version of Chrome also support font-feature-settings:"smcp" which turns on small caps as well. */ 1145 | font-variant: small-caps; 1146 | font-size: 16px; 1147 | /* Different fonts have different line-heights. This eliminates the inconsistency. */ 1148 | line-height: 1px!important; 1149 | } 1150 | 1151 | /* Added (implied) words */ 1152 | .a { 1153 | font-style: italic; 1154 | } 1155 | 1156 | /* Quotations of Jesus */ 1157 | .q { 1158 | -webkit-transition: color 1s; 1159 | transition: color 1s; 1160 | color: #D00; 1161 | } 1162 | /* Override for black letters. */ 1163 | .black_letter .q { 1164 | color: #000; 1165 | } 1166 | 1167 | 1168 | .no_results { 1169 | margin-top: 10px; 1170 | } 1171 | 1172 | 1173 | .psalm_119_heading { 1174 | text-align: center; 1175 | font-size: 23px; 1176 | } 1177 | 1178 | .psalm_119_heading:first-letter { 1179 | /* For some reason, the Hebrew letter creates a line height issues that can be solved by completely removing the line height from it. */ 1180 | line-height: 0; 1181 | } 1182 | 1183 | 1184 | /* Style the previous and next links for the non-JavaScript version. */ 1185 | .static_link { 1186 | text-decoration: none; 1187 | color: #000; 1188 | opacity: .5; 1189 | margin: 10px 0; 1190 | } 1191 | .static_link:hover { 1192 | opacity: 1; 1193 | } 1194 | 1195 | .prev { 1196 | float: left; 1197 | } 1198 | 1199 | .next { 1200 | float: right; 1201 | } 1202 | 1203 | .unsupported_warning { 1204 | display: block; 1205 | text-align: center; 1206 | padding: 10px; 1207 | } 1208 | 1209 | /* This is used in the non-JS version. */ 1210 | .footer { 1211 | padding: 20px; 1212 | max-width: 800px; 1213 | margin: 0 auto; 1214 | } 1215 | 1216 | /* Highlighted found words from searches */ 1217 | .f1 { 1218 | background: #FF6; 1219 | } 1220 | .f2 { 1221 | background: #AFF; 1222 | } 1223 | .f3 { 1224 | background: #9F9; 1225 | } 1226 | .f4 { 1227 | background: #F99; 1228 | } 1229 | .f5 { 1230 | background: #F6F; 1231 | } 1232 | .f6 { 1233 | background: #800; 1234 | } 1235 | .f7 { 1236 | background: #0A0; 1237 | } 1238 | .f8 { 1239 | background: #860; 1240 | } 1241 | .f9 { 1242 | background: #049; 1243 | } 1244 | .f10 { 1245 | background: #909; 1246 | } 1247 | .f11 { 1248 | background: #EE0; 1249 | } 1250 | .f12 { 1251 | background: #0FF; 1252 | } 1253 | .f13 { 1254 | background: #A0A; 1255 | } 1256 | .f14 { 1257 | background: #7F0; 1258 | } 1259 | .f15 { 1260 | background: #19f; 1261 | } 1262 | .f16 { 1263 | background: #F00; 1264 | } 1265 | .f17 { 1266 | background: #F70; 1267 | } 1268 | .f18 { 1269 | background: #CF0; 1270 | } 1271 | .f19 { 1272 | background: #AA9; 1273 | } 1274 | .f20 { 1275 | background: #469; 1276 | } 1277 | 1278 | /* Fonts for Mozilla/Opera/WebKit/IE9+ */ 1279 | /*NOTE: If no local() fonts are supplied, Mozilla will always download the web font if it detects an element with this CSS. */ 1280 | @font-face { 1281 | font-family: 'TeX Gyre Pagella Webfont'; 1282 | src: local('Palatino Linotype'), 1283 | local('PalatinoLinotype-Roman'), 1284 | local('Palatino'), 1285 | local('TeX Gyre Pagella'), 1286 | local('TeXGyrePagella-Regular'), 1287 | url('/fonts/TeXGyrePagella-Regular.woff') format('woff'); 1288 | } 1289 | @font-face { 1290 | font-family: 'TeX Gyre Pagella Webfont'; 1291 | src: local('Palatino Linotype Italic'), 1292 | local('PalatinoLinotype-Italic'), 1293 | local('Palatino Italic'), 1294 | local('Palatino-Italic'), 1295 | local('TeXGyrePagella-Italic'), 1296 | url('/fonts/TeXGyrePagella-Italic.woff') format('woff'); 1297 | font-style: italic; 1298 | } 1299 | @font-face { 1300 | font-family: 'TeX Gyre Pagella Webfont'; 1301 | src: local('Palatino Linotype Bold'), 1302 | local('PalatinoLinotype-Bold'), 1303 | local('Palatino Linotype, Bold'), 1304 | local('Palatino Bold'), 1305 | local('Palatino-Bold'), 1306 | local('TeXGyrePagella-Bold'), 1307 | url('/fonts/TeXGyrePagella-Bold.woff') format('woff'); 1308 | font-weight: bold; 1309 | } 1310 | @font-face { 1311 | font-family: 'TeX Gyre Pagella Webfont'; 1312 | src: local('Palatino Linotype Bold Italic'), 1313 | local('PalatinoLinotype-BoldItalic'), 1314 | local('Palatino-BoldItalic'), 1315 | local('TeXGyrePagella-BoldItalic'), 1316 | url('/fonts/TeXGyrePagella-BoldItalic.woff') format('woff'); 1317 | font-weight: bold; 1318 | font-style: italic; 1319 | } 1320 | 1321 | @font-face { 1322 | font-family: 'Bitstream Vera Sans Webfont'; 1323 | src: local('Verdana'), 1324 | local('Bitstream Vera Sans'), 1325 | local('BitstreamVeraSans-Roman'), 1326 | url('/fonts/BitstreamVeraSans-Roman.woff') format('woff'); 1327 | } 1328 | @font-face { 1329 | font-family: 'Bitstream Vera Sans Webfont'; 1330 | src: local('Verdana Italic'), 1331 | local('Verdana-Italic'), 1332 | local('Bitstream Vera Sans Oblique'), 1333 | local('BitstreamVeraSans-Oblique'), 1334 | url('/fonts/BitstreamVeraSans-Oblique.woff') format('woff'); 1335 | font-style: italic; 1336 | } 1337 | @font-face { 1338 | font-family: 'Bitstream Vera Sans Webfont'; 1339 | src: local('Verdana Bold'), 1340 | local('Verdana-Bold'), 1341 | local('Bitstream Vera Sans Bold'), 1342 | local('BitstreamVeraSans-Bold'), 1343 | url('/fonts/BitstreamVeraSans-Bold.woff') format('woff'); 1344 | font-weight: bold; 1345 | } 1346 | @font-face { 1347 | font-family: 'Bitstream Vera Sans Webfont'; 1348 | src: local('Verdana Bold Italic'), 1349 | local('Verdana-BoldItalic'), 1350 | local('Bitstream Vera Sans Bold Oblique'), 1351 | local('BitstreamVeraSans-BoldOblique'), 1352 | url('/fonts/BitstreamVeraSans-BoldOblique.woff') format('woff'); 1353 | font-weight: bold; 1354 | font-style: italic; 1355 | } 1356 | 1357 | @font-face { 1358 | font-family: 'BF_Hebrew'; 1359 | src: local('Ezra SIL SR'), 1360 | url('/fonts/BF_Hebrew.woff') format('woff'); 1361 | } 1362 | 1363 | 1364 | 1365 | /* HTML scrollbar styles (WebKit only) */ 1366 | /* NOTE: If ::-webkit-scrollbar is not present, the other styles will not work. */ 1367 | html > ::-webkit-scrollbar { 1368 | width: 8px; 1369 | background: #FFF; 1370 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 1371 | } 1372 | 1373 | html > ::-webkit-scrollbar-thumb { 1374 | border-radius: 4px; 1375 | background: rgba(130,130,130,.3); 1376 | /* Do not let the thumb get too small. */ 1377 | min-height: 35px; 1378 | } 1379 | 1380 | html > ::-webkit-scrollbar-thumb:hover { 1381 | background: rgba(130,130,130,.5); 1382 | } 1383 | 1384 | /*
scrollbars (WebKit only) */ 1385 | /* NOTE: If ::-webkit-scrollbar is not present, the other styles will not work. */ 1386 | div ::-webkit-scrollbar { 1387 | width: 8px; 1388 | background: transparent; 1389 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 1390 | border-radius: 4px; 1391 | } 1392 | 1393 | div ::-webkit-scrollbar-thumb { 1394 | border-radius: 4px; 1395 | background: rgba(130,130,130,.3); 1396 | /* Do not let the thumb get too small. */ 1397 | min-height: 25px; 1398 | } 1399 | 1400 | div ::-webkit-scrollbar-thumb:hover { 1401 | background: rgba(130,130,130,.5); 1402 | } 1403 | 1404 | 1405 | /* Set viewport width for modile devices. */ 1406 | /*NOTE: This needs to be tested on more devices and may need multiple media queries to display properly across devices. */ 1407 | @-o-viewport { 1408 | width: 67%; 1409 | user-zoom: fixed; 1410 | } 1411 | @-ms-viewport { 1412 | width: 67%; 1413 | user-zoom: fixed; 1414 | } 1415 | @-moz-viewport { 1416 | width: 67%; 1417 | user-zoom: fixed; 1418 | } 1419 | @-webkit-viewport { 1420 | width: 67%; 1421 | user-zoom: fixed; 1422 | } 1423 | @viewport { 1424 | width: 67%; 1425 | user-zoom: fixed; 1426 | } 1427 | 1428 | 1429 | /*******************************/ 1430 | /* WebKit & Opera specific CSS */ 1431 | /*******************************/ 1432 | 1433 | /* Make the drop caps aligned with the second line of text. */ 1434 | .webkit .first_verse:first-letter, .webkit .first_paragraph:first-letter, .opera .first_verse:first-letter, .opera .first_paragraph:first-letter { 1435 | margin-bottom: 0; 1436 | margin-top: 5px; 1437 | padding: 1px; 1438 | } 1439 | 1440 | /****************************/ 1441 | /* Just WebKit specific CSS */ 1442 | /****************************/ 1443 | 1444 | .webkit .lex-orig_word { 1445 | /* NOTE: Because the font size changes via a CSS transition, the line-height also must transition as well. See above for the declaration. */ 1446 | line-height: 150%; 1447 | } 1448 | 1449 | /***************************/ 1450 | /* Just Opera specific CSS */ 1451 | /***************************/ 1452 | 1453 | .opera .first_verse:first-letter, .opera .first_paragraph:first-letter { 1454 | margin-bottom: 0; 1455 | margin-top: 13px; 1456 | padding: 1px; 1457 | } 1458 | 1459 | /* Because Opera raises these elements regardless of whether they are on the same line or not, the alignment must be reverted, */ 1460 | /* and thereby causing the .lex-pronun element to be improperly aligned if it wraps. */ 1461 | .opera .lex-orig_word, .opera .lex-pronun { 1462 | vertical-align: baseline; 1463 | } 1464 | 1465 | 1466 | /*******************/ 1467 | /* IE specific CSS */ 1468 | /*******************/ 1469 | 1470 | /* Make the inputIcon appear next to the queryInput. */ 1471 | /*NOTE: The query box is slightly off centered in IE with this CSS. */ 1472 | .ie .inputIcon { 1473 | position: relative; 1474 | } 1475 | 1476 | /* Make the drop caps aligned with the second line of text. */ 1477 | .ie .first_verse:first-letter, .ie .first_paragraph:first-letter { 1478 | margin-top: 5px; 1479 | margin-bottom: 0; 1480 | } 1481 | 1482 | 1483 | /************************/ 1484 | /* Mozilla specific CSS */ 1485 | /************************/ 1486 | 1487 | /* Center the crown of thorns loader. */ 1488 | .moz .crown_loader { 1489 | -moz-transform-origin: 50.5%; 1490 | position: relative; 1491 | top: -1px; 1492 | } 1493 | -------------------------------------------------------------------------------- /server/bibleforge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * BibleForge 3 | * 4 | * @date 05-15-12 5 | * @version alpha (α) 6 | * @link http://BibleForge.com 7 | * @license The MIT License (MIT) 8 | */ 9 | 10 | /*! 11 | * The BibleForge motto: 12 | * 13 | * "all things whatsoever ye would that men should do to you, do ye even so to them." 14 | * —Jesus (Matthew 7:12) 15 | */ 16 | 17 | /*! 18 | * Copyright (C) 2022 19 | * 20 | * Permission is hereby granted, free of charge, to any person obtaining 21 | * a copy of this software and associated documentation files (the 22 | * “Software”), to deal in the Software without restriction, including 23 | * without limitation the rights to use, copy, modify, merge, publish, 24 | * distribute, sublicense, and/or sell copies of the Software, and to 25 | * permit persons to whom the Software is furnished to do so. 26 | * 27 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 28 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 30 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 31 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 32 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 33 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | */ 35 | 36 | /// Set JSHint options. 37 | // jshint bitwise:true, curly:true, eqeqeq:true, forin:true, immed:true, latedef:true, newcap:true, noarg:true, noempty:true, nonew:true, onevar:true, plusplus:true, quotmark:double, strict:true, undef:true, unused:strict, node:true 38 | 39 | "use strict"; 40 | 41 | /// Create the BibleForge global object to which everything else attaches. 42 | var BF = {}; 43 | 44 | /// ******************** 45 | /// * Create constants * 46 | /// ******************** 47 | 48 | ///TODO: Link this with the client-side code, perhaps using the Forge. 49 | BF.consts = { 50 | /// Query type "constants" 51 | verse_lookup: 1, 52 | mixed_search: 2, 53 | standard_search: 3, 54 | grammatical_search: 4, 55 | lexical_lookup: 5, 56 | 57 | /// Direction "constants" 58 | additional: 1, 59 | previous: 2 60 | }; 61 | 62 | /// **************** 63 | /// * Load modules * 64 | /// **************** 65 | 66 | BF.config = require("./config.js").config; 67 | 68 | /// Attach the database object. 69 | BF.db = require("./modules/db.js").db(BF.config.db); 70 | 71 | /// Attach the email sender module. 72 | BF.email = require("./modules/email.js").init(BF.config.smtp); 73 | 74 | /// *************************** 75 | /// * Create helper functions * 76 | /// *************************** 77 | 78 | /** 79 | * Safely parse JSON. 80 | * 81 | * @param str (string) The JSON encoded string to parse. 82 | * @return The parsed JSON or NULL if the JSON is invalid. 83 | * @todo Load this code from the client side (or copy it via the Forge). 84 | */ 85 | BF.parse_json = function (str) 86 | { 87 | try { 88 | return JSON.parse(str); 89 | } catch (e) {} 90 | }; 91 | 92 | /** 93 | * Escape a string to be safely added inside HTML. 94 | * 95 | * @example BF.escape_html('This is a "harmless" comment '); /// Returns "This is a "harmless" comment <script>...</script>" 96 | * @param str (string) The string to be escaped 97 | * @note This code only escapes the few dangerous symbols, not all of them. 98 | */ 99 | BF.escape_html = function (str) 100 | { 101 | ///NOTE: It must first replace ampersands (&); otherwise, the other entities would be escaped twice. 102 | return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(//g, ">"); 103 | }; 104 | 105 | /** 106 | * Get the book, chapter, and verse numbers from a verse ID. 107 | * 108 | * @example BF.get_b_c_v(1002003); /// Returns {b: 1, c: 2, v: 3} 109 | * @param verseID (number || string) The verse ID to convert. 110 | * @return An object containing the book, chapter, and verse numbers: {b: book, c: chapter, v: verse} 111 | * @todo Load this code from the client side (or copy it via the Forge). 112 | */ 113 | BF.get_b_c_v = function (verseID) 114 | { 115 | var c, 116 | v = verseID % 1000; 117 | 118 | c = ((verseID - v) % 1000000) / 1000; 119 | 120 | return { 121 | b: (verseID - v - c * 1000) / 1000000, 122 | c: c, 123 | v: v 124 | }; 125 | }; 126 | 127 | /** 128 | * Insert data into a string. 129 | * 130 | * @example BF.insert({"a": "text", num: 10}, "This is some {a}; {num} is a number. Here's more {a}.") /// Returns "This is some text; 10 is a number. Here's more text." 131 | * @param obj (object) An object representing the data to insert. 132 | * @param template (string) The template string using curly brackets with a name to indicate placeholders. 133 | */ 134 | BF.insert = function (obj, template) 135 | { 136 | /// Match all matching curly brackets, and send them to the function. 137 | return template.replace(/{([^{}]+)}/g, function (whole, inside) 138 | { 139 | var data = obj[inside]; 140 | return typeof data !== "undefined" ? data : whole; 141 | }); 142 | }; 143 | 144 | 145 | /** 146 | * Tries to detect bots based on their user-agent. 147 | * 148 | * @param agent (string) The user-agent to examine. 149 | * @return A boolean indicating whether or not the client appears to be a bot. 150 | * @note User-agents can be easily spoofed. 151 | */ 152 | BF.is_bot = function (agent) 153 | { 154 | ///NOTE: Using parentheses prevents the regex from possibly (however unlikely) from looking like the divion sign. 155 | return (/google(?:bot|\/)|yahoo\!|bingbot|baiduspider|iaskspider|ia_archiver|yandex/i).test(agent); 156 | }; 157 | 158 | /// ************************** 159 | /// * Create query functions * 160 | /// ************************** 161 | 162 | /** 163 | * Retrieve verses from the database. 164 | * 165 | * @example BF.verse_lookup({q: "1001001"}, function (data) {}); /// Look up verses starting with Genesis 1:1 in English. 166 | * @example BF.verse_lookup({q: "19119160", f: "1"}, function (data) {}); /// Look up verses starting with Psalm 119:160 in English. 167 | * @example BF.verse_lookup({q: "19119152", d: "2"}, function (data) {}); /// Look up previous verses starting with Psalm 119:152 in English. 168 | * @example BF.verse_lookup({q: "66022021", f: "1", l: "zh_t"}, function (data) {}); /// Look up verses starting with Revelation 22:21 in Traditional Chinese. 169 | * @param data (object) An object containing the query and query options. 170 | * Object structure: 171 | * q: "The verse ID", 172 | * d: "The direction (BF.consts.additional OR BF.consts.previous)" (optional) (default: BF.consts.additional) 173 | * f: "Whether or not to find the start of a paragraph" (optional) (default: FALSE) 174 | * l: "The language ID" (optional) (default: "en") 175 | * p: "Whether or not to return verses in groups of paragraphs" (optional) (default: TRUE) 176 | * @param callback (function) The function to send the results to. 177 | * @return An object containing the results (if any). 178 | * Object structure: 179 | * n: (array) An array of verse IDs for each verse returned 180 | * v: (array) An array of strings containing the HTML of the verses 181 | * p: (array) An array of numbers either 1 (indicating a paragraph break at that verse) or 0 (no paragraph break) 182 | * t: (number) The total number of verses 183 | */ 184 | BF.verse_lookup = function (data, callback) 185 | { 186 | var extra_fields, 187 | direction = data.d ? Number(data.d) : BF.consts.additional, 188 | find_paragraph_start = data.f === "1", 189 | in_paragraphs = typeof data.p === "undefined" ? true : data.p === "1", 190 | /// Select the language object specified by the query or use the default. 191 | lang = BF.langs[data.l] || BF.langs.en, 192 | limit, 193 | operator, 194 | order_by, 195 | verse_id = Number(data.q); 196 | 197 | if (in_paragraphs && lang.no_paragraphs) { 198 | in_paragraphs = false; 199 | } 200 | 201 | /** 202 | * Send the query to the database. 203 | * 204 | * @note This is a separate query because it can be called at two different times (and one is from an asynchronous callback). 205 | */ 206 | function run_query(starting_verse) 207 | { 208 | BF.db.query("SELECT id, words" + extra_fields + " FROM `bible_" + lang.id + "_html` WHERE id " + operator + starting_verse + order_by + " LIMIT " + limit, function (verses) 209 | { 210 | var i, 211 | len, 212 | res = { 213 | n: [], 214 | v: [] 215 | }; 216 | 217 | /// Was there no response from the database? This could mean the database crashed. 218 | if (!verses) { 219 | /// Send an empty response, and exit. 220 | callback({}); 221 | return; 222 | } 223 | 224 | if (in_paragraphs) { 225 | res.p = []; 226 | 227 | /// Determine the actual number of verses that should be returned (starting from the end). 228 | ///NOTE: Because the last verse cannot be in the middle of a paragraph break, it has to trim off the last partial paragraph from the database results. 229 | len = verses.length; 230 | /// Did it return the expected number of verses? 231 | /// If not, then it must have reached the end of the Bible, in which case it has also reached the end of a paragraph. 232 | if (len === limit) { 233 | /// Start at the end of the dataset, and look for the last (i.e., first in reverse order) paragraph marker. 234 | /// Once found, trim off the last, incomplete paragraph (if any). 235 | ///NOTE: When preforming previous lookups, there might not be anything to trim off, but additional lookups must at least trim off one verse 236 | /// because it must stop before the last paragraph marker. 237 | while (true) { 238 | /// Is it at a paragraph break? 239 | ///NOTE: Since the database may return "0" or "1" as strings, it is necessary to convert them to JavaScript Numbers so that 0 will be falesy. 240 | if (Number(verses[len - 1].paragraph)) { 241 | /// The first verse should be at a paragraph beginning, and the last verse 242 | /// should be just before one. Therefore, when looking up previous verses, 243 | /// we must get this verse (because previous lookups are in reverse). 244 | /// So, previous lookups should stop now because this verse is at the 245 | /// beginning of a paragraph, but additional lookups need to get the verse before. 246 | if (direction === BF.consts.previous) { 247 | break; 248 | } 249 | /// Move back one to get the verse before the paragraph break. 250 | len -= 1; 251 | break; 252 | } 253 | len -= 1; 254 | } 255 | } 256 | } else { 257 | /// When not breaking at paragraphs, just send back all of the verses retrieved from the database. 258 | len = verses.length; 259 | } 260 | 261 | /// Loop through the verses and fill in the results object. 262 | for (i = 0; i < len; i += 1) { 263 | res.n[i] = Number(verses[i].id); 264 | res.v[i] = verses[i].words; 265 | if (in_paragraphs) { 266 | res.p[i] = Number(verses[i].paragraph); 267 | } 268 | } 269 | 270 | /// Is the query looking up previous verses? 271 | if (direction === BF.consts.previous) { 272 | /// Because the database returns the verses in reverse order when preforming a previous lookup, they need to be reordered. 273 | ///NOTE: Because in paragraph mode, there is no way to know how many verses will be returned, it cannot simply put the verses in the array in reverse order above. 274 | res.n.reverse(); 275 | res.v.reverse(); 276 | if (in_paragraphs) { 277 | res.p.reverse(); 278 | } 279 | } 280 | 281 | /// Add the total number of verses being sent back to the client. 282 | res.t = res.n.length; 283 | 284 | callback(res); 285 | }); 286 | } 287 | 288 | /// If verse_id is not a number, send an empty response, and exit. 289 | if (isNaN(verse_id)) { 290 | callback({}); 291 | return; 292 | } 293 | 294 | if (verse_id < 1001001) { 295 | /// Default to Genesis 1:1 if the verse_id is too small. 296 | verse_id = 1001001; 297 | } else if (verse_id > 66022021) { 298 | /// If the user is looking for a verse past the end, default to Revelation 22:21. 299 | ///NOTE: 66022021 may need to be language dependent because different languages have different verse breaks. 300 | verse_id = 66022021; 301 | /// If returning paragraphs, make sure to find the beginning of the last paragraph. 302 | if (in_paragraphs) { 303 | find_paragraph_start = true; 304 | } 305 | } 306 | 307 | if (direction === BF.consts.additional) { 308 | operator = ">="; 309 | order_by = ""; 310 | } else { 311 | ///NOTE: To get the right verses in a previous verse lookup, we need to sort the database by id in reverse order because 312 | /// chapter and book boundaries are not predictable (i.e., we can't just say "WHERE id >= id - LIMIT"). 313 | operator = "<="; 314 | ///NOTE: Leading space is needed in case the preceding variable does end with whitespace. 315 | order_by = " ORDER BY id DESC"; 316 | } 317 | 318 | if (in_paragraphs) { 319 | /// The limit must be larger than the minimum length of the longest paragraph because paragraphs cannot be split. 320 | limit = lang.paragraph_limit; 321 | extra_fields = ", paragraph"; 322 | } else { 323 | limit = lang.minimum_desired_verses; 324 | extra_fields = ""; 325 | } 326 | 327 | /// If this is the first query and the query does not begin at an obvious paragraph break (e.g., the beginning of a chapter), we must first determine the where the paragraph begins. 328 | ///NOTE: For example, if the query is for Deuteronomy 6:4 (in paragraphs), the query cannot begin at Deuteronomy 6:4 because that is (or at least could be) the middle of a paragraph. 329 | /// So, we must first use another query to determine the first paragraph break before (or at) Deuteronomy 6:4. Currently, in the English version, it is Deuteronomy 6:3, so that will be used for starting_verse. 330 | if (find_paragraph_start) { 331 | /// Look up the nearest verse that is at a paragraph break, and then run the query. 332 | ///NOTE: This is much faster than adding a subquery to the main query. 333 | ///NOTE: Currently, find_paragraph_start is never true when direction === BF.consts.previous because previous lookups always start at a paragraph break. 334 | /// In order to find the correct starting verse when looking up in reverse, the comparison operator (<=) would need to be greater than or equal to (>=), 335 | /// and 1 would need to be subtracted from the found starting id. 336 | BF.db.query("SELECT id FROM `bible_" + lang.id + "_html` WHERE id <= " + verse_id + " AND paragraph = 1 ORDER BY id DESC LIMIT 1", function (start_id) 337 | { 338 | /// Was there no response from the database? This could mean the database crashed. 339 | if (!start_id || !start_id[0]) { 340 | /// Send an empty response, and exit. 341 | callback({}); 342 | return; 343 | } 344 | 345 | run_query(start_id[0].id); 346 | }); 347 | } else { 348 | /// Since if not grouping the verses in paragraphs, it does not matter where the query starts, so just start with the verse being queried. 349 | run_query(verse_id); 350 | } 351 | }; 352 | 353 | 354 | /** 355 | * Preform a standard search of the Bible. 356 | * 357 | * @example BF.standard_search({q: "love"}, function (data) {}); /// Preform a search for the word "love." 358 | * @example BF.standard_search({q: "love", s: "5033004"}, function (data) {}); /// Preform a search for the word "love" starting from Deuteronomy 33:4. 359 | * @param data (object) An object containing the query and query options. 360 | * Object structure: 361 | * q: "The search query", 362 | * l: "The language ID" (optional) (default: "en") 363 | * s: "The verse ID from which to start the search" (optional) (default: 0) 364 | * @param callback (function) The function to send the results to. 365 | * @return An object containing the results (if any). 366 | * Object structure: 367 | * n: (array) An array of verse IDs for each verse returned 368 | * v: (array) An array of strings containing the HTML of the verses 369 | * t: (number) The total number of verses found in the search, not the total sent back, only present on intial queries 370 | */ 371 | BF.standard_search = function (data, callback) 372 | { 373 | var db_client, 374 | html_table, 375 | initial, 376 | /// Select the language object specified by the query or use the default. 377 | lang = BF.langs[data.l] || BF.langs.en, 378 | query, 379 | start_at = data.s ? Number(data.s) : 0, 380 | terms = String(data.q), 381 | verse_table; 382 | 383 | html_table = "`bible_" + lang.id + "_html`"; 384 | verse_table = "`verse_text_" + lang.id + "`"; 385 | 386 | ///NOTE: Currently, the first query does not specifiy a verse. 387 | initial = !Boolean(start_at); 388 | 389 | ///TODO: Requery if no client is returned. 390 | db_client = BF.db.request_a_client(); 391 | 392 | /// Create the first part of the SQL/SphinxQL query. 393 | query = "SELECT " + verse_table + ".id, " + html_table + ".words FROM " + verse_table + ", " + html_table + " WHERE " + html_table + ".id = " + verse_table + ".id AND " + verse_table + ".query = \"" + db_client.escape_sphinx(terms) + ";limit=" + lang.minimum_desired_verses + ";ranker=none"; 394 | 395 | /// Should the query start somewhere in the middle of the Bible? 396 | if (start_at) { 397 | ///NOTE: By keeping all of the settings in the Sphinx query, Sphinx can preform the best optimizations. 398 | /// Another, less optimized, approach would be to use the database itself to filter the results like this: 399 | /// ...WHERE id >= start_at AND query="...;limit=9999999" LIMIT lang.minimum_desired_verses 400 | query += ";minid=" + start_at; 401 | } 402 | 403 | /// Determine the search mode. 404 | /// Default is SPH_MATCH_ALL (i.e., all words are required: word1 & word2). 405 | /// SPH_MATCH_ALL should be the fastest and needs no sorting. 406 | 407 | /// Is there more than one word? 408 | if (terms.indexOf(" ") >= 0) { 409 | /// Are there more than 10 search terms in the query, or does the query contains double quotes (")? 410 | ///NOTE: Could use the more accurate (/([a-z-]+[^a-z-]+){11}/.test(terms)) to find word count, but it is slower. 411 | if (terms.indexOf("\"") >= 0 || terms.split(" ").length > 9) { 412 | /// By default, other modes stop at 10, but SPH_MATCH_EXTENDED does more (256?). 413 | /// Phrases (words in quotes) require SPH_MATCH_EXTENDED mode. 414 | ///NOTE: SPH_MATCH_BOOLEAN is supposed to find more than 10 words too but doesn't seem to. 415 | /// mode=extended is the most complex (and slowest?). 416 | /// Since we want the verses in canonical order, we need to sort the results by id, not based on weight. 417 | query += ";mode=extended;sort=extended:@id asc"; 418 | /// Are boolean operators present? 419 | ///NOTE: This detects all ampersands (&), all pipes (|), and hyphens (-) only at the beginning of the string (e.g., "-word1 word2") or after a space (e.g., "word1 -word2"). 420 | /// The reason why only some hyphens are detected is that hyphens are only special symbols in certain positions. If a hyphen separates two words (e.g., " Baal-peor"), it is not a special symbol. 421 | } else if (/(?:(?:^| )-|&|\|)/.test(terms)) { 422 | /// Set mode to boolean and order by id. 423 | query += ";mode=boolean;sort=extended:@id asc"; 424 | /// Multiple words are being searched for but nothing else special. 425 | } else { 426 | /// Just order by id. 427 | query += ";sort=extended:@id asc"; 428 | } 429 | } 430 | 431 | if (initial) { 432 | /// Initial queries need to calculate the total verse. 433 | ///NOTE: SphinxSE does not return statistics by default, but we can retrieve them by running another query immediately after the first 434 | /// on the INFORMATION_SCHEMA.SESSION_STATUS table and UNION'ing it to the first. 435 | /// The only draw back to this is that both queries must have the same number of columns. 436 | /// Other ways to get the statistics is with the the following queries: 437 | /// SHOW ENGINE SPHINX STATUS; 438 | /// +--------+-------+-------------------------------------------------+ 439 | /// | Type | Name | Status | 440 | /// +--------+-------+-------------------------------------------------+ 441 | /// | SPHINX | stats | total: 421, total found: 421, time: 1, words: 1 | 442 | /// | SPHINX | words | love:421:498 | 443 | /// +--------+-------+-------------------------------------------------+ 444 | /// 445 | /// SHOW STATUS LIKE 'sphinx_%'; 446 | /// +--------------------------------+--------------+ 447 | /// | Variable_name | Value | 448 | /// +--------------------------------+--------------+ 449 | /// | sphinx_error_commits | 0 | 450 | /// | sphinx_error_group_commits | 0 | 451 | /// | sphinx_error_snapshot_file | | 452 | /// | sphinx_error_snapshot_position | 0 | 453 | /// | sphinx_time | 1 | 454 | /// | sphinx_total | 421 | 455 | /// | sphinx_total_found | 421 | 456 | /// | sphinx_word_count | 1 | 457 | /// | sphinx_words | love:421:498 | 458 | /// +--------------------------------+--------------+ 459 | /// 460 | /// However, because these queries are SHOW queries and not SELECT queries, they must be executed after the initial SELECT query. 461 | /// 462 | ///NOTE: The first column is currently ignored. 463 | query += "\" UNION SELECT 0, VARIABLE_VALUE FROM INFORMATION_SCHEMA.SESSION_STATUS WHERE VARIABLE_NAME = 'sphinx_total_found'"; 464 | } else { 465 | query += "\""; 466 | } 467 | 468 | /// Run the Sphinx search and return both the verse IDs and the HTML. 469 | db_client.query(query, function (data, err) 470 | { 471 | var i, 472 | len, 473 | res = { 474 | n: [], 475 | v: [] 476 | }; 477 | 478 | /// Was there no response (or an invalid response) from the database? This could mean the database or Sphinx crashed. 479 | ///NOTE: If merely the length is 0, that means there were no results for that query, and we need to send an empty result with the proper response and not attempt to log it as an error. 480 | if (!data) { 481 | ///TODO: Do better logging, 482 | if (err) { 483 | console.log(err); 484 | } 485 | /// Send an empty response, and exit. 486 | callback({}); 487 | return; 488 | } 489 | 490 | if (initial) { 491 | if (data.length > 0) { 492 | /// Because all of the columns share the same name when using UNION, the total verses found statistic is in the "words" column. 493 | res.t = Number(data.pop().words); 494 | } else { 495 | res.t = 0; 496 | } 497 | } 498 | 499 | len = data.length; 500 | 501 | for (i = 0; i < len; i += 1) { 502 | res.n[i] = Number(data[i].id); 503 | res.v[i] = data[i].words; 504 | } 505 | 506 | callback(res); 507 | }); 508 | }; 509 | 510 | /** 511 | * Preform a grammatical (morphological) search of the Bible. 512 | * 513 | * @example BF.standard_search({q: '["love",[[3,1]],[0]]'}, function (data) {}); /// Preform a search for the word "love" when spoken by Jesus (i.e., red letter). 514 | * @example BF.standard_search({q: '["love",[[3,1]],[0]]', s: "704772"}, function (data) {}); /// Preform a search for the word "love" when spoken by Jesus (i.e., red letter) starting from the 704772th word (in English). 515 | * @example BF.standard_search({q: '["love",[[3,1],[6,3],[5,2]],[1,0,0]]'}, function (data) {}); /// Preform a search for the word "love" when not spoken by Jesus (i.e., black letter) and that is in the third person plural. 516 | * @param data (object) An object containing the query and query options. 517 | * Object structure: 518 | * q: "A JSON encoed string defining an array of search terms and grammatical properties.", 519 | * JSON structure: 520 | * ["WORD", [[(number) GRAMMATICAL_CATEGORY, (number) ATTRIBUTE], ...],[(boolean) EXCLUDE, ...]] 521 | * l: "The language ID" (optional) (default: "en") 522 | * s: "The word ID (in the translated language) from which to start the search" (optional) (default: 0) 523 | * @param callback (function) The function to send the results to. 524 | * @return An object containing the results (if any). 525 | * Object structure: 526 | * i: (array) An array of word IDs for each highlighted word 527 | * n: (array) An array of verse IDs for each verse returned 528 | * v: (array) An array of strings containing the HTML of the verses 529 | * t: (number) The total number of verses found in the search, not the total sent back, only present on intial queries 530 | * @note Grammatical searches examine the morphology of the original languages, not translations. 531 | * @note The data.s property refers to the word ID, not the verse ID. 532 | */ 533 | BF.grammatical_search = function (data, callback) 534 | { 535 | var db_client, 536 | html_table, 537 | i, 538 | initial, 539 | /// Select the language object specified by the query or use the default. 540 | lang = BF.langs[data.l] || BF.langs.en, 541 | morphological_table, 542 | query, 543 | start_at = data.s ? Number(data.s) : 0, 544 | ///TODO: Make this an object instead. 545 | query_arr = BF.parse_json(data.q); 546 | 547 | /// If query_arr is not an array, return an empty result. 548 | /// If the first element is not a string or the second or third element is not an array, return an empty result. 549 | ///NOTE: This assumes only one grammatical word being searched for, which is currently the limit. 550 | if (!(query_arr instanceof Array) || typeof query_arr[0] !== "string" || !(query_arr[1] instanceof Array) || !(query_arr[2] instanceof Array)) { 551 | callback({}); 552 | return; 553 | } 554 | 555 | ///TODO: Requery if no client is returned. 556 | db_client = BF.db.request_a_client(); 557 | 558 | html_table = "`bible_" + lang.id + "_html`"; 559 | morphological_table = "`morphological_" + lang.id + "`"; 560 | ///NOTE: Currently, the first query does not specifiy a verse. 561 | initial = !Boolean(start_at); 562 | 563 | /// Create the first part of the SQL/SphinxQL query. 564 | query = "SELECT " + morphological_table + ".id, " + morphological_table + ".verseID, " + html_table + ".words FROM " + morphological_table + ", " + html_table + " WHERE " + html_table + ".id = " + morphological_table + ".verseID AND " + morphological_table + ".query = \"" + db_client.escape_sphinx(query_arr[0]) + ";limit=" + lang.minimum_desired_verses + ";ranker=none"; 565 | 566 | /// Should the query start somewhere in the middle of the Bible? 567 | if (start_at) { 568 | ///NOTE: By keeping all of the settings in the Sphinx query, Sphinx can preform the best optimizations. 569 | /// Another less optimized approach would be to use the database itself to filter the results like this: 570 | /// ...WHERE id >= start_at AND query="...;limit=9999999" LIMIT lang.minimum_desired_verses 571 | query += ";minid=" + start_at; 572 | } 573 | 574 | /// Create the filter from the query. 575 | /// Examples: 576 | /// ... AS RED => ["...", [[3, 1]], [0]] => "...;filter=red,1" 577 | /// ... AS NOT RED => ["...", [[3, 1]], [1]] => "...;!filter=red,1" 578 | /// ... AS NOT RED, THIRD_PERSON, PLURAL => ["...", [[3, 1], [6, 3], [5, 2]],[1, 0, 0]] => "...;filter=number,2;filter=person,3;!filter=red,1" 579 | ///NOTE: The filters are added in reverse order simply to make the loop simpler. 580 | for (i = query_arr[1].length - 1; i >= 0; i -= 1) { 581 | query += ";" + (query_arr[2][i] ? "!" : "") + "filter=" + lang.grammar_categories[query_arr[1][i][0]] + "," + query_arr[1][i][1]; 582 | } 583 | 584 | if (initial) { 585 | /// Initial queries need to calculate the total verse. 586 | ///NOTE: SphinxSE does not return statistics by default, but we can retrieve them by running another query immediately after the first 587 | /// on the INFORMATION_SCHEMA.SESSION_STATUS table and UNION'ing it to the first. 588 | /// The only draw back to this is that both queries must have the same number of columns. 589 | /// Other ways to get the statistics is with the the following queries: 590 | /// SHOW ENGINE SPHINX STATUS; 591 | /// +--------+-------+-------------------------------------------------+ 592 | /// | Type | Name | Status | 593 | /// +--------+-------+-------------------------------------------------+ 594 | /// | SPHINX | stats | total: 421, total found: 421, time: 1, words: 1 | 595 | /// | SPHINX | words | love:421:498 | 596 | /// +--------+-------+-------------------------------------------------+ 597 | /// 598 | /// SHOW STATUS LIKE 'sphinx_%'; 599 | /// +--------------------------------+--------------+ 600 | /// | Variable_name | Value | 601 | /// +--------------------------------+--------------+ 602 | /// | sphinx_error_commits | 0 | 603 | /// | sphinx_error_group_commits | 0 | 604 | /// | sphinx_error_snapshot_file | | 605 | /// | sphinx_error_snapshot_position | 0 | 606 | /// | sphinx_time | 1 | 607 | /// | sphinx_total | 421 | 608 | /// | sphinx_total_found | 421 | 609 | /// | sphinx_word_count | 1 | 610 | /// | sphinx_words | love:421:498 | 611 | /// +--------------------------------+--------------+ 612 | /// 613 | /// However, because these queries are SHOW queries and not SELECT queries, they must be executed after the initial SELECT query. 614 | /// 615 | ///NOTE: The first two columns are currently ignored. 616 | query += "\" UNION SELECT 0, 0, VARIABLE_VALUE FROM INFORMATION_SCHEMA.SESSION_STATUS WHERE VARIABLE_NAME = 'sphinx_total_found'"; 617 | } else { 618 | query += "\""; 619 | } 620 | 621 | /// Run the Sphinx search, and return both the verse IDs and the HTML. 622 | db_client.query(query, function (data) 623 | { 624 | var i, 625 | len, 626 | res = { 627 | i: [], 628 | n: [], 629 | v: [] 630 | }, 631 | verse_count = 0; 632 | 633 | /// Was there no response from the database? This could mean the database or Sphinx crashed. 634 | if (!data) { 635 | /// Send an empty response, and exit. 636 | callback({}); 637 | return; 638 | } 639 | 640 | if (initial) { 641 | /// Because all of the columns share the same name when using UNION, the total verses found statistic is in the "words" column. 642 | res.t = Number(data.pop().words); 643 | } 644 | 645 | len = data.length; 646 | 647 | for (i = 0; i < len; i += 1) { 648 | res.i[i] = Number(data[i].id); 649 | /// Because Sphinx is searching at the word level, it might return multiple verses, so only add non-duplicate verses. 650 | if (res.n[verse_count - 1] !== Number(data[i].verseID)) { 651 | res.n[verse_count] = Number(data[i].verseID); 652 | res.v[verse_count] = data[i].words; 653 | verse_count += 1; 654 | } 655 | } 656 | 657 | callback(res); 658 | }); 659 | }; 660 | 661 | /** 662 | * Retrieve lexical data from the database. 663 | * 664 | * @example BF.standard_search({q: "1"}, function (data) {}); /// Get the lexical data for the first word in the english Bible. 665 | * @param data (object) An object containing the query and query options. 666 | * Object structure: 667 | * q: "The word ID (in the translated language)", 668 | * l: "The language ID" (optional) (default: "en") 669 | * @param callback (function) The function to send the results to. 670 | * @return An object containing the results (if any). 671 | * Object structure: 672 | * word (string) The original Greek, Hebrew, or Aramaic word, in Unicode. 673 | * pronun (string) A JSON string containing the pronunciation of the word (same as data.pronun below except for the actual word, not the base form). 674 | * strongs (integer) The designated Strong's number for that word. 675 | * base_word (string) The original Greek, Hebrew, or Aramaic base form of the word, in Unicode. 676 | * data (string) A JSON object containing the lexical information about the word. 677 | * Object structure: 678 | * def: {lit: "The literal definition of a word (especially a name)", 679 | * long: ["A multi-dimensional array of various possible definitions"], 680 | * short: "A short and simple definition"} 681 | * deriv: "Information about words this word is derived from", 682 | * pronun: {ipa: "IPA Biblical reconstructed pronunciation (base form)", 683 | * ipa_mod: "IPA modern pronunciation (base form)", 684 | * dic: "Dictionary Biblical reconstructed pronunciation (base form)", 685 | * dic_mod: "Dictionary modern pronunciation (base form)", 686 | * sbl: "The Society of Biblical Literature's phonemic transliteration"} 687 | * see: ["An array of Strong's numbers identifying additional words of interest."] 688 | * comment: "A string containing additional useful information" 689 | * usage (string) A list of ways the word is translated. 690 | * @note The usage data is planned to be completely redone and expanded. 691 | * @note Also, any number of the following: 692 | * part_of_speech, declinability, case_5, number, gender, degree, tense, voice, mood, person, middle, transitivity, miscellaneous, noun_type, numerical, form, dialect, type, pronoun_type 693 | */ 694 | BF.lexical_lookup = function (data, callback) 695 | { 696 | /// Select the language object specified by the query or use the default. 697 | var bible_table, 698 | lang = BF.langs[data.l] || BF.langs.en, 699 | query, 700 | word_id = Number(data.q); 701 | 702 | bible_table = "`bible_" + lang.id + "`"; 703 | 704 | /// Is it an Old Testament word? 705 | if (word_id < lang.divisions.nt) { 706 | query = "SELECT `bible_original`.word, `bible_original`.pronun, `lexicon_hebrew`.strongs, `lexicon_hebrew`.base_word, `lexicon_hebrew`.data, `lexicon_hebrew`.usage FROM " + bible_table + ", `bible_original`, `lexicon_hebrew`, `morphology` WHERE " + bible_table + ".id = " + word_id + " AND `bible_original`.id = " + bible_table + ".orig_id AND lexicon_hebrew.strongs = `bible_original`.strongs LIMIT 1"; 707 | } else { 708 | query = "SELECT `bible_original`.word, `bible_original`.pronun, `lexicon_greek`.strongs, `lexicon_greek`.base_word, `lexicon_greek`.data, `lexicon_greek`.usage, `morphology`.part_of_speech, `morphology`.declinability, `morphology`.case_5, `morphology`.number, `morphology`.gender, `morphology`.degree, `morphology`.tense, `morphology`.voice, `morphology`.mood, `morphology`.person, `morphology`.middle, `morphology`.transitivity, `morphology`.miscellaneous, `morphology`.noun_type, `morphology`.numerical, `morphology`.form, `morphology`.dialect, `morphology`.type, `morphology`.pronoun_type FROM " + bible_table + ", `bible_original`, `lexicon_greek`, `morphology` WHERE " + bible_table + ".id = " + word_id + " AND `bible_original`.id = " + bible_table + ".orig_id AND lexicon_greek.strongs = `bible_original`.strongs AND `morphology`.id = `bible_original`.id LIMIT 1"; 709 | } 710 | 711 | ///FIXME: Currently, BibleForge links words to the lexicon by Strong's numbers; however, this is too simplistic because some Strong's numbers have multiple entries. 712 | /// So, there needs to be another identifier. 713 | BF.db.query(query, function (data) 714 | { 715 | /// Was there no response from the database? This could mean the database crashed. 716 | if (!data || !data.length) { 717 | /// Send an empty response, and exit. 718 | callback({}); 719 | return; 720 | } 721 | 722 | ///NOTE: Currently, only one results is requested, so it can simply send data[0]. 723 | /// In the future, it should return multiple results for some words (e.g., hyphenated words, phrases translated as one word). 724 | 725 | callback(data[0]); 726 | }); 727 | }; 728 | 729 | 730 | /// ******************************* 731 | /// * Prepare to start the server * 732 | /// ******************************* 733 | 734 | /** 735 | * Load the language specific files and start the server. 736 | */ 737 | (function () 738 | { 739 | ///NOTE: Since the server cannot start until this is done, async only slows things down. 740 | var files = require("fs").readdirSync(BF.config.static_path + "js/lang"), 741 | i, 742 | id, 743 | lang; 744 | 745 | /// Pepare the langs object for the languages to attach to. 746 | BF.langs = {}; 747 | 748 | for (i = files.length - 1; i >= 0; i -= 1) { 749 | lang = require(BF.config.static_path + "js/lang/" + files[i]).BF.langs; 750 | ///NOTE: Object.keys() ignores prototypes, so there is no need for hasOwnProperty(). 751 | id = Object.keys(lang)[0]; 752 | BF.langs[id] = lang[id]; 753 | } 754 | 755 | /** 756 | * Listen for HTTP forwarded requests. 757 | */ 758 | (function start_server() 759 | { 760 | /** 761 | * Create a closure to handle queries from the client. 762 | * 763 | * @return A function to handle queries. 764 | */ 765 | var handle_query = (function () 766 | { 767 | /** 768 | * Create a closure to house the code to produce the non-JavaScript version. 769 | * 770 | * @return A function to create the HTML for the non-JavaScript version. 771 | */ 772 | var create_simple_page = (function () 773 | { 774 | /** 775 | * Create a closure to get the base HTML of the non-JavaScript code. 776 | * 777 | * @return A function to get the HTML for the non-JavaScript version. 778 | */ 779 | var get_simple_html = (function () 780 | { 781 | /// Prepare a variable in the closure to cache the results. 782 | var html; 783 | 784 | /** 785 | * Get the base HTML of the non-JavaScript code. 786 | * 787 | * @param callback (function) The function to send the HTML back to. 788 | * @return NULL 789 | * @note The callback function could be called synchronously or asynchronously. 790 | */ 791 | return function get_simple_html(callback) 792 | { 793 | /// Has the HTML already been cached? 794 | if (html) { 795 | callback(html); 796 | } else { 797 | /// Asynchronously read the file. 798 | require("fs").readFile(__dirname + "/index_non-js.html", "utf8", function (err, data) 799 | { 800 | if (err) { 801 | ///TODO: Log errors. 802 | console.error(err); 803 | } 804 | /// Is BibleForge configued to cache the results? 805 | ///NOTE: Production servers should use the cache. 806 | if (BF.config.cache_simple_html) { 807 | /// Optionally, cache the HTML in the closure. 808 | html = data; 809 | } 810 | 811 | callback(data); 812 | }); 813 | } 814 | }; 815 | }()); 816 | 817 | /** 818 | * Create the non-JavaScript version and send the results to the client. 819 | * 820 | * @param url (string) The URL from which to create the query. 821 | * @param data (object) The GET/POST data as an object. 822 | * @param connection (object) The connection object though which data may be sent back to the client. 823 | */ 824 | return function create_simple_page(url, data, connection, info) 825 | { 826 | /// Because the URI starts with a slash (/), the first array element is empty. 827 | var full_featured_uri, 828 | lang, 829 | query, 830 | /// Separate the URL to possibly obtain the language and query. 831 | ///NOTE: Three matches are possibly returned because the leading slash (/) counts as one black result. 832 | /// I.e., "/en/love/".split("/", 3) returns ["", "en", "love"]. 833 | query_arr = url.path.split("/", 3); 834 | 835 | /// First, parse the query array for valid langauges and searches. 836 | /// Example queries: 837 | /// /! 838 | /// /en/! 839 | /// /en/love/! 840 | /// /en_em/Romans 3:16/! 841 | /// /love/! 842 | /// /Romans 3:16/! 843 | 844 | /// Is the first parameter a valid language ID? 845 | if (BF.langs[query_arr[1]]) { 846 | /// Example queries: 847 | /// /en/! 848 | /// /en/love/! 849 | lang = BF.langs[query_arr[1]]; 850 | /// Is the second parameter a query? 851 | ///NOTE: If the last parameter is simply a question mark, it is not a valid query and indicates the switch to the non-JavaScript version. 852 | if (query_arr[2] && query_arr[2] !== "!") { 853 | /// Example query: /en/love/! 854 | query = query_arr[2]; 855 | } 856 | } else { 857 | /// Since there was no language specified, use the default language. 858 | ///TODO: Determine how to determine the default language. 859 | lang = BF.langs.en; 860 | /// Since the first parameter was not a language ID, the first parameter should be the query (if present). 861 | /// Is the first parameter a query? 862 | ///NOTE: If the first parameter not a valid language ID, the first parameter is treated as the query and any other parameters are discarded. 863 | ///NOTE: If the last parameter is simply a question mark, it is not a valid query and indicates the switch to the non-JavaScript version. 864 | if (query_arr[1] && query_arr[1] !== "!") { 865 | /// Example query: /love/! 866 | query = query_arr[1]; 867 | } 868 | } 869 | 870 | /// Was there no query specified in the URL? 871 | /// Example queries: 872 | /// /! 873 | /// /en/! 874 | if (query === undefined || query === "") { 875 | /// Get the queried language. 876 | if (data && data.l && BF.langs[data.l]) { 877 | lang = BF.langs[data.l]; 878 | } 879 | /// Was there a query specified in the GET data? 880 | ///NOTE: For example, this will occur when submitting a query from the query box in the non-JavaScript version. 881 | /// Example query: /en/!?q=love 882 | if (data && data.q) { 883 | query = data.q; 884 | } else { 885 | /// If there is no query present, then preform a verse lookup starting at the beginning of the Bible (e.g., Genesis 1:1). 886 | query = lang.books_short[1] + " 1:1"; 887 | } 888 | } else { 889 | /// Convert special symbols to normal ones (e.g., "%26" becomes "&"). 890 | query = global.decodeURIComponent(query); 891 | } 892 | 893 | /// Create the URL to the full-featured page, used to possibly redirect proper browsers to. 894 | ///NOTE: A scenario where this could be used is if someone using the non-JavaScript version sends a link to someone with a browser capable of handing the full-featured page. 895 | ///NOTE: Both the leading and trailing slashes (/) are necessary. 896 | full_featured_uri = "/" + lang.id + "/" + global.encodeURIComponent(query) + "/"; 897 | 898 | /// If a query string is present, redirect it to the correct URL and cloes the connection. 899 | ///TODO: Check for the presence of both the exclamation point (!) and _escaped_fragment_ and redirect to a page without the exclamation point. 900 | ///TODO: Retrieve any query in the _escaped_fragment_ variable. 901 | if (data && (data.q || data.l)) { 902 | connection.writeHead(301, {"Location": "http" + (BF.config.use_ssl ? "s" : "") + "://" + url.host + (Number(url.port) !== 80 ? ":" + url.port : "") + full_featured_uri + "!"}); 903 | connection.end(); 904 | return; 905 | } 906 | 907 | /// Now that we know the request will not be redirected, send the OK status code and appropriate header. 908 | connection.writeHead(200, {"Content-Type": "text/html"}); 909 | 910 | /** 911 | * Create the page based on the retrieved HTML and send it to the client. 912 | * 913 | * @param html (string) The HTML of the non-JavaScript version. 914 | * @note The callback function could be called synchronously or asynchronously. 915 | */ 916 | get_simple_html(function (html) 917 | { 918 | var b, 919 | c, 920 | content = {}, 921 | lang_css_html = "", /// If there is no language specific CSS, a blank string is needed. 922 | lang_select = " element that lists the available languages. 1150 | Object.keys(BF.langs).sort().forEach(function (lang_id) 1151 | { 1152 | /// Create the drop down box. 1153 | lang_select += ""; 1154 | /// Create links for the footer for SEO purposes primarily. 1155 | content.FOOTER += "" + BF.langs[lang_id].full_name + "
"; 1156 | }); 1157 | content.LANG_SELECT = lang_select + ""; 1158 | }); 1159 | }; 1160 | }()); 1161 | 1162 | /** 1163 | * Handle all incomming requests. 1164 | * 1165 | * @param url (object) The parsed URL. 1166 | * Object structure: 1167 | * host: "The server (e.g., 'bibleforge.com')", 1168 | * path: "The URL path (e.g., '/api'), 1169 | * port: "The port number (as a string) (e.g., '80')" 1170 | * @param data (object) The GET data. 1171 | * @param connection (object) The object used to communicate with the client. 1172 | * Object structure: 1173 | * end: function (data, encoding) 1174 | * writeHead: function (statusCode, headers) 1175 | * @param headers (object) The HTTP headers from the request. 1176 | */ 1177 | return function handle_query(url, data, connection, headers) 1178 | { 1179 | var send_results; 1180 | 1181 | /// Is the request for the APIs? 1182 | if (url.path === "/api") { 1183 | /// Send the proper header. 1184 | connection.writeHead(200, {"Content-Type": "application/json"}); 1185 | 1186 | /** 1187 | * Send the results back to the client as a JSON string. 1188 | * 1189 | * @param data (object) The data to be sent back to the client. 1190 | */ 1191 | send_results = function (data) 1192 | { 1193 | connection.end(JSON.stringify(data)); 1194 | }; 1195 | 1196 | switch (Number(data.t)) { 1197 | case BF.consts.verse_lookup: 1198 | BF.verse_lookup(data, send_results); 1199 | break; 1200 | case BF.consts.standard_search: 1201 | BF.standard_search(data, send_results); 1202 | break; 1203 | case BF.consts.grammatical_search: 1204 | BF.grammatical_search(data, send_results); 1205 | break; 1206 | case BF.consts.lexical_lookup: 1207 | BF.lexical_lookup(data, send_results); 1208 | break; 1209 | default: 1210 | if (data.t === "email") { 1211 | BF.email.send_user_message(data, send_results); 1212 | } else { 1213 | /// The request type was invalid, so close the connection. 1214 | connection.end(); 1215 | } 1216 | } 1217 | } else { 1218 | /// All other requests are replied to with the non-Javascript version. 1219 | create_simple_page(url, data, connection, {is_bot: BF.is_bot(headers["user-agent"])}); 1220 | } 1221 | }; 1222 | }()); 1223 | 1224 | /** 1225 | * Start the server. 1226 | */ 1227 | (function () 1228 | { 1229 | var url = require("url"), 1230 | qs = require("querystring"); 1231 | 1232 | /// HTTP is Node 0.10- is broken. This mitigates the problem. 1233 | /// See https://github.com/LearnBoost/knox/commit/0bc57294e1bf7b4526ce9f51aee6553bac77cebc. 1234 | require("http").globalAgent.maxSockets = 99999; 1235 | 1236 | /** 1237 | * Finally create the server. 1238 | * 1239 | * @param request (object) The object containing info about the request. 1240 | * @param response (object) The object used to communicate back to the client. 1241 | */ 1242 | require("http").createServer(function (request, response) 1243 | { 1244 | /// Give an object with a subset of the response's functions. 1245 | var connection = { 1246 | /** 1247 | * Close the connection to the client and optionally write a final message. 1248 | * 1249 | * @param data (string OR buffer) (optional) The final message to write to the client. 1250 | * @param encoding (string) (optional) The encoding of the data. 1251 | * @note This must be called in order for the client to finish the request. 1252 | */ 1253 | end: function (data, encoding) 1254 | { 1255 | response.end(data, encoding); 1256 | }, 1257 | /** 1258 | * Write the header to the client. 1259 | * 1260 | * @example connection.writeHead(200, {"Content-Type": "text/html"}); 1261 | * @param statusCode (number) The HTTP status code (e.g., 200 for "OK"; 404 for "File not found") 1262 | * @param headers (object) An object containing the headers to be sent. 1263 | * @note This must be called in order for the client to finish the request. 1264 | */ 1265 | writeHead: function (statusCode, headers) 1266 | { 1267 | response.writeHead(statusCode, headers); 1268 | } 1269 | }, 1270 | /// Get the original URI that is being requested and parse it. 1271 | ///NOTE: Use the X-Request-URI header if present because sometimes the original URL gets modified (e.g., a request to /en/love/ is redirected to /api). 1272 | url_parsed = url.parse(request.headers["x-request-uri"] || request.headers.url || request.url); 1273 | 1274 | /// Is there GET data? 1275 | ///TODO: Merge POST data with GET data. 1276 | if (request.method.toUpperCase() === "GET") { 1277 | handle_query({host: request.headers.host, path: url_parsed.pathname, port: request.headers.port}, qs.parse(url_parsed.query), connection, request.headers); 1278 | } else { 1279 | ///TODO: Also handle POST data. 1280 | /// If there is no data, close the connection. 1281 | connection.end(); 1282 | } 1283 | }).listen(BF.config.port); 1284 | }()); 1285 | }()); 1286 | }()); 1287 | --------------------------------------------------------------------------------