├── LICENSE ├── README.md ├── config.json ├── editor └── scripts.js ├── index.html ├── lib ├── alertdialog.js ├── app.css ├── app.js ├── appprefs.js ├── askdialog.js ├── buildrss.js ├── confirmdialog.js ├── ga.js ├── menus.css ├── menus.js ├── strftime.js └── utils.js ├── scripts.js ├── styles.css └── templates ├── default.html ├── scripts.js └── styles.css /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dave Winer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # myWordEditor 2 | 3 | A simple silo-free blogging tool that creates beautiful essay pages. 4 | 5 | Here's an example of the kind of page you can create with MyWord Editor. 6 | 7 | And the blog post announcing the release of MyWord Editor in open source. 8 | 9 | #### Demo app 10 | 11 | Here's the demo app for this project, MyWord Editor. 12 | 13 | You can set up your own blogging environment and use it to host your writing and for other people, your friends, colleagues, members of a club, whatever you like. 14 | 15 | My name is Dave Winer. I blog at Scripting News. I wrote some of the earliest blogging, RSS and podcasting software. 16 | 17 | #### It's radical software 18 | 19 | These days blogging tools try to lock you into their business model, and lock other developers out. I have the freedom to do what I want, so I decided to take the exact opposite approach. I don't want to operate a free public blogging tool and lock users in and make them dependent on me. Instead, I want to learn from thinkers and writers and developers. I want to engage with other minds. Making money, at this stage of my career, is not so interesting to me. I'd much rather make ideas, and new working relationships, and friends. 20 | 21 | So MyWord Editor is radically silo-free. 22 | 23 | #### How to clone this 24 | 25 | This app runs on the user's computer, in a browser. It's a JavaScript app. It communicates via a simple API, with a nodeStorage server, that uses Twitter for identity and Amazon S3 for storage. That server, like the app, is open source. The first part of creating your own blogging system is to set up a nodeStorage server. 26 | 27 | Once you've done that, download the contents of the myWordEditor repository, and put it in a folder that's publicly accessible over the web. I like using S3 buckets, but you could put it in a folder on an Apache or nginx server. 28 | 29 | Edit the config.json file so that the urlTwitterServer string is the URL of your nodeStorage server. 30 | 31 | You can also change the default image for your users' blog posts, or you can leave it as-is, with a nice picture from Wikipedia of the Grateful Dead. 32 | 33 | Test the installation by logging on. Follow the instructions on the MyWord Editor blog. 34 | 35 | #### Updates 36 | 37 | ##### v0.73 -- 9/19/17 by DW 38 | 39 | Changed URLs in includes in <head> section of index.html to not be specific to HTTP, so that people can run their nodeStorage servers behind HTTPS. 40 | 41 | ##### v0.72 -- 7/27/15 by DW 42 | 43 | Added strikethrough button to the toolbar, demonstrating that we can add items to the toolbar. They come from this list. 44 | 45 | ##### v0.71 -- 7/27/15 by DW 46 | 47 | Add Facebook and Twitter metadata to the <head> section of the home page. 48 | 49 | Changed the default value of the flAutoPublish setting from false to true. This is the behavior I demo in the video, and it seems to be what people expect. 50 | 51 | ##### v0.70 -- 7/25/15 by DW 52 | 53 | By default Disqus comments are now on. You can turn them off in the Settings dialog. 54 | 55 | ##### v0.69 -- 7/25/15 by DW 56 | 57 | Added rssCloud support to the RSS feed produced by MWE. 58 | 59 | The feature can be turned on in config.json, if you're running your own MWE. 60 | 61 | I'm using Andrew Shell's rssCloud server for notification, rsscloud.io. 62 | 63 | Here's my RSS feed, as an example. 64 | 65 | ##### v0.67 -- 7/24/15 by DW 66 | 67 | We now have a beautiful editor, integrated, medium-editor. Problem solved. :-) 68 | 69 | You can read about the update in this note, and a blog post. 70 | 71 | ##### v0.64 -- 4/3/15 by DW 72 | 73 | Run config.startupCode at startup. See blog post for details. 74 | 75 | ##### v0.63 -- 4/2/15 by DW 76 | 77 | Took a step toward editor plug-ins. 78 | 79 | ##### v0.62 -- 4/1/15 by DW 80 | 81 | Support for Disqus comments. See this blog post for details. 82 | 83 | ##### v0.61 -- 3/31/15 by DW 84 | 85 | Add scalars from appConsts and appPrefs to the pagetable in the rendered page. This allows scripts running in the page to know the title of the site, the version of MWE that created the page, etc. 86 | 87 | In the title of essay pages, we use the author's name if it's available instead of the name of the product. So a story might say "Jordan Jones: What I want for Christmas" instead of "MyWord Editor: What I want for Christmas". 88 | 89 | ##### v0.60 -- 3/30/15 by DW 90 | 91 | This is the first release of MWE with template support. Be sure to read the note about breakage on that page. Any work you do with templates now is likely to break. 92 | 93 | ##### v0.59 -- 3/27/15 by DW 94 | 95 | First source release of the JavaScript code and CSS styles used in the rendered pages. Explained in this blog post. 96 | 97 | ##### v0.58 -- 3/27/15 by DW 98 | 99 | The *Open file* command in the Editor menu is replaced by a new History menu. Explained in this blog post. 100 | 101 | ##### v0.57 -- 3/26/15 by DW 102 | 103 | There were lots of debugging calls to console.log that displayed huge data structures. I quieted them down, so we get simpler readouts from the console, now that this code isn't so new. Changed the urlTemplateFile constant to be a relative URL, relative to the folder MWE is running from. That way I can change the template on my server without changing it on everyone's. Still more of this kind of factoring to do. Must move carefully (slowly). 104 | 105 | ##### v0.56 -- 3/26/15 by DW 106 | 107 | New supported value in config.json -- googleAnalyticsAccount. If specified, we use it in ga.js to make calls to Google Analytics. 108 | 109 | ##### v0.55 -- 3/26/15 by DW 110 | 111 | Change notes in this blog post. 112 | 113 | ##### v0.54 -- 3/24/15 by DW 114 | 115 | A new command in the Editor menu: Publish all posts. After confirmation, it opens each of your posts and does exactly what clicking on the Publish button would do. I added this feature because I wanted a quick way to re-generate all the files. It'll be useful if there's a template change, or other change that requires a complete rebuild of a blog. 116 | 117 | Also improved the error dialog on startup, if there was an error connecting to the server, it would report the problem as the user not being whitelisted. Usually the problem is the URL of the nodeStorage server was not correctly specified. I got bit by this myself. We needed a better message here. 118 | 119 | ##### v0.53 -- 3/24/15 by DW 120 | 121 | In addition to generating an HTML file for each essay, we also generate a JSON file. Example. I think this will be generally useful, I want to use it immediately to try to create a home page essay browser, using snap.js. There's a corresponding element in the RSS feed, called <source:linkJson>. 122 | 123 | #### Support 124 | 125 | If you're a developer, or running a server, please ask questions on the server-snacks mail list. 126 | 127 | If you're a blogger, and need help writing with MWE, please join the myword-editor mail list. 128 | 129 | In either case, please read the docs and scratch your head at least a little before asking for help. 130 | 131 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "urlTwitterServer": "http://twitter.myword.io/", 3 | "urlDefaultImage": "http://scripting.com/2015/03/21/gratefulDead.png", 4 | "googleAnalyticsAccount": "UA-39531990-1", 5 | "templates": { 6 | "Default": "http://myword.io/templates/default.html", 7 | "Plain": "http://myword.io/templates/plain/template.html" 8 | }, 9 | "rssCloud": { 10 | "enabled": true, 11 | "domain": "rpc.rsscloud.io", 12 | "port": 5337, 13 | "path": "/pleaseNotify" 14 | }, 15 | "startupCode": "console.log (\"Hello from config.startupCode.\");" 16 | } 17 | -------------------------------------------------------------------------------- /editor/scripts.js: -------------------------------------------------------------------------------- 1 | //stuff for plain editor 2 | function getEditorText (s) { 3 | return ($("#idTextArea").val ()); 4 | } 5 | function setEditorText (s) { 6 | $("#idTextArea").val (s); 7 | } 8 | function initEditor () { 9 | var placeholder = "Obviously this is a good place to write something."; 10 | $("#idEditorContainer").append (''); 11 | } 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | MyWord Editor 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 |
60 | 123 |
124 |
125 |
126 | 178 |
179 |
180 | 188 | 197 | 205 |
206 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /lib/alertdialog.js: -------------------------------------------------------------------------------- 1 | var flAlertDialogSetUp = false; 2 | 3 | function setupAlertDialog () { 4 | var s = 5 | "
\"alert
OK
" 6 | $("body").prepend (s); 7 | $("#idAlertDialog").on ("keydown", function (event) { //5/6/13 by DW 8 | if (event.which == 13) { 9 | okAlertDialog (); 10 | return (false); 11 | } 12 | }); 13 | return (s); 14 | } 15 | function okAlertDialog () { 16 | $("#idAlertDialog").modal ('hide'); 17 | }; 18 | function alertDialog (prompt) { 19 | if (!flAlertDialogSetUp) { 20 | setupAlertDialog (); 21 | flAlertDialogSetUp = true; 22 | } 23 | document.getElementById ("idAlertDialogPrompt").innerHTML = prompt; 24 | $("#idAlertDialog").modal ("show"); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /lib/app.css: -------------------------------------------------------------------------------- 1 | /* prefs */ 2 | .divPrefsDialog { 3 | font-family: Arial; 4 | font-size: 16px; 5 | line-height: 140%; 6 | } 7 | .divPrefsDialog p { 8 | font-family: Arial; 9 | font-size: 16px; 10 | line-height: 140%; 11 | min-height: 10px; 12 | margin-bottom: 1.3em; 13 | } 14 | .divPrefsDialog label { 15 | display: inline; 16 | float: left; 17 | text-align: right; 18 | padding-top: 4px; 19 | width: 130px; 20 | margin-right: .5em; 21 | font-family: Arial; 22 | font-size: 16px; 23 | } 24 | .divPrefsDialog fieldset { /* 4/30/13 by DW */ 25 | margin-left: 5px; 26 | margin-right: 5px; 27 | } 28 | .divPrefsDialog .clearfix { 29 | margin-bottom: 18px; 30 | } 31 | .divPrefsDialog .uneditable-input { 32 | -moz-box-shadow: 0; 33 | border: none; 34 | } 35 | .divPrefsDialog .divPrefsCheckbox { 36 | margin-left: 140px; 37 | margin-top: -2px; 38 | } 39 | .divPrefsDialog .divPrefsRadioList { 40 | margin-left: 140px; 41 | line-height: 200%; 42 | } 43 | .divPrefsDialog h3 { 44 | margin-top: 10px; 45 | margin-bottom: 6px; 46 | } 47 | .divPrefsDialog .divPrefsCheckbox input { 48 | margin-top: 0; 49 | } 50 | .divPrefsDialog textarea { 51 | width: 530px; 52 | height: auto; 53 | display: inline-block; 54 | } 55 | .divPrefsDialog .row { 56 | line-height: 24px; 57 | margin-bottom: 12px; 58 | } 59 | .divPrefsDialog red { 60 | color: red; 61 | } 62 | .divPrefsDialog h4 { 63 | font-size: 22px; 64 | padding-top: 15px; 65 | padding-bottom: 12px; /* 4/12/12 DW -- increased from 5px */ 66 | } 67 | 68 | .divPrefsDialog .xlarge { 69 | width: 530px; 70 | font-size: 16px; 71 | height: auto; 72 | } 73 | .divPrefsDialog input { 74 | margin-bottom: 0; 75 | } 76 | 77 | .divPrefsDialog .nav { 78 | margin-bottom: 18px; 79 | } 80 | .divDialogElements input { 81 | font-size: 18px; 82 | padding: 3px; 83 | height: 32px; 84 | } 85 | .divDialogElements .xlarge { 86 | width: 530px; 87 | font-size: 16px; 88 | height: auto; 89 | } 90 | .divDialogElements .numberPrefsInput { 91 | width: 50px; 92 | } 93 | .divDialogElements textarea { 94 | width: 530px; 95 | height: auto; 96 | display: inline-block; 97 | font-size: 16px; 98 | } 99 | .divDialogElements input[type="checkbox"] { 100 | margin-right: 3px; 101 | margin-top: -1px; 102 | } 103 | .divButton { 104 | padding-top: 12px; 105 | } 106 | .divPrefsDialog .modal { 107 | left: 40%; 108 | width: 740px; 109 | } 110 | .divPrefsDialog .btn { 111 | width: 80px; 112 | margin-left: 5px; 113 | } 114 | .divPrefsDialog .ui-input-text, .divPrefsDialog .ui-checkbox { /* 4/11/13 by DW */ 115 | display: inline-block; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | function aboutDialog () { 2 | alertDialog (appConsts.productnameForDisplay + " v" + appConsts.version + "."); 3 | } 4 | 5 | -------------------------------------------------------------------------------- /lib/appprefs.js: -------------------------------------------------------------------------------- 1 | 2 | var pathAppPrefs = "appPrefs.json"; 3 | 4 | function prefsToStorage () { 5 | var jsontext = JSON.stringify (appPrefs), whenstart = new Date (); 6 | localStorage.appPrefs = jsontext; 7 | if (getBoolean (appPrefs.flServerBasedPrefs)) { 8 | twUploadFile (pathAppPrefs, jsontext, "application/json", true, function (data) { 9 | var archivepath = getDatePath (whenstart) + pathAppPrefs; 10 | twUploadFile (archivepath, jsontext, "application/json", true, function (data) { 11 | console.log ("prefsToStorage: uploaded \"" + archivepath + "\" to server in " + secondsSince (whenstart) + " secs."); 12 | }); 13 | }); 14 | } 15 | } 16 | function storageToPrefs (callback) { 17 | if (getBoolean (appPrefs.flServerBasedPrefs)) { 18 | var whenstart = new Date (); 19 | twGetFile (pathAppPrefs, true, true, function (error, data) { 20 | if (data != undefined) { 21 | var storedPrefs = JSON.parse (data.filedata); 22 | for (var x in storedPrefs) { 23 | appPrefs [x] = storedPrefs [x]; 24 | } 25 | console.log ("storageToPrefs: downloaded from server in " + secondsSince (whenstart) + " secs."); 26 | if (callback != undefined) { //8/16/14 by DW 27 | callback (); 28 | } 29 | } 30 | else { //call the callback even on an error 31 | if (callback != undefined) { 32 | var errorInfo = { 33 | flFileNotFound: false 34 | }; 35 | if (error.status == 500) { 36 | var s3response = JSON.parse (error.responseText); 37 | if (s3response.code == "NoSuchKey") { 38 | errorInfo.flFileNotFound = true; 39 | } 40 | } 41 | callback (errorInfo); 42 | } 43 | } 44 | }); 45 | } 46 | else { 47 | if (localStorage.appPrefs != undefined) { 48 | var storedPrefs = JSON.parse (localStorage.appPrefs); 49 | for (var x in storedPrefs) { 50 | appPrefs [x] = storedPrefs [x]; 51 | } 52 | } 53 | if (callback != undefined) { //11/6/14 by DW 54 | callback (); 55 | } 56 | } 57 | } 58 | function storageStartup (callback) { //11/7/14 by DW 59 | storageToPrefs (function (errorInfo) { 60 | var flStartupFail = false; 61 | if (errorInfo != undefined) { 62 | console.log ("storageStartup: errorInfo == " + jsonStringify (errorInfo)); 63 | if (errorInfo.flFileNotFound != undefined) { 64 | if (!errorInfo.flFileNotFound) { //some error other than file-not-found (which is a benign error, first-time user 65 | if (callback != undefined) { //startup fail 66 | callback (false); 67 | flStartupFail = true; 68 | } 69 | } 70 | } 71 | } 72 | if (!flStartupFail) { 73 | if (callback != undefined) { //good start 74 | callback (true); 75 | } 76 | } 77 | }); 78 | } 79 | function prefsToCookie () { 80 | prefsToStorage (); 81 | } 82 | function twitterToPrefs (twitterUserInfo) { //fill in RSS prefs from Twitter -- 8/7/14 by DW 83 | if (!appPrefs.flRssPrefsInitialized) { 84 | appPrefs.rssTitle = twitterUserInfo.name; 85 | appPrefs.rssDescription = twitterUserInfo.description; 86 | appPrefs.flRssPrefsInitialized = true; 87 | appPrefs.rssLink = twitterUserInfo.url; 88 | prefsToStorage (); 89 | twDerefUrl (twitterUserInfo.url, function (longUrl) { //try to unshorten the URL 90 | appPrefs.rssLink = longUrl; 91 | prefsToStorage (); 92 | }); 93 | } 94 | } 95 | function prefsDialogShow () { 96 | 97 | try { //6/7/14 by DW 98 | concord.stopListening (); //3/11/13 by DW 99 | } 100 | catch (err) { 101 | } 102 | 103 | $("#idPrefsDialog").modal ('show'); 104 | 105 | $("#idPrefsDialog").on ("keydown", function (event) { //1/26/15 by DW 106 | if (event.which == 13) { 107 | prefsOkClicked (); 108 | return (false); 109 | } 110 | }); 111 | }; 112 | function prefsCloseDialog () { 113 | 114 | try { //6/7/14 by DW 115 | concord.resumeListening (); //3/11/13 by DW 116 | } 117 | catch (err) { 118 | } 119 | 120 | $("#idPrefsDialog").modal ('hide'); 121 | }; 122 | 123 | 124 | function prefsOkClicked () { 125 | var inputs = document.getElementById ("idPrefsDialog").getElementsByTagName ("input"), i; 126 | for (var i = 0; i < inputs.length; i++) { 127 | if (inputs [i].type == "checkbox") { 128 | appPrefs [inputs [i].name] = inputs [i].checked; 129 | } 130 | else { 131 | appPrefs [inputs [i].name] = inputs [i].value; 132 | } 133 | } 134 | 135 | var textareas = document.getElementById ("idPrefsDialog").getElementsByTagName ("textarea"), i; 136 | for (var i = 0; i < textareas.length; i++) { 137 | appPrefs [textareas [i].name] = textareas [i].value; 138 | } 139 | 140 | prefsCloseDialog (); 141 | applyPrefs (); 142 | prefsToCookie (); 143 | }; 144 | function getStoredPrefs (callback) { //9/6/14 by DW 145 | var whenstart = new Date (); 146 | twGetFile (pathAppPrefs, true, true, function (error, data) { 147 | if (data != undefined) { 148 | var storedPrefs = JSON.parse (data.filedata); 149 | for (var x in storedPrefs) { 150 | appPrefs [x] = storedPrefs [x]; 151 | } 152 | console.log ("getStoredPrefs: downloaded from server in " + secondsSince (whenstart) + " secs."); 153 | if (callback != undefined) { //8/16/14 by DW 154 | callback (); 155 | } 156 | } 157 | else { //don't call the callback on an error, just put up an alert. hope the user follows our advice! ;-) 158 | if (callback != undefined) { 159 | var responsestruct = JSON.parse (error.responseText); 160 | alert ("Error connecting to server: \"" + responsestruct.message + "\" Please reload the page to try again."); 161 | } 162 | } 163 | }); 164 | } 165 | $(document).ready (function () { 166 | $("#idPrefsDialog").bind ('show', function () { 167 | var inputs = document.getElementById ("idPrefsDialog").getElementsByTagName ("input"), i; 168 | for (var i = 0; i < inputs.length; i++) { 169 | if (appPrefs [inputs [i].name] != undefined) { 170 | if (inputs [i].type == "checkbox") { 171 | inputs [i].checked = appPrefs [inputs [i].name]; 172 | } 173 | else { 174 | inputs [i].value = appPrefs [inputs [i].name]; 175 | } 176 | } 177 | } 178 | 179 | var textareas = document.getElementById ("idPrefsDialog").getElementsByTagName ("textarea"), i; 180 | for (var i = 0; i < textareas.length; i++) { 181 | if (appPrefs [textareas [i].name] != undefined) { 182 | textareas [i].value = appPrefs [textareas [i].name]; 183 | } 184 | } 185 | }); 186 | }); 187 | 188 | -------------------------------------------------------------------------------- /lib/askdialog.js: -------------------------------------------------------------------------------- 1 | var flAskDialogSetUp = false; 2 | var askDialogCallback; 3 | 4 | 5 | function okAskDialog () { 6 | var input = document.getElementById ("idAskDialogInput"); 7 | closeAskDialog (); 8 | askDialogCallback (input.value, false); 9 | }; 10 | function cancelAskDialog () { 11 | askDialogCallback ("", true); 12 | closeAskDialog (); 13 | } 14 | function setupAskDialog (callback) { 15 | if (flAskDialogSetUp) { 16 | callback (); 17 | } 18 | else { 19 | readHttpFile ("http://fargo.io/code/node/shared/askdialog.html", function (s) { 20 | $("body").prepend (s); 21 | $("#idAskDialogInput").on ("keydown", function (event) { //3/22/13 by DW 22 | if (event.which == 13) { 23 | okAskDialog (); 24 | return (false); 25 | } 26 | }); 27 | flAskDialogSetUp = true; 28 | callback (); 29 | }); 30 | } 31 | } 32 | function closeAskDialog () { 33 | $("#idAskDialog").modal ('hide'); 34 | }; 35 | function askDialog (prompt, defaultvalue, placeholder, askcallback, type) { 36 | var input; 37 | if (defaultvalue === undefined) { 38 | defaultvalue = ""; 39 | } 40 | if (placeholder === undefined) { 41 | placeholder = ""; 42 | } 43 | if (type === undefined) { 44 | type = "text"; 45 | } 46 | 47 | setupAskDialog (function () { 48 | input = document.getElementById ("idAskDialogInput"); 49 | if (defaultvalue === undefined) { 50 | defaultvalue = ""; 51 | } 52 | input.value = defaultvalue; 53 | input.type = type; 54 | input.placeholder = placeholder; 55 | askDialogCallback = askcallback; 56 | document.getElementById ("idAskDialogPrompt").innerHTML = prompt; 57 | $("#idAskDialog").on ("shown", function () { 58 | input.focus (); 59 | input.select (); 60 | }); 61 | $("#idAskDialog").modal ("show"); 62 | }); 63 | 64 | 65 | } 66 | 67 | -------------------------------------------------------------------------------- /lib/buildrss.js: -------------------------------------------------------------------------------- 1 | var urlGetRssEnclosureInfo = "http://pub2.fargo.io:5347/getEnclosureInfo?url="; 2 | 3 | function getRssEnclosureInfo (obj, callback) { 4 | var jxhr = $.ajax ({ 5 | url: urlGetRssEnclosureInfo + encodeURIComponent (obj.enclosure.url), 6 | dataType: "jsonp", 7 | timeout: 30000, 8 | jsonpCallback : "getData" 9 | }) 10 | .success (function (data, status) { 11 | if (data.flError != undefined) { //2/15/14 by DW 12 | obj.enclosure.flError = true; 13 | } 14 | else { 15 | obj.enclosure.type = data.type; 16 | obj.enclosure.length = data.length; 17 | if (callback != undefined) { 18 | callback (); 19 | } 20 | } 21 | }) 22 | .error (function (status) { 23 | console.log ("getEnclosureInfo: Error getting type and length -- " + jsonStringify (status)); 24 | obj.enclosure.flError = true; 25 | }); 26 | } 27 | function buildRssFeed (headElements, historyArray) { 28 | function encode (s) { 29 | var lines = encodeXml (s).split (String.fromCharCode (10)); 30 | var returnedstring = ""; 31 | for (var i = 0; i < lines.length; i++) { 32 | returnedstring += trimWhitespace (lines [i]); 33 | if (i < (lines.length - 1)) { 34 | returnedstring += " "; 35 | } 36 | } 37 | return (returnedstring); 38 | } 39 | function whenMostRecentTweet () { 40 | if (historyArray.length > 0) { 41 | return (new Date (historyArray [0].when)); 42 | } 43 | else { 44 | return (new Date (0)); 45 | } 46 | } 47 | function buildOutlineXml (theOutline) { 48 | function addOutline (outline) { 49 | var s = " 0); 52 | } 53 | function addAtt (name) { 54 | if (outline [name] != undefined) { 55 | s += " " + name + "=\"" + encode (outline [name]) + "\" "; 56 | } 57 | } 58 | addAtt ("text"); 59 | addAtt ("type"); 60 | addAtt ("created"); 61 | addAtt ("name"); 62 | 63 | if (hasSubs (outline)) { 64 | add (s + ">"); 65 | indentlevel++; 66 | for (var i = 0; i < outline.subs.length; i++) { 67 | addOutline (outline.subs [i]); 68 | } 69 | add (""); 70 | indentlevel--; 71 | } 72 | else { 73 | add (s + "/>"); 74 | } 75 | 76 | } 77 | addOutline (theOutline); 78 | return (xmltext); 79 | } 80 | function markdownProcess (s) { //7/24/15 by DW 81 | var md = new Markdown.Converter (), theList = s.split ("

"), markdowntext = ""; 82 | for (var i = 0; i < theList.length; i++) { 83 | var lt = theList [i]; 84 | if ((lt.length > 0) && (lt != "

") && (lt != "

")) { 85 | markdowntext += "

" + md.makeHtml (lt) + "

"; 86 | } 87 | } 88 | return (markdowntext); 89 | } 90 | var xmltext = "", indentlevel = 0, starttime = new Date (); nowstring = starttime.toGMTString (); 91 | var username = headElements.twitterScreenName, maxitems = headElements.maxFeedItems; 92 | function add (s) { 93 | xmltext += filledString ("\t", indentlevel) + s + "\n"; 94 | } 95 | function addAccount (servicename, username) { 96 | if ((username != undefined) && (username.length > 0)) { 97 | add ("" + encode (username) + ""); 98 | } 99 | } 100 | add ("") 101 | add ("") 102 | add (""); indentlevel++ 103 | add (""); indentlevel++; 104 | //add header elements 105 | add ("" + encode (headElements.title) + ""); 106 | add ("" + encode (headElements.link) + ""); 107 | add ("" + encode (headElements.description) + ""); 108 | add ("" + whenMostRecentTweet ().toUTCString () + ""); 109 | add ("" + nowstring + ""); 110 | add ("" + encode (headElements.language) + ""); 111 | add ("" + headElements.generator + ""); 112 | add ("" + headElements.docs + ""); 113 | 114 | // element -- 6/5/15 by DW 115 | if (headElements.flRssCloudEnabled) { 116 | add ("") 117 | } 118 | 119 | addAccount ("twitter", username); 120 | //add items 121 | var ctitems = 0; 122 | for (var i = 0; (i < historyArray.length) && (ctitems < maxitems); i++) { 123 | var item = historyArray [i], itemcreated = twTwitterDateToGMT (item.when), itemtext = encode (item.text); 124 | var linktotweet = encode ("https://twitter.com/" + username + "/status/" + item.idTweet); 125 | add (""); indentlevel++; 126 | if (item.title !== undefined) { //3/4/15 by DW 127 | add ("" + encode (item.title) + ""); 128 | } 129 | //description -- 3/26/15 by DW 130 | if (getBoolean (item.flMarkdown)) { 131 | if (getBoolean (item.flPgfLevelMarkdown)) { //7/24/15 by DW 132 | add ("" + encode (markdownProcess (item.text)) + ""); 133 | } 134 | else { 135 | add ("" + encode (new Markdown.Converter ().makeHtml (item.text)) + ""); 136 | } 137 | } 138 | else { 139 | add ("" + encode (item.text) + ""); 140 | } 141 | add ("" + itemcreated + ""); 142 | //link -- 8/12/14 by DW 143 | if (item.link != undefined) { 144 | add ("" + encode (item.link) + ""); 145 | } 146 | else { 147 | add ("" + linktotweet + ""); 148 | } 149 | //source:linkShort -- 8/26/14 by DW 150 | if (item.linkShort != undefined) { 151 | add ("" + encode (item.linkShort) + ""); 152 | } 153 | //guid -- 8/12/14 by DW 154 | if (item.guid != undefined) { 155 | if (getBoolean (item.guid.flPermalink)) { 156 | add ("" + encode (item.guid.value) + ""); 157 | } 158 | else { 159 | add ("" + encode (item.guid.value) + ""); 160 | } 161 | } 162 | else { 163 | add ("" + linktotweet + ""); 164 | } 165 | //enclosure -- 8/11/14 by DW 166 | if (item.enclosure != undefined) { 167 | var enc = item.enclosure; 168 | if ((enc.url != undefined) && (enc.type != undefined) && (enc.length != undefined)) { 169 | add (""); 170 | } 171 | } 172 | //source:markdown -- 3/26/15 by DW 173 | if (getBoolean (item.flMarkdown)) { 174 | add ("" + itemtext + ""); 175 | } 176 | //source:jsonUrl -- 3/24/15 by DW 177 | if (item.linkJson !== undefined) { 178 | add ("" + encode (item.linkJson) + ""); 179 | } 180 | //source:outline 181 | if (item.outline != undefined) { //10/15/14 by DW 182 | buildOutlineXml (item.outline); 183 | } 184 | else { 185 | if (item.idTweet != undefined) { 186 | add (""); 187 | } 188 | if (item.enclosure != undefined) { //9/23/14 by DW 189 | var enc = item.enclosure; 190 | if (enc.type != undefined) { //10/25/14 by DW 191 | if (beginsWith (enc.type.toLowerCase (), "image")) { 192 | add (""); 193 | } 194 | } 195 | } 196 | } 197 | add (""); indentlevel--; 198 | ctitems++; 199 | } 200 | add (""); indentlevel--; 201 | add (""); indentlevel--; 202 | return (xmltext); 203 | } 204 | 205 | -------------------------------------------------------------------------------- /lib/confirmdialog.js: -------------------------------------------------------------------------------- 1 | var flConfirmDialogSetUp = false; 2 | var confirmDialogCallback; 3 | 4 | function setupConfirmDialog () { 5 | var s = 6 | "
" 7 | $("body").prepend (s) 8 | $("#idConfirmDialog").on ("keydown", function (event) { //5/6/13 by DW 9 | if (event.which == 13) { 10 | okConfirmDialog (); 11 | return (false); 12 | } 13 | }); 14 | return (s); 15 | } 16 | function closeConfirmDialog () { 17 | $("#idConfirmDialog").modal ('hide'); 18 | }; 19 | function okConfirmDialog () { 20 | $("#idConfirmDialog").modal ('hide'); 21 | confirmDialogCallback (); 22 | }; 23 | function confirmDialog (prompt, callback) { 24 | if (!flConfirmDialogSetUp) { 25 | setupConfirmDialog (); 26 | flConfirmDialogSetUp = true; 27 | } 28 | document.getElementById ("idConfirmDialogPrompt").innerHTML = prompt; 29 | confirmDialogCallback = callback; 30 | $("#idConfirmDialog").modal ("show"); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /lib/ga.js: -------------------------------------------------------------------------------- 1 | 2 | var whenLastGoogleAnalyticsPing = new Date (0); 3 | 4 | 5 | function haveGoogleAnalytics () { 6 | return ((appConsts.domain !== undefined) && (appConsts.googleAnalyticsAccount !== undefined)); 7 | } 8 | function initGoogleAnalytics () { 9 | if (haveGoogleAnalytics ()) { 10 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 11 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 12 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 13 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 14 | 15 | ga('create', appConsts.googleAnalyticsAccount, appConsts.domain); 16 | ga('send', 'pageview'); 17 | } 18 | } 19 | function pingGoogleAnalytics () { 20 | if (haveGoogleAnalytics ()) { 21 | if (secondsSince (whenLastGoogleAnalyticsPing) >= 300) { //ping google analytics every 5 minutes 22 | if (secondsSince (whenLastUserAction) <= 300) { //don't ping if the user isn't doing anything 23 | ga ("send", "pageview"); 24 | } 25 | whenLastGoogleAnalyticsPing = new Date (); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/menus.css: -------------------------------------------------------------------------------- 1 | .divMenubar { 2 | } 3 | .divMenubar .container { 4 | width: 840px; 5 | } 6 | .divMenubar .dropdown { 7 | font-family: Ubuntu; 8 | font-size: 16px; 9 | margin-top: 5px; 10 | margin-bottom: 5px; 11 | } 12 | .divMenubar .navbar .nav > li > a { 13 | font-size: 16px; 14 | letter-spacing: .05em; 15 | padding-top: 12px; 16 | padding-left: 8px; padding-right: 8px; //6/3/13 by DW 17 | outline: none !important; 18 | } 19 | .dropdown-menu > li > a { 20 | cursor: pointer; 21 | } 22 | .navbar-inner { 23 | -moz-border-radius: 0; 24 | -moz-border-radius: none; 25 | -moz-box-shadow: none; 26 | background-image: none; 27 | border-radius: 0; 28 | } 29 | .divMenubar .brand { 30 | margin-top: 5px; 31 | margin-bottom: 5px; 32 | } 33 | .divMenubar .nav li { 34 | font-family: Ubuntu; 35 | font-size: 16px; 36 | font-weight: bold; 37 | } 38 | .menuKeystroke { 39 | float: right; 40 | margin-left: 25px; 41 | } 42 | .menuKeystroke:before { 43 | content: ""; 44 | } 45 | #idMenuProductName { 46 | font-family: Rancho; 47 | font-size: 32px; 48 | } 49 | .divNavSocialMediaLinks { 50 | margin-top: 15px; 51 | font-size: 1.3em; 52 | float: right; 53 | } 54 | .divNavSocialMediaLinks .nav>li>a { 55 | display: inline; 56 | } 57 | .divNavSocialMediaLinks .aSocialMediaLink { 58 | letter-spacing: 14px; 59 | } 60 | 61 | -------------------------------------------------------------------------------- /lib/menus.js: -------------------------------------------------------------------------------- 1 | function initMenus () { 2 | document.getElementById ("idMenuProductName").innerHTML = appConsts.productnameForDisplay; 3 | document.getElementById ("idMenuAboutProductName").innerHTML = appConsts.productnameForDisplay; 4 | $("#idMenubar .dropdown-menu li").each (function () { 5 | var li = $(this); 6 | var liContent = li.html (); 7 | liContent = liContent.replace ("Cmd-", getCmdKeyPrefix ()); 8 | li.html (liContent); 9 | }); 10 | } 11 | function initTwitterMenuItems () { 12 | twUpdateTwitterMenuItem ("idTwitterConnectMenuItem"); 13 | twUpdateTwitterUsername ("idTwitterUsername"); 14 | $("#idTwitterIcon").html (twittericon); 15 | } 16 | function initFacebookMenuItems () { 17 | fbUpdateFacebookMenuItem ("idFacebookConnectMenuItem"); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /lib/strftime.js: -------------------------------------------------------------------------------- 1 | // version 0.11 by Daniel Rench 2 | // More information: http://dren.ch/strftime/ 3 | // This is public domain software 4 | 5 | Number.prototype.pad = 6 | function (n,p) { 7 | var s = '' + this; 8 | p = p || '0'; 9 | while (s.length < n) s = p + s; 10 | return s; 11 | }; 12 | 13 | Date.prototype.months = [ 14 | 'January', 'February', 'March', 'April', 'May', 'June', 'July', 15 | 'August', 'September', 'October', 'November', 'December' 16 | ]; 17 | Date.prototype.weekdays = [ 18 | 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 19 | 'Thursday', 'Friday', 'Saturday' 20 | ]; 21 | Date.prototype.dpm = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; 22 | 23 | Date.prototype.strftime_f = { 24 | A: function (d) { return d.weekdays[d.getDay()] }, 25 | a: function (d) { return d.weekdays[d.getDay()].substring(0,3) }, 26 | B: function (d) { return d.months[d.getMonth()] }, 27 | b: function (d) { return d.months[d.getMonth()].substring(0,3) }, 28 | C: function (d) { return Math.floor(d.getFullYear()/100); }, 29 | c: function (d) { return d.toString() }, 30 | D: function (d) { 31 | return d.strftime_f.m(d) + '/' + 32 | d.strftime_f.d(d) + '/' + d.strftime_f.y(d); 33 | }, 34 | d: function (d) { return d.getDate().pad(2,'0') }, 35 | e: function (d) { return d.getDate().pad(2,' ') }, 36 | F: function (d) { 37 | return d.strftime_f.Y(d) + '-' + d.strftime_f.m(d) + '-' + 38 | d.strftime_f.d(d); 39 | }, 40 | H: function (d) { return d.getHours().pad(2,'0') }, 41 | I: function (d) { return ((d.getHours() % 12 || 12).pad(2)) }, 42 | j: function (d) { 43 | var t = d.getDate(); 44 | var m = d.getMonth() - 1; 45 | if (m > 1) { 46 | var y = d.getYear(); 47 | if (((y % 100) == 0) && ((y % 400) == 0)) ++t; 48 | else if ((y % 4) == 0) ++t; 49 | } 50 | while (m > -1) t += d.dpm[m--]; 51 | return t.pad(3,'0'); 52 | }, 53 | k: function (d) { return d.getHours().pad(2,' ') }, 54 | l: function (d) { return ((d.getHours() % 12 || 12).pad(2,' ')) }, 55 | M: function (d) { return d.getMinutes().pad(2,'0') }, 56 | m: function (d) { return (d.getMonth()+1).pad(2,'0') }, 57 | n: function (d) { return "\n" }, 58 | p: function (d) { return (d.getHours() > 11) ? 'PM' : 'AM' }, 59 | R: function (d) { return d.strftime_f.H(d) + ':' + d.strftime_f.M(d) }, 60 | r: function (d) { 61 | return d.strftime_f.I(d) + ':' + d.strftime_f.M(d) + ':' + 62 | d.strftime_f.S(d) + ' ' + d.strftime_f.p(d); 63 | }, 64 | S: function (d) { return d.getSeconds().pad(2,'0') }, 65 | s: function (d) { return Math.floor(d.getTime()/1000) }, 66 | T: function (d) { 67 | return d.strftime_f.H(d) + ':' + d.strftime_f.M(d) + ':' + 68 | d.strftime_f.S(d); 69 | }, 70 | t: function (d) { return "\t" }, 71 | /* U: function (d) { return false }, */ 72 | u: function (d) { return(d.getDay() || 7) }, 73 | /* V: function (d) { return false }, */ 74 | v: function (d) { 75 | return d.strftime_f.e(d) + '-' + d.strftime_f.b(d) + '-' + 76 | d.strftime_f.Y(d); 77 | }, 78 | /* W: function (d) { return false }, */ 79 | w: function (d) { return d.getDay() }, 80 | X: function (d) { return d.toTimeString() }, // wrong? 81 | x: function (d) { return d.toDateString() }, // wrong? 82 | Y: function (d) { return d.getFullYear() }, 83 | y: function (d) { return (d.getYear() % 100).pad(2) }, 84 | // Z: function (d) { return d.toString().match(/\((.+)\)$/)[1]; }, 85 | // z: function (d) { return d.getTimezoneOffset() }, // wrong 86 | // z: function (d) { return d.toString().match(/\sGMT([+-]\d+)/)[1]; }, 87 | '%': function (d) { return '%' } 88 | }; 89 | 90 | Date.prototype.strftime_f['+'] = Date.prototype.strftime_f.c; 91 | Date.prototype.strftime_f.h = Date.prototype.strftime_f.b; 92 | 93 | Date.prototype.strftime = 94 | function (fmt) { 95 | var r = ''; 96 | var n = 0; 97 | while(n < fmt.length) { 98 | var c = fmt.substring(n, n+1); 99 | if (c == '%') { 100 | c = fmt.substring(++n, n+1); 101 | r += (this.strftime_f[c]) ? this.strftime_f[c](this) : c; 102 | } else r += c; 103 | ++n; 104 | } 105 | return r; 106 | }; 107 | 108 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | function sameDay (d1, d2) { 2 | //returns true if the two dates are on the same day 3 | d1 = new Date (d1); 4 | d2 = new Date (d2); 5 | return ((d1.getFullYear () == d2.getFullYear ()) && (d1.getMonth () == d2.getMonth ()) && (d1.getDate () == d2.getDate ())); 6 | } 7 | function dayGreaterThanOrEqual (d1, d2) { //9/2/14 by DW 8 | d1 = new Date (d1); 9 | d1.setHours (0); 10 | d1.setMinutes (0); 11 | d1.setSeconds (0); 12 | 13 | d2 = new Date (d2); 14 | d2.setHours (0); 15 | d2.setMinutes (0); 16 | d2.setSeconds (0); 17 | 18 | return (d1 >= d2); 19 | } 20 | function stringLower (s) { 21 | if (s === undefined) { //1/26/15 by DW 22 | return (""); 23 | } 24 | s = s.toString (); //1/26/15 by DW 25 | return (s.toLowerCase ()); 26 | } 27 | function secondsSince (when) { 28 | var now = new Date (); 29 | when = new Date (when); 30 | return ((now - when) / 1000); 31 | } 32 | function padWithZeros (num, ctplaces) { 33 | var s = num.toString (); 34 | while (s.length < ctplaces) { 35 | s = "0" + s; 36 | } 37 | return (s); 38 | } 39 | function getDatePath (theDate, flLastSeparator) { 40 | if (theDate === undefined) { 41 | theDate = new Date (); 42 | } 43 | else { 44 | theDate = new Date (theDate); //8/12/14 by DW -- make sure it's a date type 45 | } 46 | if (flLastSeparator === undefined) { 47 | flLastSeparator = true; 48 | } 49 | 50 | var month = padWithZeros (theDate.getMonth () + 1, 2); 51 | var day = padWithZeros (theDate.getDate (), 2); 52 | var year = theDate.getFullYear (); 53 | 54 | if (flLastSeparator) { 55 | return (year + "/" + month + "/" + day + "/"); 56 | } 57 | else { 58 | return (year + "/" + month + "/" + day); 59 | } 60 | } 61 | function multipleReplaceAll (s, adrTable, flCaseSensitive, startCharacters, endCharacters) { 62 | if(flCaseSensitive===undefined){ 63 | flCaseSensitive = false; 64 | } 65 | if(startCharacters===undefined){ 66 | startCharacters=""; 67 | } 68 | if(endCharacters===undefined){ 69 | endCharacters=""; 70 | } 71 | for( var item in adrTable){ 72 | var replacementValue = adrTable[item]; 73 | var regularExpressionModifier = "g"; 74 | if(!flCaseSensitive){ 75 | regularExpressionModifier = "gi"; 76 | } 77 | var regularExpressionString = (startCharacters+item+endCharacters).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); 78 | var regularExpression = new RegExp(regularExpressionString, regularExpressionModifier); 79 | s = s.replace(regularExpression, replacementValue); 80 | } 81 | return s; 82 | } 83 | function endsWith (s, possibleEnding, flUnicase) { 84 | if ((s === undefined) || (s.length == 0)) { 85 | return (false); 86 | } 87 | var ixstring = s.length - 1; 88 | if (flUnicase === undefined) { 89 | flUnicase = true; 90 | } 91 | if (flUnicase) { 92 | for (var i = possibleEnding.length - 1; i >= 0; i--) { 93 | if (stringLower (s [ixstring--]) != stringLower (possibleEnding [i])) { 94 | return (false); 95 | } 96 | } 97 | } 98 | else { 99 | for (var i = possibleEnding.length - 1; i >= 0; i--) { 100 | if (s [ixstring--] != possibleEnding [i]) { 101 | return (false); 102 | } 103 | } 104 | } 105 | return (true); 106 | } 107 | function stringContains (s, whatItMightContain, flUnicase) { //11/9/14 by DW 108 | if (flUnicase === undefined) { 109 | flUnicase = true; 110 | } 111 | if (flUnicase) { 112 | s = s.toLowerCase (); 113 | whatItMightContain = whatItMightContain.toLowerCase (); 114 | } 115 | return (s.indexOf (whatItMightContain) != -1); 116 | } 117 | function beginsWith (s, possibleBeginning, flUnicase) { 118 | if (s === undefined) { //7/15/15 by DW 119 | return (false); 120 | } 121 | if (s.length == 0) { //1/1/14 by DW 122 | return (false); 123 | } 124 | if (flUnicase === undefined) { 125 | flUnicase = true; 126 | } 127 | if (flUnicase) { 128 | for (var i = 0; i < possibleBeginning.length; i++) { 129 | if (stringLower (s [i]) != stringLower (possibleBeginning [i])) { 130 | return (false); 131 | } 132 | } 133 | } 134 | else { 135 | for (var i = 0; i < possibleBeginning.length; i++) { 136 | if (s [i] != possibleBeginning [i]) { 137 | return (false); 138 | } 139 | } 140 | } 141 | return (true); 142 | } 143 | function isAlpha (ch) { 144 | return (((ch >= 'a') && (ch <= 'z')) || ((ch >= 'A') && (ch <= 'Z'))); 145 | } 146 | function isNumeric (ch) { 147 | return ((ch >= '0') && (ch <= '9')); 148 | } 149 | function trimLeading (s, ch) { 150 | while (s.charAt (0) === ch) { 151 | s = s.substr (1); 152 | } 153 | return (s); 154 | } 155 | function trimTrailing (s, ch) { 156 | while (s.charAt (s.length - 1) === ch) { 157 | s = s.substr (0, s.length - 1); 158 | } 159 | return (s); 160 | } 161 | function trimWhitespace (s) { //rewrite -- 5/30/14 by DW 162 | function isWhite (ch) { 163 | switch (ch) { 164 | case " ": case "\r": case "\n": case "\t": 165 | return (true); 166 | } 167 | return (false); 168 | } 169 | if (s === undefined) { //9/10/14 by DW 170 | return (""); 171 | } 172 | while (isWhite (s.charAt (0))) { 173 | s = s.substr (1); 174 | } 175 | while (s.length > 0) { 176 | if (!isWhite (s.charAt (0))) { 177 | break; 178 | } 179 | s = s.substr (1); 180 | } 181 | while (s.length > 0) { 182 | if (!isWhite (s.charAt (s.length - 1))) { 183 | break; 184 | } 185 | s = s.substr (0, s.length - 1); 186 | } 187 | return (s); 188 | } 189 | function addPeriodAtEnd (s) { 190 | s = trimWhitespace (s); 191 | if (s.length == 0) { 192 | return (s); 193 | } 194 | switch (s [s.length - 1]) { 195 | case ".": 196 | case ",": 197 | case "?": 198 | case "\"": 199 | case "'": 200 | case ":": 201 | case ";": 202 | case "!": 203 | return (s); 204 | default: 205 | return (s + "."); 206 | } 207 | } 208 | function getBoolean (val) { //12/5/13 by DW 209 | switch (typeof (val)) { 210 | case "string": 211 | if (val.toLowerCase () == "true") { 212 | return (true); 213 | } 214 | break; 215 | case "boolean": 216 | return (val); 217 | case "number": 218 | if (val == 1) { 219 | return (true); 220 | } 221 | break; 222 | } 223 | return (false); 224 | } 225 | function bumpUrlString (s) { //5/10/14 by DW 226 | if (s === undefined) { 227 | s = "0"; 228 | } 229 | function bumpChar (ch) { 230 | function num (ch) { 231 | return (ch.charCodeAt (0)); 232 | } 233 | if ((ch >= "0") && (ch <= "8")) { 234 | ch = String.fromCharCode (num (ch) + 1); 235 | } 236 | else { 237 | if (ch == "9") { 238 | ch = "a"; 239 | } 240 | else { 241 | if ((ch >= "a") && (ch <= "y")) { 242 | ch = String.fromCharCode (num (ch) + 1); 243 | } 244 | else { 245 | throw "rollover!"; 246 | } 247 | } 248 | } 249 | return (ch); 250 | } 251 | try { 252 | var chlast = bumpChar (s [s.length - 1]); 253 | s = s.substr (0, s.length - 1) + chlast; 254 | return (s); 255 | } 256 | catch (tryError) { 257 | if (s.length == 1) { 258 | return ("00"); 259 | } 260 | else { 261 | s = s.substr (0, s.length - 1); 262 | s = bumpUrlString (s) + "0"; 263 | return (s); 264 | } 265 | } 266 | } 267 | function stringDelete (s, ix, ct) { 268 | var start = ix - 1; 269 | var end = (ix + ct) - 1; 270 | var s1 = s.substr (0, start); 271 | var s2 = s.substr (end); 272 | return (s1 + s2); 273 | } 274 | function replaceAll (s, searchfor, replacewith) { 275 | function escapeRegExp (string) { 276 | return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 277 | } 278 | return (s.replace (new RegExp (escapeRegExp (searchfor), 'g'), replacewith)); 279 | } 280 | function stringCountFields (s, chdelim) { 281 | var ct = 1; 282 | if (s.length == 0) { 283 | return (0); 284 | } 285 | for (var i = 0; i < s.length; i++) { 286 | if (s [i] == chdelim) { 287 | ct++; 288 | } 289 | } 290 | return (ct) 291 | } 292 | function stringNthField (s, chdelim, n) { 293 | var splits = s.split (chdelim); 294 | if (splits.length >= n) { 295 | return splits [n-1]; 296 | } 297 | return (""); 298 | } 299 | function dateYesterday (d) { 300 | return (new Date (new Date (d) - (24 * 60 * 60 * 1000))); 301 | } 302 | function stripMarkup (s) { //5/24/14 by DW 303 | if ((s === undefined) || (s == null) || (s.length == 0)) { 304 | return (""); 305 | } 306 | return (s.replace (/(<([^>]+)>)/ig, "")); 307 | } 308 | function maxStringLength (s, len, flWholeWordAtEnd, flAddElipses) { 309 | if (flWholeWordAtEnd === undefined) { 310 | flWholeWordAtEnd = true; 311 | } 312 | if (flAddElipses === undefined) { //6/2/14 by DW 313 | flAddElipses = true; 314 | } 315 | if (s.length > len) { 316 | s = s.substr (0, len); 317 | if (flWholeWordAtEnd) { 318 | while (s.length > 0) { 319 | if (s [s.length - 1] == " ") { 320 | if (flAddElipses) { 321 | s += "..."; 322 | } 323 | break; 324 | } 325 | s = s.substr (0, s.length - 1); //pop last char 326 | } 327 | } 328 | } 329 | return (s); 330 | } 331 | function random (lower, upper) { 332 | var range = upper - lower + 1; 333 | return (Math.floor ((Math.random () * range) + lower)); 334 | } 335 | function removeMultipleBlanks (s) { //7/30/14 by DW 336 | return (s.toString().replace (/ +/g, " ")); 337 | } 338 | function jsonStringify (jstruct, flFixBreakage) { //7/30/14 by DW 339 | //Changes 340 | //6/16/15; 10:43:25 AM by DW 341 | //Andrew Shell reported an issue in the encoding of JSON that's solved by doing character replacement. 342 | //However, this is too big a change to make for all the code that calls this library routine, so we added a boolean flag, flFixBreakage. 343 | //If this proves to be harmless, we'll change the default to true. 344 | //http://river4.smallpict.com/2015/06/16/jsonEncodingIssueSolved.html 345 | if (flFixBreakage === undefined) { 346 | flFixBreakage = false; 347 | } 348 | var s = JSON.stringify (jstruct, undefined, 4); 349 | if (flFixBreakage) { 350 | s = s.replace (/\u2028/g,'\\u2028').replace (/\u2029/g,'\\u2029'); 351 | } 352 | return (s); 353 | } 354 | function stringAddCommas (x) { //5/27/14 by DW 355 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 356 | } 357 | function readHttpFile (url, callback, timeoutInMilliseconds, headers) { //5/27/14 by DW xxx 358 | if (timeoutInMilliseconds === undefined) { 359 | timeoutInMilliseconds = 30000; 360 | } 361 | var jxhr = $.ajax ({ 362 | url: url, 363 | dataType: "text", 364 | headers: headers, 365 | timeout: timeoutInMilliseconds 366 | }) 367 | .success (function (data, status) { 368 | callback (data); 369 | }) 370 | .error (function (status) { 371 | console.log ("readHttpFile: url == " + url + ", error == " + jsonStringify (status)); 372 | callback (undefined); 373 | }); 374 | } 375 | function readHttpFileThruProxy (url, type, callback) { //10/25/14 by DW 376 | var urlReadFileApi = "http://pub2.fargo.io:5347/httpReadUrl"; 377 | if (type === undefined) { 378 | type = "text/plain"; 379 | } 380 | var jxhr = $.ajax ({ 381 | url: urlReadFileApi + "?url=" + encodeURIComponent (url) + "&type=" + encodeURIComponent (type), 382 | dataType: "text" , 383 | timeout: 30000 384 | }) 385 | .success (function (data, status) { 386 | if (callback != undefined) { 387 | callback (data); 388 | } 389 | }) 390 | .error (function (status) { 391 | console.log ("readHttpFileThruProxy: url == " + url + ", error == " + status.statusText + "."); 392 | if (callback != undefined) { 393 | callback (undefined); 394 | } 395 | }); 396 | } 397 | function stringPopLastField (s, chdelim) { //5/28/14 by DW 398 | if (s.length == 0) { 399 | return (s); 400 | } 401 | if (endsWith (s, chdelim)) { 402 | s = stringDelete (s, s.length, 1); 403 | } 404 | while (s.length > 0) { 405 | if (endsWith (s, chdelim)) { 406 | return (stringDelete (s, s.length, 1)); 407 | } 408 | s = stringDelete (s, s.length, 1); 409 | } 410 | return (s); 411 | } 412 | function stringPopExtension (s) { //4/29/15 by DW 413 | for (var i = s.length - 1; i >= 0; i--) { 414 | if (s [i] == ".") { 415 | return (stringMid (s, 1, i)); 416 | } 417 | } 418 | return (s); 419 | } 420 | function filledString (ch, ct) { //6/4/14 by DW 421 | var s = ""; 422 | for (var i = 0; i < ct; i++) { 423 | s += ch; 424 | } 425 | return (s); 426 | } 427 | function encodeXml (s) { //7/15/14 by DW 428 | var charMap = { 429 | '<': '<', 430 | '>': '>', 431 | '&': '&', 432 | '"': '&'+'quot;' 433 | }; 434 | s = s.toString(); 435 | s = s.replace(/\u00A0/g, " "); 436 | var escaped = s.replace(/[<>&"]/g, function(ch) { 437 | return charMap [ch]; 438 | }); 439 | return escaped; 440 | } 441 | function decodeXml (s) { //11/7/14 by DW 442 | return (s.replace (/</g,'<').replace(/>/g,'>').replace(/&/g,'&')); 443 | } 444 | function hotUpText (s, url) { //7/18/14 by DW 445 | 446 | if (url === undefined) { //makes it easier to call -- 3/14/14 by DW 447 | return (s); 448 | } 449 | 450 | function linkit (s) { 451 | return ("" + s + ""); 452 | } 453 | var ixleft = s.indexOf ("["), ixright = s.indexOf ("]"); 454 | if ((ixleft == -1) || (ixright == -1)) { 455 | return (linkit (s)); 456 | } 457 | if (ixright < ixleft) { 458 | return (linkit (s)); 459 | } 460 | 461 | var linktext = s.substr (ixleft + 1, ixright - ixleft - 1); //string.mid (s, ixleft, ixright - ixleft + 1); 462 | linktext = "" + linktext + ""; 463 | 464 | var leftpart = s.substr (0, ixleft); 465 | var rightpart = s.substr (ixright + 1, s.length); 466 | s = leftpart + linktext + rightpart; 467 | return (s); 468 | } 469 | function getDomainFromUrl (url) { //7/11/15 by DW 470 | if ((url != null ) && (url != "")) { 471 | url = url.replace("www.","").replace("www2.", "").replace("feedproxy.", "").replace("feeds.", ""); 472 | var root = url.split('?')[0]; // cleans urls of form http://domain.com?a=1&b=2 473 | url = root.split('/')[2]; 474 | } 475 | return (url); 476 | }; 477 | function getFavicon (url) { //7/18/14 by DW 478 | var domain = getDomainFromUrl (url); 479 | return ("http://www.google.com/s2/favicons?domain=" + domain); 480 | }; 481 | function getURLParameter (name) { //7/21/14 by DW 482 | return (decodeURI ((RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1])); 483 | } 484 | function urlSplitter (url) { //7/15/14 by DW 485 | var pattern = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/; 486 | var result = pattern.exec (url); 487 | if (result == null) { 488 | result = []; 489 | result [5] = url; 490 | } 491 | var splitUrl = { 492 | scheme: result [1], 493 | host: result [3], 494 | port: result [4], 495 | path: result [5], 496 | query: result [6], 497 | hash: result [7] 498 | }; 499 | return (splitUrl); 500 | } 501 | function innerCaseName (text) { //8/12/14 by DW 502 | var s = "", ch, flNextUpper = false; 503 | text = stripMarkup (text); 504 | for (var i = 0; i < text.length; i++) { 505 | ch = text [i]; 506 | if (isAlpha (ch) || isNumeric (ch)) { 507 | if (flNextUpper) { 508 | ch = ch.toUpperCase (); 509 | flNextUpper = false; 510 | } 511 | else { 512 | ch = ch.toLowerCase (); 513 | } 514 | s += ch; 515 | } 516 | else { 517 | if (ch == ' ') { 518 | flNextUpper = true; 519 | } 520 | } 521 | } 522 | return (s); 523 | } 524 | function hitCounter (counterGroup, counterServer) { //8/12/14 by DW 525 | var defaultCounterGroup = "scripting", defaultCounterServer = "http://counter2.fargo.io:5337/counter"; 526 | var thispageurl = location.href; 527 | if (counterGroup === undefined) { 528 | counterGroup = defaultCounterGroup; 529 | } 530 | if (counterServer === undefined) { 531 | counterServer = defaultCounterServer; 532 | } 533 | if (thispageurl === undefined) { 534 | thispageurl = ""; 535 | } 536 | if (endsWith (thispageurl, "#")) { 537 | thispageurl = thispageurl.substr (0, thispageurl.length - 1); 538 | } 539 | var jxhr = $.ajax ({ 540 | url: counterServer + "?group=" + encodeURIComponent (counterGroup) + "&referer=" + encodeURIComponent (document.referrer) + "&url=" + encodeURIComponent (thispageurl), 541 | dataType: "jsonp", 542 | jsonpCallback : "getData", 543 | timeout: 30000 544 | }) 545 | .success (function (data, status, xhr) { 546 | console.log ("hitCounter: counter ping accepted by server, group == " + counterGroup + ", page url == " + thispageurl); 547 | }) 548 | .error (function (status, textStatus, errorThrown) { 549 | console.log ("hitCounter: counter ping error: " + textStatus); 550 | }); 551 | } 552 | function stringMid (s, ix, len) { //8/12/14 by DW 553 | return (s.substr (ix-1, len)); 554 | } 555 | function getCmdKeyPrefix () { //8/15/14 by DW 556 | if (navigator.platform.toLowerCase ().substr (0, 3) == "mac") { 557 | return ("⌘"); 558 | } 559 | else { 560 | return ("Ctrl+"); 561 | } 562 | } 563 | function getRandomSnarkySlogan () { //8/15/14 by DW 564 | var snarkySlogans = [ 565 | "Good for the environment.", 566 | "All baking done on premises.", 567 | "Still diggin!", 568 | "It's even worse than it appears.", 569 | "Ask not what the Internet can do for you...", 570 | "You should never argue with a crazy man.", 571 | "Welcome back my friends to the show that never ends.", 572 | "Greetings, citizen of Planet Earth. We are your overlords. :-)", 573 | "We don't need no stinkin rock stars.", 574 | "This aggression will not stand.", 575 | "Pay no attention to the man behind the curtain.", 576 | "Only steal from the best.", 577 | "Reallll soooon now...", 578 | "What a long strange trip it's been.", 579 | "Ask not what the Internet can do for you.", 580 | "When in doubt, blog.", 581 | "Shut up and eat your vegetables.", 582 | "Don't slam the door on the way out.", 583 | "Yeah well, that's just, you know, like, your opinion, man.", 584 | "So, it has come to this." 585 | ] 586 | return (snarkySlogans [random (0, snarkySlogans.length - 1)]); 587 | } 588 | function dayOfWeekToString (theDay) { //8/23/14 by DW 589 | var weekday = [ 590 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" 591 | ]; 592 | return (weekday[theDay]); 593 | } 594 | function viewDate (when, flShortDayOfWeek) { //8/23/14 by DW 595 | var now = new Date (); 596 | when = new Date (when); 597 | if (sameDay (when, now)) { 598 | return (timeString (when, false)) //2/9/13 by DW; 599 | } 600 | else { 601 | var oneweek = 1000 * 60 * 60 * 24 * 7; 602 | var cutoff = now - oneweek; 603 | if (when > cutoff) { //within the last week 604 | var s = dayOfWeekToString (when.getDay ()); 605 | if (flShortDayOfWeek) { 606 | s = s.substring (0, 3); 607 | } 608 | return (s); 609 | } 610 | else { 611 | return (when.toLocaleDateString ()); 612 | } 613 | } 614 | } 615 | function timeString (when, flIncludeSeconds) { //8/26/14 by DW 616 | var hour = when.getHours (), minutes = when.getMinutes (), ampm = "AM", s; 617 | if (hour >= 12) { 618 | ampm = "PM"; 619 | } 620 | if (hour > 12) { 621 | hour -= 12; 622 | } 623 | if (hour == 0) { 624 | hour = 12; 625 | } 626 | if (minutes < 10) { 627 | minutes = "0" + minutes; 628 | } 629 | if (flIncludeSeconds) { 630 | var seconds = when.getSeconds (); 631 | if (seconds < 10) { 632 | seconds = "0" + seconds; 633 | } 634 | s = hour + ":" + minutes + ":" + seconds + ampm; 635 | } 636 | else { 637 | s = hour + ":" + minutes + ampm; 638 | } 639 | return (s); 640 | } 641 | function stringLastField (s, chdelim) { //8/27/14 by DW 642 | var ct = stringCountFields (s, chdelim); 643 | if (ct == 0) { //8/31/14 by DW 644 | return (s); 645 | } 646 | return (stringNthField (s, chdelim, ct)); 647 | } 648 | function maxLengthString (s, maxlength) { //8/27/14 by DW 649 | if (s.length > maxlength) { 650 | s = s.substr (0, maxlength); 651 | while (true) { 652 | var len = s.length; flbreak = false; 653 | if (len == 0) { 654 | break; 655 | } 656 | if (s [len - 1] == " ") { 657 | flbreak = true; 658 | } 659 | s = s.substr (0, len - 1); 660 | if (flbreak) { 661 | break; 662 | } 663 | } 664 | s = s + "..."; 665 | } 666 | return (s); 667 | } 668 | function formatDate (theDate, dateformat, timezone) { //8/28/14 by DW 669 | if (theDate === undefined) { 670 | theDate = new Date (); 671 | } 672 | if (dateformat === undefined) { 673 | dateformat = "%c"; 674 | } 675 | if (timezone === undefined) { 676 | timezone = - (new Date ().getTimezoneOffset () / 60); 677 | } 678 | try { 679 | var offset = new Number (timezone); 680 | var d = new Date (theDate); 681 | var localTime = d.getTime (); 682 | var localOffset = d.getTimezoneOffset () * 60000; 683 | var utc = localTime + localOffset; 684 | var newTime = utc + (3600000 * offset); 685 | return (new Date (newTime).strftime (dateformat)); 686 | } 687 | catch (tryerror) { 688 | return (new Date (theDate).strftime (dateformat)); 689 | } 690 | } 691 | function addPeriodToSentence (s) { //8/29/14 by DW 692 | if (s.length > 0) { 693 | var fladd = true; 694 | var ch = s [s.length - 1]; 695 | switch (ch) { 696 | case "!": case "?": case ":": 697 | fladd = false; 698 | break; 699 | default: 700 | if (endsWith (s, ".\"")) { 701 | fladd = false; 702 | } 703 | else { 704 | if (endsWith (s, ".'")) { 705 | fladd = false; 706 | } 707 | } 708 | } 709 | if (fladd) { 710 | s += "."; 711 | } 712 | } 713 | return (s); 714 | } 715 | function copyScalars (source, dest) { //8/31/14 by DW 716 | for (var x in source) { 717 | var type, val = source [x]; 718 | if (val instanceof Date) { 719 | val = val.toString (); 720 | } 721 | type = typeof (val); 722 | if ((type != "object") && (type != undefined)) { 723 | dest [x] = val; 724 | } 725 | } 726 | } 727 | function linkToDomainFromUrl (url, flshort, maxlength) { //10/10/14 by DW 728 | var splitUrl = urlSplitter (url), host = splitUrl.host.toLowerCase (); 729 | if (flshort === undefined) { 730 | flshort = false; 731 | } 732 | if (flshort) { 733 | var splithost = host.split ("."); 734 | if (splithost.length == 3) { 735 | host = splithost [1]; 736 | } 737 | else { 738 | host = splithost [0]; 739 | } 740 | } 741 | else { 742 | if (beginsWith (host, "www.")) { 743 | host = stringDelete (host, 1, 4); 744 | } 745 | } 746 | 747 | if (maxlength != undefined) { //10/10/14; 10:46:56 PM by DW 748 | if (host.length > maxlength) { 749 | host = stringMid (host, 1, maxlength) + "..."; 750 | } 751 | } 752 | 753 | return ("" + host + ""); 754 | } 755 | function getRandomPassword (ctchars) { //10/14/14 by DW 756 | var s= "", ch; 757 | while (s.length < ctchars) { 758 | ch = String.fromCharCode (random (33, 122)); 759 | if (isAlpha (ch) || isNumeric (ch)) { 760 | s += ch; 761 | } 762 | } 763 | return (s.toLowerCase ()); 764 | } 765 | function monthToString (theMonthNum) { //11/4/14 by DW 766 | 767 | 768 | var theDate; 769 | if (theMonthNum === undefined) { 770 | theDate = new Date (); 771 | } 772 | else { 773 | theDate = new Date ((theMonthNum + 1) + "/1/2014"); 774 | } 775 | return (formatDate (theDate, "%B")); 776 | } 777 | function getCanonicalName (text) { //11/4/14 by DW 778 | var s = "", ch, flNextUpper = false; 779 | text = stripMarkup (text); //6/30/13 by DW 780 | for (var i = 0; i < text.length; i++) { 781 | ch = text [i]; 782 | if (isAlpha (ch) || isNumeric (ch)) { 783 | if (flNextUpper) { 784 | ch = ch.toUpperCase (); 785 | flNextUpper = false; 786 | } 787 | else { 788 | ch = ch.toLowerCase (); 789 | } 790 | s += ch; 791 | } 792 | else { 793 | if (ch == ' ') { 794 | flNextUpper = true; 795 | } 796 | } 797 | } 798 | return (s); 799 | } 800 | function clockNow () { //11/7/14 by DW 801 | return (new Date ()); 802 | } 803 | function sleepTillTopOfMinute (callback) { //11/22/14 by DW 804 | var ctseconds = Math.round (60 - (new Date ().getSeconds () + 60) % 60); 805 | if (ctseconds == 0) { 806 | ctseconds = 60; 807 | } 808 | setTimeout (everyMinute, ctseconds * 1000); 809 | } 810 | function scheduleNextRun (callback, ctMillisecsBetwRuns) { //11/27/14 by DW 811 | var ctmilliseconds = ctMillisecsBetwRuns - (Number (new Date ().getMilliseconds ()) + ctMillisecsBetwRuns) % ctMillisecsBetwRuns; 812 | setTimeout (callback, ctmilliseconds); 813 | } 814 | function urlEncode (s) { //12/4/14 by DW 815 | return (encodeURIComponent (s)); 816 | } 817 | function popTweetNameAtStart (s) { //12/8/14 by DW 818 | var ch; 819 | s = trimWhitespace (s); 820 | if (s.length > 0) { 821 | if (s.charAt (0) == "@") { 822 | while (s.charAt (0) != " ") { 823 | s = s.substr (1) 824 | } 825 | while (s.length > 0) { 826 | ch = s.charAt (0); 827 | if ((ch != " ") && (ch != "-")) { 828 | break; 829 | } 830 | s = s.substr (1) 831 | } 832 | } 833 | } 834 | return (s); 835 | } 836 | function httpHeadRequest (url, callback) { //12/17/14 by DW 837 | var jxhr = $.ajax ({ 838 | url: url, 839 | type: "HEAD", 840 | dataType: "text", 841 | timeout: 30000 842 | }) 843 | .success (function (data, status, xhr) { 844 | callback (xhr); //you can do xhr.getResponseHeader to get one of the header elements 845 | }) 846 | } 847 | function httpExt2MIME (ext) { //12/24/14 by DW 848 | var lowerext = stringLower (ext); 849 | var map = { 850 | "au": "audio/basic", 851 | "avi": "application/x-msvideo", 852 | "bin": "application/x-macbinary", 853 | "css": "text/css", 854 | "dcr": "application/x-director", 855 | "dir": "application/x-director", 856 | "dll": "application/octet-stream", 857 | "doc": "application/msword", 858 | "dtd": "text/dtd", 859 | "dxr": "application/x-director", 860 | "exe": "application/octet-stream", 861 | "fatp": "text/html", 862 | "ftsc": "text/html", 863 | "fttb": "text/html", 864 | "gif": "image/gif", 865 | "gz": "application/x-gzip", 866 | "hqx": "application/mac-binhex40", 867 | "htm": "text/html", 868 | "html": "text/html", 869 | "jpeg": "image/jpeg", 870 | "jpg": "image/jpeg", 871 | "js": "application/javascript", 872 | "mid": "audio/x-midi", 873 | "midi": "audio/x-midi", 874 | "mov": "video/quicktime", 875 | "mp3": "audio/mpeg", 876 | "pdf": "application/pdf", 877 | "png": "image/png", 878 | "ppt": "application/mspowerpoint", 879 | "ps": "application/postscript", 880 | "ra": "audio/x-pn-realaudio", 881 | "ram": "audio/x-pn-realaudio", 882 | "sit": "application/x-stuffit", 883 | "sys": "application/octet-stream", 884 | "tar": "application/x-tar", 885 | "text": "text/plain", 886 | "txt": "text/plain", 887 | "wav": "audio/x-wav", 888 | "wrl": "x-world/x-vrml", 889 | "xml": "text/xml", 890 | "zip": "application/zip" 891 | }; 892 | for (x in map) { 893 | if (stringLower (x) == lowerext) { 894 | return (map [x]); 895 | } 896 | } 897 | return ("text/plain"); 898 | } 899 | function kilobyteString (num) { //1/24/15 by DW 900 | num = Number (num) / 1024; 901 | return (num.toFixed (2) + "K"); 902 | } 903 | function megabyteString (num) { //1/24/15 by DW 904 | var onemeg = 1024 * 1024; 905 | if (num <= onemeg) { 906 | return (kilobyteString (num)); 907 | } 908 | num = Number (num) / onemeg; 909 | return (num.toFixed (2) + "MB"); 910 | } 911 | function gigabyteString (num) { //1/24/15 by DW 912 | var onegig = 1024 * 1024 * 1024; 913 | if (num <= onegig) { 914 | return (megabyteString (num)); 915 | } 916 | num = Number (num) / onegig; 917 | return (num.toFixed (2) + "GB"); 918 | } 919 | function dateToNumber (theDate) { //2/15/15 by DW 920 | return (Number (new Date (theDate))); 921 | } 922 | 923 | -------------------------------------------------------------------------------- /scripts.js: -------------------------------------------------------------------------------- 1 | /* The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dave Winer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | structured listing: http://scripting.com/listings/mywordeditor.html 24 | */ 25 | 26 | var appConsts = { 27 | productname: "myWordEditor", 28 | productnameForDisplay: "MyWord Editor", 29 | "description": "A simple silo-free blogging tool that creates beautiful essay pages.", 30 | urlTwitterServer: "http://twitter.myword.io/", //backup, in case config.json is missing 31 | domain: "myword.io", //the real value is set in startup () 32 | version: "0.73" 33 | }; 34 | var appPrefs = { 35 | authorName: "", authorWebsite: "", 36 | ctStartups: 0, minSecsBetwAutoSaves: 3, 37 | flAutoPublish: true, 38 | flMarkdownProcess: false, 39 | textFont: "Ubuntu", textFontSize: 18, textLineHeight: 24, 40 | flDisqusComments: true, disqusGroupName: "smallpict", 41 | flRssPrefsInitialized: false, rssTitle: "", rssDescription: "", rssLink: "", rssMaxItemsInFeed: 25, rssLanguage: "en-us", 42 | rssHistory: [], rssFeedUrl: "", 43 | flUseDefaultImage: false, defaultImageUrl: "", 44 | nameDefaultTemplate: "default", //3/29/15 by DW 45 | flDisqusComments: true, disqusGroupName: "mywordcomments", //3/31/15 by DW 46 | nameTextEditor: "html4", //4/1/15 by DW 47 | lastTweetText: "", lastUserName: "davewiner", 48 | fileSerialnum: 0, 49 | lastFilePath: "", 50 | lastPublishedUrl: "" 51 | }; 52 | var flStartupFail = false; 53 | var flPrefsChanged = false, flFeedChanged = false, flHistoryChanged = false; 54 | var whenLastUserAction = new Date (); 55 | var myTextFilename = "myTextFile.txt"; 56 | var globalDefaultImageUrl = "http://scripting.com/2015/03/06/allmans.png"; 57 | 58 | var theData = { //the file being edited now 59 | title: "", 60 | description: "", 61 | authorname: "", 62 | authorwebsite: "", 63 | when: new Date (0), 64 | img: "", 65 | subs: [], 66 | whenLastSave: new Date (0), 67 | ctSaves: 0, 68 | publishedUrl: "", 69 | linkJson: "", //3/24/15 by DW 70 | nameTemplate: appPrefs.nameDefaultTemplate //3/28/15 by DW 71 | }; 72 | var urlTemplateFile = "templates/default.html"; 73 | var jsontextForLastSave; 74 | var whenLastUserAction = new Date (), whenLastKeystroke = whenLastUserAction; 75 | var randomMysteryString, ctCloseAttempts = 0; 76 | var fnameconfig = "config.json"; //3/20/15 by DW 77 | var config; //3/28/15 by DW 78 | var defaultEditorButtons = ["bold", "italic", "underline", "strikethrough", "anchor", "h2", "h3", "quote"]; //7/27/15 by DW 79 | var defaultTemplates = { 80 | "Default": "http://myword.io/templates/default.html", 81 | "Plain": "http://myword.io/templates/plain/template.html" 82 | }; 83 | var disqusCode = //3/31/15 by DW 84 | "
\n"; 85 | 86 | function patchPrefs () { 87 | console.log ("patchPrefs"); 88 | //7/24/15 by DW -- advent of the flPgfLevelMarkdown field of rssHistory objects 89 | for (var i = 0; i < appPrefs.rssHistory.length; i++) { 90 | var obj = appPrefs.rssHistory [i]; 91 | if (obj.flPgfLevelMarkdown === undefined) { 92 | obj.flPgfLevelMarkdown = true; 93 | prefsChanged (); 94 | } 95 | } 96 | //3/27/15 by DW -- some early files in rssHistory have incorrect filepath fields 97 | for (var i = 0; i < appPrefs.rssHistory.length; i++) { 98 | var obj = appPrefs.rssHistory [i]; 99 | if (endsWith (obj.filepath, ".html")) { 100 | obj.filepath = stringMid (obj.filepath, 1, obj.filepath.length - 5) + ".json"; 101 | prefsChanged (); 102 | } 103 | } 104 | } 105 | function keyupTextArea () { 106 | } 107 | function runStartupCode () { //4/3/15 by DW 108 | if (config != undefined) { 109 | if (config.startupCode != undefined) { 110 | if (config.startupCode.length > 0) { 111 | var s = config.startupCode.replace ("\n", ""); 112 | eval (s); 113 | } 114 | } 115 | } 116 | } 117 | function getAllPosts (callback) { 118 | var postArray = []; 119 | function readOne (ix) { 120 | if (ix > 0) { 121 | var path = "essays/" + padWithZeros (ix, 3) + ".json"; 122 | twGetFile (path, true, true, function (error, data) { 123 | if (data != undefined) { 124 | postArray [ix - 1] = JSON.parse (data.filedata); 125 | } 126 | readOne (ix - 1); 127 | }); 128 | } 129 | else { 130 | twUploadFile ("files.json", jsonStringify (postArray), "application/json", false, function (data) { 131 | if (callback !== undefined) { 132 | callback (postArray, data.url); 133 | } 134 | }); 135 | } 136 | } 137 | readOne (appPrefs.fileSerialnum); 138 | } 139 | function getPostsInJson () { 140 | getAllPosts (function (postArray, urlJsonFile) { 141 | confirmDialog ("View the JSON file?", function () { 142 | window.open (urlJsonFile); 143 | }); 144 | }); 145 | } 146 | function closeFileOpenDialog () { 147 | $("#idFileOpenDialog").modal ("hide"); 148 | } 149 | function openThisFile (id) { 150 | console.log ("openThisFile: id == " + id); 151 | appPrefs.lastFilePath = id; 152 | openEssayFile (function () { 153 | prefsChanged (); 154 | closeFileOpenDialog (); 155 | }); 156 | } 157 | function fileOpenDialog () { 158 | getAllPosts (function (postArray, urlJsonFile) { 159 | var htmltext = "
"; 166 | $("#idFileOpenDialog").modal ("show"); 167 | $("#idWhereToDisplayFileList").html (htmltext); 168 | }); 169 | } 170 | function publishAllPosts () { //3/24/15 by DW 171 | confirmDialog ("Publish all posts?", function () { 172 | var savedFilePath = appPrefs.lastFilePath; 173 | function publishOne (ix, callback) { 174 | if (ix > 0) { 175 | appPrefs.lastFilePath = "essays/" + padWithZeros (ix, 3) + ".json"; 176 | openEssayFile (function () { 177 | console.log ("publishAllPosts: publishing " + appPrefs.lastFilePath); 178 | publishButtonClick (false, function () { 179 | publishOne (ix - 1, callback); 180 | }); 181 | }); 182 | } 183 | else { 184 | if (callback !== undefined) { 185 | callback (); 186 | } 187 | } 188 | } 189 | publishOne (appPrefs.fileSerialnum, function () { 190 | appPrefs.lastFilePath = savedFilePath; 191 | openEssayFile (); 192 | }); 193 | }); 194 | } 195 | function fieldsToHistory () { //copy from theData into the current history array element 196 | function getBodyText () { 197 | //Changes 198 | //3/26/15; 12:37:43 PM by DW 199 | //We're now passing the text through a markdown processor as the feed is built, so we need to use markdown conventions for paragraphs. 200 | var s = ""; 201 | for (var i = 0; i < theData.subs.length; i++) { 202 | s += theData.subs [i] + "\n\n"; 203 | } 204 | return (s); 205 | } 206 | for (var i = 0; i < appPrefs.rssHistory.length; i++) { 207 | var obj = appPrefs.rssHistory [i]; 208 | if (obj.filepath == theData.filePath) { 209 | console.log ("fieldsToHistory: copying data into history array element #" + i); 210 | obj.guid.flPermalink = true; 211 | obj.guid.value = theData.publishedUrl; 212 | obj.title = theData.title; 213 | obj.link = theData.publishedUrl; 214 | obj.text = getBodyText (); 215 | obj.twitterScreenName = twGetScreenName (); 216 | obj.when = theData.when; 217 | obj.filepath = theData.filePath; 218 | obj.linkJson = theData.linkJson; 219 | obj.flMarkdown = true; //3/26/15 by DW 220 | obj.flPgfLevelMarkdown = true; //7/24/15 by DW 221 | historyChanged (); //3/27/15 by DW 222 | break; 223 | } 224 | } 225 | } 226 | function addToHistory () { 227 | var obj = new Object (); 228 | appPrefs.rssHistory.unshift (obj); 229 | while (appPrefs.rssHistory.length > appPrefs.rssMaxItemsInFeed) { 230 | appPrefs.rssHistory.pop (); 231 | } 232 | obj.guid = new Object (); 233 | obj.filepath = theData.filePath; //otherwise fieldsToHistory won't find it! oy 234 | fieldsToHistory (); 235 | } 236 | function buildHistoryMenu () { //3/27/15 by DW 237 | var maxCharsHistoryMenuItem = 25; 238 | $("#idHistoryMenuList").empty (); 239 | for (var i = 0; i < appPrefs.rssHistory.length; i++) { 240 | var obj = appPrefs.rssHistory [i]; 241 | var liMenuItem = $("
  • "); 242 | var menuItemNameLink = $(""); 243 | 244 | //set text of menu item 245 | var itemtext = obj.title; 246 | itemtext = maxLengthString (itemtext, maxCharsHistoryMenuItem); 247 | 248 | if (obj.filepath == appPrefs.lastFilePath) { 249 | itemtext = "" + itemtext; 250 | } 251 | 252 | if (itemtext.length === 0) { 253 | itemtext = " "; 254 | } 255 | menuItemNameLink.html (itemtext); 256 | 257 | menuItemNameLink.attr ("onclick", "openThisFile ('" + obj.filepath + "')"); 258 | menuItemNameLink.attr ("target", "_blank"); 259 | liMenuItem.append (menuItemNameLink); 260 | $("#idHistoryMenuList").append (liMenuItem); 261 | } 262 | } 263 | function rssCloudPing (urlServer, urlFeed) { //7/25/15 by DW 264 | $.post (urlServer, {url: urlFeed}, function (data, status) { 265 | console.log ("rssCloudPing: urlServer == " + urlServer + ", urlFeed == " + urlFeed + ", status == " + status); 266 | }); 267 | } 268 | function myWordBuildRssFeed () { 269 | var now = new Date (), flcloud = false; 270 | var headElements = { 271 | title: appPrefs.rssTitle, 272 | link: appPrefs.rssLink, 273 | description: appPrefs.rssDescription, 274 | language: appPrefs.rssLanguage, 275 | generator: appConsts.productnameForDisplay + " v" + appConsts.version, 276 | docs: "http://cyber.law.harvard.edu/rss/rss.html", 277 | twitterScreenName: twGetScreenName (), 278 | maxFeedItems: appPrefs.rssMaxItemsInFeed, 279 | appDomain: appConsts.domain 280 | } 281 | if (config.rssCloud !== undefined) { //7/25/15 by DW 282 | if (getBoolean (config.rssCloud.enabled)) { 283 | headElements.flRssCloudEnabled = true; 284 | headElements.rssCloudDomain = config.rssCloud.domain; 285 | headElements.rssCloudPort = config.rssCloud.port; 286 | headElements.rssCloudPath = config.rssCloud.path; 287 | headElements.rssCloudRegisterProcedure = ""; 288 | headElements.rssCloudProtocol = "http-post"; 289 | flcloud = true; 290 | } 291 | } 292 | var xmltext = buildRssFeed (headElements, appPrefs.rssHistory); 293 | twUploadFile ("rss.xml", xmltext, "text/xml", false, function (data) { 294 | console.log ("myWordBuildRssFeed: " + data.url + " (" + secondsSince (now) + " seconds)"); 295 | if (appPrefs.rssFeedUrl != data.url) { 296 | appPrefs.rssFeedUrl = data.url; 297 | prefsChanged (); 298 | } 299 | if (flcloud) { //7/25/15 by DW 300 | var urlRssCloudServer = "http://" + headElements.rssCloudDomain + ":" + headElements.rssCloudPort + "/ping"; 301 | rssCloudPing (urlRssCloudServer, data.url); 302 | } 303 | }); 304 | } 305 | function viewPrefs () { 306 | console.log (jsonStringify (appPrefs)); 307 | } 308 | function viewPublishedFile () { 309 | var url = theData.publishedUrl; 310 | if (url.length == 0) { 311 | alertDialog ("Can't view the file because the current essay hasn't been published yet."); 312 | } 313 | else { 314 | window.open (url); 315 | } 316 | } 317 | function viewFeed () { 318 | if (appPrefs.rssFeedUrl.length == 0) { 319 | alertDialog ("Can't view the feed because no essays have been posted."); 320 | } 321 | else { 322 | window.open (appPrefs.rssFeedUrl); 323 | } 324 | } 325 | function applyPrefs () { 326 | theData.authorname = appPrefs.authorName; 327 | theData.authorwebsite = appPrefs.authorWebsite; 328 | 329 | appPrefs.defaultImageUrl = trimWhitespace (appPrefs.defaultImageUrl); //3/8/15 by DW 330 | 331 | prefsChanged (); 332 | } 333 | function newFileData () { //set fields of theData to represent a new file 334 | theData.body = ""; 335 | theData.title = ""; 336 | theData.description = ""; 337 | theData.nameTemplate = appPrefs.nameDefaultTemplate; //3/28/15 by DW 338 | theData.subs = []; 339 | 340 | theData.authorname = appPrefs.authorName; 341 | theData.authorwebsite = appPrefs.authorWebsite; 342 | 343 | theData.img = ""; 344 | theData.when = new Date (); 345 | theData.whenLastSave = new Date (0); 346 | theData.ctSaves = 0; 347 | theData.filePath = "essays/" + padWithZeros (++appPrefs.fileSerialnum, 3) + ".json"; 348 | theData.publishedUrl = ""; 349 | theData.linkJson = ""; //3/24/15 by DW 350 | 351 | addToHistory (); 352 | 353 | appPrefs.lastFilePath = theData.filePath; 354 | appPrefs.lastPublishedUrl = ""; 355 | viewPublishedUrl (); 356 | prefsChanged (); 357 | } 358 | 359 | function setBackgroundImage () { 360 | askDialog ("URL of background image:", theData.img, "Enter the URL of your background image here.", function (s, flcancel) { 361 | if (!flcancel) { 362 | theData.img = s; 363 | prefsChanged (); 364 | } 365 | }); 366 | } 367 | function setEditorFields (titletext, descriptiontext, bodytext) { 368 | $("#idTitleEditor").html (titletext); 369 | var editorTitle = new MediumEditor (".divTitleEditor", { 370 | placeholder: { 371 | text: "Title" 372 | }, 373 | toolbar: { 374 | buttons: defaultEditorButtons, 375 | }, 376 | buttonLabels: "fontawesome", 377 | disableReturn: true 378 | }); 379 | 380 | $("#idDescriptionEditor").html (descriptiontext); 381 | var editorDescription = new MediumEditor (".divDescriptionEditor", { 382 | placeholder: { 383 | text: "Description" 384 | }, 385 | toolbar: { 386 | buttons: defaultEditorButtons, 387 | }, 388 | buttonLabels: "fontawesome", 389 | disableReturn: true 390 | }); 391 | 392 | $("#idBodyEditor").html (bodytext); 393 | var editorBody = new MediumEditor (".divBodyEditor", { 394 | placeholder: { 395 | text: "Tell your story..." 396 | }, 397 | toolbar: { 398 | buttons: defaultEditorButtons, 399 | }, 400 | buttonLabels: "fontawesome" 401 | }); 402 | } 403 | function subsToText (subs) { 404 | var s = ""; 405 | for (var i = 0; i < subs.length; i++) { 406 | s += "

    " + subs [i] + "

    \n"; 407 | } 408 | return (s); 409 | } 410 | 411 | function dataToFields () { 412 | 413 | setEditorFields (theData.title, theData.description, subsToText (theData.subs)); 414 | 415 | 416 | } 417 | function fieldsToData () { 418 | theData.title = $("#idTitleEditor").html (); 419 | theData.description = $("#idDescriptionEditor").html (); 420 | theData.body = $("#idBodyEditor").html (); 421 | theData.subs = theData.body.split ("\n\n"); //4/2/15 by DW 422 | 423 | } 424 | function saveButtonClick (callback) { 425 | var now = new Date (); 426 | 427 | fieldsToData (); 428 | fieldsToHistory (); 429 | 430 | theData.whenLastSave = now; 431 | theData.ctSaves++; 432 | jsontextForLastSave = jsonStringify (theData); 433 | 434 | $("#idSavedStatus").html (""); 435 | twUploadFile (appPrefs.lastFilePath, jsontextForLastSave, "text/plain", true, function (data) { 436 | console.log ("saveButtonClick: " + data.url + " (" + secondsSince (now) + " seconds)"); 437 | $("#idSavedStatus").html ("SAVED"); 438 | if (callback != undefined) { 439 | callback (); 440 | } 441 | }); 442 | } 443 | function newButtonClick () { 444 | confirmDialog ("Erase all fields, starting a new web page?", function () { 445 | newFileData (); 446 | dataToFields (); 447 | saveButtonClick (); 448 | }); 449 | } 450 | function viewPublishedUrl () { 451 | $("#idPublishedUrl").html ("" + appPrefs.lastPublishedUrl + ""); 452 | } 453 | function publishButtonClick (flInteract, callback) { 454 | //Changes 455 | //3/24/15; 6:53:47 PM by DW 456 | //New optional param, flInteract. If false, we don't put up a dialog asking if the user wants to see the rendered file. 457 | //3/21/15; 5:31:07 PM by DW 458 | //There is one specific circumstance where we have to upload twice. If appPrefs.lastPublishedUrl is the empty string, we upload the first time to set the value, then upload again, so that it can be correct in the pagetable. The Facebook metadata needs the canonical URL for the page to be correct. 459 | function markdownProcess (s) { 460 | if (appPrefs.flMarkdownProcess) { 461 | var md = new Markdown.Converter (), theList = s.split ("

    "), markdowntext = ""; 462 | for (var i = 0; i < theList.length; i++) { 463 | var lt = theList [i]; 464 | if ((lt.length > 0) && (lt != "

    ") && (lt != "

    ")) { 465 | markdowntext += "

    " + md.makeHtml (lt) + "

    "; 466 | } 467 | } 468 | return (markdowntext); 469 | } 470 | else { 471 | return (s); 472 | } 473 | } 474 | var now = new Date (), urlTemplate; 475 | if (flInteract === undefined) { 476 | flInteract = true; 477 | } 478 | fieldsToData (); 479 | //set urlTemplate, unicase search 480 | var lowername = stringLower (theData.nameTemplate); 481 | for (var x in config.templates) { 482 | if (stringLower (x) == lowername) { 483 | urlTemplate = config.templates [x]; 484 | break; 485 | } 486 | } 487 | if (urlTemplate === undefined) { //not found, use the first as default 488 | for (var x in config.templates) { 489 | urlTemplate = config.templates [x]; 490 | break; 491 | } 492 | } 493 | 494 | 495 | function uploadOnce (templatetext, callback) { 496 | var username = twGetScreenName (), commentstext = ""; 497 | var filepath = replaceAll (theData.filePath, ".json", ".html"); 498 | var urlpage = appPrefs.lastPublishedUrl; //"http://myword.io/users/" + username + filepath; 499 | var urlimage = theData.img; 500 | if (urlimage.length == 0) { 501 | if (appPrefs.flUseDefaultImage && (appPrefs.defaultImageUrl.length > 0)) { //user has specified a default image, use it 502 | urlimage = appPrefs.defaultImageUrl; 503 | } 504 | else { 505 | urlimage = globalDefaultImageUrl; 506 | } 507 | } 508 | 509 | if (appPrefs.flDisqusComments) {//3/31/15 by DW 510 | commentstext = replaceAll (disqusCode, "[%disqusgroupname%]", appPrefs.disqusGroupName); 511 | } 512 | 513 | var pagetable = { //3/30/15 by DW -- add appPrefs and appConsts to the pagetable 514 | flFromEditor: true, 515 | authorname: theData.authorname, 516 | authorwebsite: theData.authorwebsite, 517 | when: theData.when, 518 | body: theData.body, 519 | pagetitle: theData.title, 520 | ogtitle: theData.title, 521 | ogdescription: theData.description, 522 | ogimage: urlimage, 523 | ogurl: urlpage, 524 | twscreenname: "@" + username, 525 | twtitle: theData.title, 526 | twdescription: theData.description, 527 | twimage: urlimage, 528 | rssfeedurl: appPrefs.rssFeedUrl, 529 | nametemplate: theData.nameTemplate, //3/28/15 by DW 530 | urltemplate: urlTemplate, //3/29/15 by DW 531 | username: username, //8/7/15 PM by DW -- the user's screenname without the leading @ 532 | urltwitterprofile: "https://twitter.com/" + username, //8/7/15 by DW 533 | appConsts: new Object (), //3/30/15 by DW 534 | appPrefs: new Object () //3/30/15 by DW 535 | }; 536 | copyScalars (appConsts, pagetable.appConsts); 537 | copyScalars (appPrefs, pagetable.appPrefs); 538 | pagetable.pagetableinjson = jsonStringify (pagetable); 539 | pagetable.disquscomments = commentstext; //3/31/15 by DW 540 | pagetable.commenttext = commentstext; //4/1/15 by DW -- grandfathered, first version of default template used this name 541 | pagetable.renderedtext = markdownProcess (pagetable.body); //for substitution in the template -- 3/26/15 by DW 542 | 543 | var renderedtext = multipleReplaceAll (templatetext, pagetable, false, "[%", "%]"); 544 | twUploadFile (theData.filePath, pagetable.pagetableinjson, "application/json", false, function (data) { 545 | theData.linkJson = data.url; 546 | twUploadFile (filepath, renderedtext, "text/html", false, function (data) { 547 | console.log ("publishButtonClick: " + data.url + " (" + secondsSince (now) + " seconds)"); 548 | theData.publishedUrl = data.url; 549 | appPrefs.lastPublishedUrl = data.url; //7/29/15 by DW 550 | callback (data); 551 | }); 552 | }); 553 | } 554 | function afterUpload (data) { 555 | viewPublishedUrl (); 556 | prefsChanged (); 557 | feedChanged (); 558 | saveButtonClick (function () { 559 | if (flInteract) { 560 | confirmDialog ("View the published essay?", function () { 561 | window.open (data.url); 562 | if (callback != undefined) { 563 | callback (); 564 | } 565 | }); 566 | } 567 | else { 568 | if (callback != undefined) { 569 | callback (); 570 | } 571 | } 572 | }); 573 | } 574 | readHttpFile (urlTemplate, function (templatetext) { 575 | console.log ("publishButtonClick: read " + templatetext.length + " chars from " + urlTemplate); 576 | uploadOnce (templatetext, function (data) { 577 | if (appPrefs.lastPublishedUrl.length == 0) { //have to upload a second time 578 | appPrefs.lastPublishedUrl = data.url; 579 | theData.publishedUrl = data.url; 580 | uploadOnce (templatetext, function (data) { 581 | afterUpload (data); 582 | }); 583 | } 584 | else { 585 | afterUpload (data); 586 | } 587 | }); 588 | }); 589 | } 590 | function buildTemplateMenu () { //3/28/15 by DW 591 | $("#idTemplateSelect").empty (); 592 | for (x in config.templates) { 593 | $("#idTemplateSelect").append (""); 594 | } 595 | } 596 | function templateMenuSelect () { //3/28/15 by DW 597 | console.log ("templateMenuSelect: you chose == " + $("#idTemplateSelect").val () + " template."); 598 | } 599 | function openEssayFile (callback) { 600 | if (appPrefs.lastFilePath.length == 0) { //first run 601 | newFileData (); 602 | dataToFields (); 603 | jsontextForLastSave = jsonStringify (theData); 604 | if (callback !== undefined) { 605 | callback (); 606 | } 607 | } 608 | else { 609 | console.log ("openEssayFile: appPrefs.lastFilePath == " + appPrefs.lastFilePath); 610 | twGetFile (appPrefs.lastFilePath, true, true, function (error, data) { 611 | if (data != undefined) { 612 | try { 613 | theData = JSON.parse (data.filedata); 614 | if (theData.nameTemplate === undefined) { 615 | theData.nameTemplate = appPrefs.nameDefaultTemplate; 616 | } 617 | console.log ("openEssayFile: " + data.filedata.length + " chars."); 618 | } 619 | catch (err) { 620 | newFileData (); 621 | console.log ("openEssayFile: error == " + err.message); 622 | } 623 | } 624 | else { 625 | newFileData (); 626 | console.log ("openEssayFile: error == " + jsonStringify (error)); 627 | } 628 | dataToFields (); 629 | jsontextForLastSave = jsonStringify (theData); 630 | if (callback != undefined) { 631 | callback (); 632 | } 633 | }); 634 | } 635 | } 636 | function showHideEditor () { 637 | var homeDisplayVal = "none", aboutDisplayVal = "none", startupFailDisplayVal = "none"; 638 | 639 | if (twIsTwitterConnected ()) { 640 | if (flStartupFail) { 641 | startupFailDisplayVal = "block"; 642 | } 643 | else { 644 | homeDisplayVal = "block"; 645 | } 646 | } 647 | else { 648 | aboutDisplayVal = "block"; 649 | } 650 | 651 | $("#idEditor").css ("display", homeDisplayVal); 652 | $("#idLogonMessage").css ("display", aboutDisplayVal); 653 | $("#idStartupFailBody").css ("display", startupFailDisplayVal); 654 | } 655 | function prefsChanged () { 656 | flPrefsChanged = true; 657 | } 658 | function historyChanged () { 659 | flHistoryChanged = true; 660 | } 661 | function feedChanged () { 662 | flFeedChanged = true; 663 | } 664 | function settingsCommand () { 665 | twStorageToPrefs (appPrefs, function () { 666 | prefsDialogShow (); 667 | }); 668 | } 669 | function switchServer () { //3/19/15 by DW 670 | askDialog ("URL of new server:", twStorageData.urlTwitterServer, "Enter the URL of your server here.", function (s, flcancel) { 671 | if (!flcancel) { 672 | localStorage.urlTwitterServer = s; 673 | twStorageData.urlTwitterServer = s; 674 | twDisconnectFromTwitter (); 675 | twConnectToTwitter (); 676 | } 677 | }); 678 | } 679 | function initMenus () { 680 | var cmdKeyPrefix = getCmdKeyPrefix (); //10/6/14 by DW 681 | document.getElementById ("idMenuProductName").innerHTML = appConsts.productnameForDisplay; 682 | document.getElementById ("idMenuAboutProductName").innerHTML = appConsts.productnameForDisplay; 683 | $("#idMenubar .dropdown-menu li").each (function () { 684 | var li = $(this); 685 | var liContent = li.html (); 686 | liContent = liContent.replace ("Cmd-", cmdKeyPrefix); 687 | li.html (liContent); 688 | }); 689 | twUpdateTwitterMenuItem ("idTwitterConnectMenuItem"); 690 | twUpdateTwitterUsername ("idTwitterUsername"); 691 | } 692 | function tellOtherInstancesToQuit () { 693 | randomMysteryString = getRandomPassword (25); 694 | localStorage.youAreNotNeeded = randomMysteryString; 695 | } 696 | function initEditor () { 697 | } 698 | function everySecond () { 699 | var now = clockNow (); 700 | twUpdateTwitterMenuItem ("idTwitterConnectMenuItem"); 701 | twUpdateTwitterUsername ("idTwitterUsername"); 702 | pingGoogleAnalytics (); 703 | showHideEditor (); 704 | 705 | //auto-save 706 | if (secondsSince (whenLastKeystroke) >= 1) { 707 | var jsontext; 708 | fieldsToData (); 709 | jsontext = jsonStringify (theData); 710 | if (jsontext != jsontextForLastSave) { 711 | saveButtonClick (function () { 712 | if (appPrefs.flAutoPublish) { //7/23/15 by DW 713 | publishButtonClick (false); 714 | } 715 | }); 716 | } 717 | } 718 | 719 | if (flPrefsChanged) { 720 | twPrefsToStorage (appPrefs); 721 | flPrefsChanged = false; 722 | } 723 | if (flHistoryChanged) { //3/27/15 by DW 724 | buildHistoryMenu (); 725 | flHistoryChanged = false; 726 | } 727 | if (flFeedChanged) { 728 | myWordBuildRssFeed (); 729 | flFeedChanged = false; 730 | } 731 | 732 | //if another copy of MWE launched, we are not needed, we quit 733 | if (localStorage.youAreNotNeeded != undefined) { 734 | if (localStorage.youAreNotNeeded != randomMysteryString) { 735 | if (ctCloseAttempts < 2) { 736 | ctCloseAttempts++; 737 | window.close (); 738 | } 739 | } 740 | } 741 | } 742 | function onKeyup () { 743 | whenLastUserAction = new Date (); 744 | whenLastKeystroke = whenLastUserAction; 745 | } 746 | function initCmdKeys () { 747 | myCombos = [ 748 | { //Cmd-P to publish 749 | "keys": "meta p", 750 | "is_ordered": true, 751 | "on_keydown": function (ev) { 752 | publishButtonClick (); 753 | event.stopPropagation (); 754 | event.preventDefault (); 755 | return (false); 756 | } 757 | } 758 | ]; 759 | keypress.register_many (myCombos); 760 | } 761 | function startup () { 762 | var flSkipConfigRead = false; 763 | function readConfigJson (flSkipServerSet, callback) { //3/20/15 by DW 764 | readHttpFile (fnameconfig, function (s) { 765 | if (s !== undefined) { //the file exists and was read correctly 766 | try { 767 | var jstruct = JSON.parse (s); 768 | console.log ("readConfigJson: " + fnameconfig + " contains " + jsonStringify (jstruct)); 769 | if (!flSkipServerSet) { 770 | if (jstruct.urlTwitterServer !== undefined) { 771 | twStorageData.urlTwitterServer = jstruct.urlTwitterServer; 772 | console.log ("readConfigJson: twStorageData.urlTwitterServer == " + twStorageData.urlTwitterServer); 773 | } 774 | } 775 | if (jstruct.urlDefaultImage != undefined) { //3/21/15 by DW 776 | globalDefaultImageUrl = jstruct.urlDefaultImage; 777 | console.log ("readConfigJson: globalDefaultImageUrl == " + globalDefaultImageUrl); 778 | } 779 | if (jstruct.googleAnalyticsAccount !== undefined) { //3/26/15 by DW 780 | appConsts.googleAnalyticsAccount = jstruct.googleAnalyticsAccount; 781 | appConsts.domain = stringNthField (window.location.href, "/", 3); //3/22/15 by DW 782 | console.log ("readConfigJson: appConsts.domain == " + appConsts.domain); 783 | console.log ("readConfigJson: appConsts.googleAnalyticsAccount == " + appConsts.googleAnalyticsAccount); 784 | initGoogleAnalytics (); 785 | } 786 | config = jstruct; //3/28/15 by DW -- keep it as a global 787 | if (config.templates === undefined) { //3/30/15 by DW 788 | config.templates = defaultTemplates; 789 | } 790 | buildTemplateMenu (); //3/28/15 by DW 791 | } 792 | catch (err) { 793 | console.log ("readConfigJson: err == " + err); 794 | } 795 | } 796 | callback (); 797 | }); 798 | } 799 | console.log ("startup"); 800 | hitCounter (); 801 | initGoogleAnalytics (); 802 | $("#idTwitterIcon").html (twStorageConsts.fontAwesomeIcon); 803 | $("#idVersionNumber").html ("v" + appConsts.version); 804 | $("#idTitleEditor").keyup (onKeyup); 805 | $("#idDescriptionEditor").keyup (onKeyup); 806 | $("#idBodyEditor").keyup (onKeyup); 807 | initCmdKeys (); 808 | initMenus (); 809 | tellOtherInstancesToQuit (); 810 | twGetOauthParams (); //redirects if OAuth params are present 811 | //determine the URL of the nodeStorage server -- 3/19/15 by DW 812 | if (localStorage.urlTwitterServer !== undefined) { 813 | twStorageData.urlTwitterServer = localStorage.urlTwitterServer; 814 | //if the user specified a server that happens to be our server, patch it up with the new URL 815 | twStorageData.urlTwitterServer = replaceAll (twStorageData.urlTwitterServer, "http://twitter.myword.io/", appConsts.urlTwitterServer); //5/9/15 by DW 816 | flSkipConfigRead = true; //the localStorage value takes precedence 817 | } 818 | else { 819 | twStorageData.urlTwitterServer = appConsts.urlTwitterServer; 820 | } 821 | readConfigJson (flSkipConfigRead, function () { 822 | if (twIsTwitterConnected ()) { 823 | twUserWhitelisted (twGetScreenName (), function (flwhitelisted) { 824 | if (flwhitelisted) { 825 | twStorageStartup (appPrefs, function (flGoodStart) { 826 | flStartupFail = !flGoodStart; 827 | patchPrefs (); //3/27/15 by DW 828 | if (flGoodStart) { 829 | initEditor (); //4/2/15 by DW 830 | openEssayFile (function () { 831 | showHideEditor (); 832 | viewPublishedUrl (); 833 | buildHistoryMenu (); //3/27/15 by DW 834 | appPrefs.ctStartups++; 835 | prefsChanged (); 836 | applyPrefs (); 837 | twGetTwitterConfig (function () { //twStorageData.twitterConfig will have information from twitter.com 838 | twGetUserInfo (twGetScreenName (), function (userData) { 839 | if (appPrefs.authorName.length == 0) { 840 | appPrefs.authorName = userData.name; 841 | theData.authorname = userData.name; //3/21/15 by DW 842 | prefsChanged (); 843 | } 844 | if (appPrefs.authorWebsite.length == 0) { 845 | appPrefs.authorWebsite = userData.url; 846 | theData.authorwebsite = userData.url; //3/21/15 by DW 847 | prefsChanged (); 848 | twDerefUrl (appPrefs.authorWebsite, function (longUrl) { //try to unshorten the URL 849 | appPrefs.authorWebsite = longUrl; 850 | theData.authorwebsite = longUrl; //3/21/15 by DW 851 | prefsChanged (); 852 | }); 853 | } 854 | twitterToPrefs (userData); //fill in RSS prefs with info we got from Twitter 855 | }); 856 | runStartupCode (); //4/3/15 by DW 857 | self.setInterval (everySecond, 1000); 858 | }); 859 | }); 860 | } 861 | }); 862 | } 863 | else { 864 | alertDialog ("Can't open the editor because there was an error connecting to the server, " + twStorageData.urlTwitterServer + ", or the user \"" + twGetScreenName () + "\" is not whitelisted."); 865 | } 866 | }); 867 | } 868 | else { 869 | showHideEditor (); 870 | } 871 | }); 872 | } 873 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Ubuntu; 3 | font-size: 18px; 4 | line-height: 140%; 5 | background-color: whitesmoke; 6 | } 7 | .divPageBody { 8 | width: 65%; 9 | margin-top: 120px; 10 | margin-left: auto; 11 | margin-right: auto; 12 | margin-bottom: 400px; 13 | } 14 | .divBodyEditor { 15 | font-family: "Merriweather"; 16 | font-size: 19px; 17 | letter-spacing: 0.01rem; 18 | font-weight: 400; 19 | font-style: normal; 20 | line-height: 1.69; 21 | padding-left: 6px; 22 | padding-right: 6px; 23 | margin-top: 40px; 24 | min-height: 300px; 25 | } 26 | .divBodyEditor p { 27 | margin-top: 3px; 28 | margin-bottom: 17px; 29 | } 30 | .divBodyEditor h3 { 31 | font-family: "Open Sans"; 32 | font-size: 1.3em; 33 | margin-top: 0; 34 | } 35 | .divBodyEditor h2 { 36 | font-family: "Open Sans"; 37 | font-size: 1.7em; 38 | margin-top: 0; 39 | } 40 | .divTitleEditor { 41 | font-family: "Open Sans"; 42 | font-size: 50px; 43 | font-weight: 800; 44 | letter-spacing: -0.03em; 45 | min-height: 1em; 46 | 47 | padding-top: .3em; 48 | padding-bottom: .3em; 49 | padding-left: 6px; 50 | padding-right: 6px; 51 | line-height: 100%; 52 | } 53 | .divTitleEditor p { 54 | -webkit-margin-before: 0; 55 | -webkit-margin-after: 0; 56 | } 57 | .divDescriptionEditor { 58 | font-family: "Open Sans"; 59 | font-size: 30px; 60 | font-weight: 800; 61 | letter-spacing: -0.03em; 62 | min-height: 1em; 63 | margin-top: 0; 64 | 65 | padding-top: .3em; 66 | padding-bottom: .3em; 67 | padding-left: 6px; 68 | padding-right: 6px; 69 | line-height: 100%; 70 | } 71 | :focus, :visited:focus { 72 | 73 | outline: 1px solid silver; 74 | background-color: white; 75 | 76 | } 77 | .divBelowTheFold { 78 | margin-top: 40px; 79 | } 80 | .divChangeNotes { 81 | margin-top: 25px; 82 | } 83 | .divChangeNotes li { 84 | margin-bottom: .4em; 85 | } 86 | .medium-editor-placeholder:after { 87 | font-style: normal; 88 | font-size: 1em; 89 | top: .3em; 90 | } 91 | .divVersionNumber { 92 | font-size: 12px; 93 | float: right; 94 | padding: 19px; 95 | } 96 | .iMenuCheck { 97 | margin-left: -20px; 98 | margin-right: 4px; 99 | } 100 | -------------------------------------------------------------------------------- /templates/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [%pagetitle%] 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 | [%ogtitle%] 49 |
    50 |
    51 | [%ogdescription%] 52 |
    53 |
    54 |
    55 |
    56 |
    57 | [%renderedtext%] 58 |
    59 |
    60 | [%disquscomments%] 61 |
    62 |
    63 |
    64 |
    65 |
    66 |
    67 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /templates/scripts.js: -------------------------------------------------------------------------------- 1 | /* The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dave Winer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | */ 23 | var appConsts = { 24 | productname: "myWord", 25 | productnameForDisplay: "MyWord Editor", 26 | productLink: "http://myword.io/editor/", 27 | domain: "myword.io", 28 | version: "0.53" 29 | } 30 | var defaultImageUrl = "http://scripting.com/2015/03/06/allmans.png"; 31 | var mySnap, flSnapDrawerOpen = false; 32 | 33 | document.write (''); 34 | document.write (''); 35 | document.write (''); 36 | document.write (''); 37 | document.write (''); 38 | 39 | document.write (''); 40 | document.write (''); 41 | document.write (''); 42 | document.write (''); 43 | 44 | document.write (""); //3/23/15 by DW 45 | document.write (''); 46 | 47 | document.write (""); 48 | document.write (""); 49 | document.write (""); 50 | 51 | document.write (''); 52 | 53 | function viewPagetable () { 54 | console.log (jsonStringify (pagetable)); 55 | } 56 | function readLinksFromRss (url, idBlogPosts, callback) { //3/25/15 by DW 57 | var htmltext = "", indentlevel = 0, whenstart = new Date (); 58 | var thisPageUrl = stringNthField (window.location.href, "?", 1); 59 | function getSub (item, name) { 60 | var url; 61 | $(item).children (name).each (function () { 62 | url = $(this).text (); 63 | }); 64 | return (url); 65 | } 66 | function add (s) { 67 | htmltext += s + "\r\n"; 68 | } 69 | $("#" + idBlogPosts).empty (); 70 | readHttpFile (url, function (xmltext) { 71 | var xstruct = $($.parseXML (xmltext)); 72 | var adrchannel = xmlGetAddress (xstruct, "channel"); 73 | add (""); indentlevel--; 91 | $("#" + idBlogPosts).append (htmltext); 92 | if (callback != undefined) { 93 | callback (); 94 | } 95 | }); 96 | } 97 | function handleHamburgerClick () { //3/25/15 by DW 98 | console.log ("handleHamburgerClick: flSnapDrawerOpen == " + flSnapDrawerOpen); 99 | console.log ("handleHamburgerClick: window.location.href == " + window.location.href); 100 | if (flSnapDrawerOpen) { 101 | mySnap.close ("left"); 102 | if (stringContains (window.location.href, "?tocOpen=true")) { //redirect 103 | window.location.href = stringNthField (window.location.href, "?", 1); 104 | } 105 | flSnapDrawerOpen = false; 106 | } 107 | else { 108 | mySnap.open ("left"); 109 | flSnapDrawerOpen = true; 110 | } 111 | } 112 | function initSnap (flSnapOpenInitially) { //3/25/15 by DW 113 | var snapcontent, openicon; 114 | //add the hamburger in the upper-left corner 115 | $("#idSnapContent").prepend ("
    "); 116 | snapcontent = document.getElementById ("idSnapContent"); 117 | openicon = document.getElementById ("idSnapOpenIcon"); 118 | if (snapcontent != null) { 119 | mySnap = new Snap ({ 120 | element: snapcontent, touchToDrag: false, maxPosition: 400, minPosition: -400, transitionSpeed: 0 121 | }); 122 | } 123 | if (flSnapOpenInitially) { 124 | if (mySnap !== undefined) { 125 | mySnap.open ("left"); 126 | flSnapDrawerOpen = true; 127 | } 128 | } 129 | if (mySnap !== undefined) { 130 | mySnap.settings ({transitionSpeed: 0.3}); 131 | } 132 | if (openicon != null) { 133 | openicon.addEventListener ("click", handleHamburgerClick); 134 | } 135 | } 136 | function everySecond () { 137 | } 138 | function initGoogleAnalytics () { 139 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 140 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 141 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 142 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 143 | 144 | ga('create', 'UA-39531990-1', appConsts.domain); 145 | ga('send', 'pageview'); 146 | } 147 | function emojifyString (s) { 148 | emojify.setConfig ({ 149 | img_dir: "http://fargo.io/code/emojify/images/emoji", 150 | }); 151 | return (emojify.replace (s)); 152 | } 153 | function getFooterText (pagetable) { 154 | var rssLink = ""; 155 | var dateString = new Date (pagetable.when).toLocaleDateString (); 156 | var timeString = new Date (pagetable.when).toLocaleTimeString (); 157 | var username = pagetable.authorname; 158 | var productlink = "" + appConsts.productnameForDisplay + ""; 159 | if (pagetable.authorwebsite.length > 0) { 160 | username = "" + username + ""; 161 | } 162 | return ("

    Created on " + dateString + " at " + timeString + " by " + username + ", using " + productlink + ". " + rssLink + "

    "); 163 | } 164 | function viewPostFromEditor (pagetable) { 165 | var now = new Date (), markdown = new Markdown.Converter (); 166 | //background image 167 | var imgurl = trimWhitespace (pagetable.ogimage); 168 | if (imgurl.length > 0) { 169 | if ($("#idBackgroundImage")) { 170 | $("#idBackgroundImage").css ("background-image", "url(" + imgurl + ")"); 171 | } 172 | } 173 | //byline 174 | if (pagetable.authorname != undefined) { 175 | if ($("#idPageByline")) { 176 | var author = pagetable.authorname, byline; 177 | if (pagetable.authorwebsite != undefined) { 178 | author = "" + author + ""; 179 | } 180 | byline = "By " + author; 181 | if (pagetable.when != undefined) { 182 | var whenstring; 183 | if (sameDay (new Date (pagetable.when), now)) { 184 | whenstring = " at "; 185 | } 186 | else { 187 | whenstring = " on "; 188 | } 189 | byline += whenstring + viewDate (pagetable.when); 190 | } 191 | $("#idPageByline").html (byline + "."); 192 | } 193 | } 194 | //footer text 195 | if ($("#idPageFooter")) { 196 | $("#idPageFooter").html ("
    " + getFooterText (pagetable) + "
    "); 197 | } 198 | //comments -- 7/25/15 by DW 199 | if (!getBoolean (pagetable.flDisqusComments)) { 200 | $("#idDisqusComments").css ("display", "none"); 201 | } 202 | 203 | return; //7/23/15 by DW 204 | 205 | //title 206 | if (pagetable.ogtitle != undefined) { 207 | if ($("#idPageTitle")) { 208 | $("#idPageTitle").html (pagetable.ogtitle); 209 | } 210 | } 211 | if (pagetable.pagetitle != undefined) { 212 | var titleprefix; 213 | if (pagetable.appPrefs.authorName) { 214 | titleprefix = pagetable.appPrefs.authorName; 215 | } 216 | else { 217 | titleprefix = appConsts.productnameForDisplay; 218 | } 219 | document.title = titleprefix + ": " + pagetable.pagetitle; 220 | } 221 | 222 | //description 223 | if (pagetable.ogdescription != undefined) { 224 | if ($("#idPageDescription")) { 225 | $("#idPageDescription").html (pagetable.ogdescription); 226 | } 227 | } 228 | //essay text 229 | if (pagetable.body != undefined) { 230 | if ($("#idEssayText")) { 231 | var essaytext = "
    " + markdown.makeHtml (emojifyString (pagetable.body)) + "
    "; 232 | $("#idEssayText").html (essaytext); 233 | } 234 | } 235 | } 236 | function startup () { 237 | var urlparam = decodeURIComponent (getURLParameter ("url")), urlEssay = "essay.json", jstruct, imgurl = defaultImageUrl; 238 | var tocparam = decodeURIComponent (getURLParameter ("tocOpen")), flSnapOpenInitially = false; 239 | var markdown = new Markdown.Converter (), now = new Date (), flmarkdown; 240 | console.log ("startup"); 241 | $("#idVersionNumber").html ("v" + appConsts.version + ""); 242 | initGoogleAnalytics (); 243 | hitCounter (); 244 | if (urlparam != "null") { 245 | urlEssay = urlparam; 246 | } 247 | if (tocparam != "null") { //3/25/15 by DW 248 | if (getBoolean (tocparam)) { 249 | flSnapOpenInitially = true; 250 | } 251 | } 252 | initSnap (flSnapOpenInitially); 253 | readLinksFromRss (pagetable.rssfeedurl, "idLinksToPosts"); 254 | if ((pagetable !== undefined) && (pagetable.flFromEditor)) { 255 | viewPostFromEditor (pagetable); 256 | self.setInterval (everySecond, 1000); 257 | } 258 | else { 259 | readHttpFile (urlEssay, function (s) { 260 | try { 261 | jstruct = JSON.parse (s); 262 | } 263 | catch (err) { 264 | alertDialog (err + ". Try using jsonlint to find the error in the JSON file."); 265 | return; 266 | } 267 | 268 | flmarkdown = getBoolean (jstruct.flMarkdown); //2/13/15 by DW 269 | 270 | //image 271 | if (jstruct.img != undefined) { 272 | imgurl = jstruct.img; 273 | } 274 | $("#idBackgroundImage").css ("background-image", "url(" + imgurl + ")"); 275 | //title 276 | if (jstruct.title != undefined) { 277 | $("#idPageTitle").html (jstruct.title); 278 | document.title = appConsts.productnameForDisplay + ": " + jstruct.title; 279 | } 280 | //fonts 281 | if (jstruct.titlefont != undefined) { 282 | $("#idPageTopText").css ("font-family", jstruct.titlefont); 283 | } 284 | if (jstruct.bodyfont != undefined) { 285 | $("#idEssayText").css ("font-family", jstruct.bodyfont); 286 | } 287 | 288 | //byline 289 | if (jstruct.authorname != undefined) { 290 | var author = jstruct.authorname, byline; 291 | if (jstruct.authorwebsite != undefined) { 292 | author = "" + author + ""; 293 | } 294 | byline = "By " + author; 295 | if (jstruct.when != undefined) { 296 | var whenstring; 297 | if (sameDay (new Date (jstruct.when), now)) { 298 | whenstring = " at "; 299 | } 300 | else { 301 | whenstring = " on "; 302 | } 303 | byline += whenstring + viewDate (jstruct.when); 304 | } 305 | $("#idPageByline").html (byline + "."); 306 | } 307 | //description 308 | if (jstruct.description != undefined) { 309 | $("#idPageDescription").html (jstruct.description); 310 | } 311 | //essay text 312 | var essaytext = ""; 313 | function dolist (thelist) { 314 | for (var i = 0; i < thelist.length; i++) { 315 | var sub = thelist [i]; 316 | if (typeof sub == "string") { 317 | if (flmarkdown) { 318 | essaytext += sub + "\n\n"; 319 | } 320 | else { 321 | essaytext += "

    " + sub + "

    "; 322 | } 323 | } 324 | else { 325 | if (sub.title != undefined) { 326 | essaytext += "
    " + sub.title + "
    "; 327 | } 328 | if (sub.subs != undefined) { 329 | dolist (sub.subs); 330 | } 331 | } 332 | } 333 | } 334 | dolist (jstruct.subs); 335 | 336 | if (flmarkdown) { 337 | console.log ("startup: essay text before Markdown processing == " + essaytext); 338 | essaytext = "
    " + markdown.makeHtml (essaytext) + "
    "; 339 | } 340 | 341 | $("#idEssayText").html (essaytext); 342 | self.setInterval (everySecond, 1000); 343 | }); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /templates/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Open Sans"; 3 | font-size: 18px; 4 | background-color: whitesmoke; 5 | } 6 | .divBackgroundImage { 7 | position: absolute; 8 | width: 100%; 9 | height: 400px; 10 | margin-left: 0; 11 | margin-top: 0; 12 | border-top: 1px solid silver; 13 | z-index: 0; 14 | 15 | -webkit-background-size: cover; 16 | -moz-background-size: cover; 17 | -o-background-size: cover; 18 | background-size: cover; 19 | background-position: center; 20 | background-repeat: no-repeat; 21 | 22 | background-color: black; 23 | } 24 | .divTextBackground { 25 | position: absolute; 26 | top: 120; 27 | left: 15%; 28 | width: 70%; 29 | height: 170px; 30 | z-index: 10; 31 | background-color: black; 32 | opacity: 0.25; 33 | } 34 | .divPageTopText { 35 | position: absolute; 36 | top: 120; 37 | left: 15%; 38 | width: 70%; 39 | height: 200px; 40 | color: white; 41 | padding-top: 10px; 42 | padding-left: 10px; 43 | z-index: 20; 44 | } 45 | .divPageTopText a { 46 | color: white; 47 | } 48 | .divPageTitle { 49 | font-size: 50px; 50 | letter-spacing: -0.02em; 51 | font-weight: 700; 52 | font-style: normal; 53 | letter-spacing: -0.04em; 54 | line-height: 1.1; 55 | margin-bottom: 8px; 56 | color: white; 57 | text-shadow: 1px 1px black; 58 | } 59 | .divPageByline { 60 | font-size: 18px; 61 | color: white; 62 | margin-bottom: .5em; 63 | text-shadow: 1px 1px black; 64 | } 65 | .divPageDescription { 66 | font-size: 24px; 67 | color: white; 68 | margin-top: .5em; 69 | height: 3em; 70 | line-height: 120%; 71 | text-shadow: 1px 1px black; 72 | } 73 | .divPageBody { 74 | position: absolute; 75 | top: 400; 76 | left: 20%; 77 | width: 60%; 78 | margin-top: 50px; 79 | margin-left: auto; 80 | margin-right: auto; 81 | } 82 | .divEssayText { 83 | font-family: "Lora", "Georgia", Serif; 84 | font-size: 22px; 85 | line-height: 145%; 86 | margin-bottom: 30px; 87 | } 88 | .divEssayText p { 89 | line-height: 145%; 90 | margin-bottom: 1em; 91 | } 92 | .divEssayText li { 93 | line-height: 145%; 94 | } 95 | .divSubhead { 96 | font-family: "Open Sans"; 97 | font-weight: bold; 98 | margin-top: 1em; 99 | margin-bottom: .15em; 100 | } 101 | .divMarkdownText h4 { 102 | font-size: 24px; 103 | margin-top: 1.25em; 104 | margin-bottom: .75em; 105 | } 106 | .divVersionNumber { 107 | position: absolute; 108 | top: 0; 109 | right:0; 110 | font-size: 13px; 111 | padding: 5px; 112 | color: white; 113 | z-index: 20; 114 | } 115 | .divVersionNumber a { 116 | color: white; 117 | } 118 | .divPageFooter { 119 | font-size: 16px; 120 | width: 100%; 121 | margin-bottom: 400px; 122 | padding-bottom: 3em; 123 | } 124 | .divPageFooter a { 125 | cursor: pointer; 126 | color: black; 127 | } 128 | .divPageFooter hr { 129 | border-top: 1px solid silver; 130 | margin: 0; 131 | margin-bottom: 5px; 132 | } 133 | .divPageFooter p { 134 | } 135 | .spFooterRssIcon { 136 | font-size: .9em; 137 | color: orange; 138 | font-weight: bold; 139 | margin-left: 8px; 140 | margin-top: 2px; 141 | } 142 | .divComments { 143 | border: 1px solid silver; 144 | padding: 8px; 145 | background-color: white; 146 | margin-top: 50px; 147 | margin-bottom: 50px; 148 | } 149 | .emoji { 150 | width: 1em; 151 | height: 1em; 152 | display: inline-block; 153 | margin-bottom: 0; 154 | } 155 | 156 | #idSnapOpenIcon { 157 | background: url(http://fargo.io/cms/snap/open.png) center center no-repeat; 158 | display: block; 159 | width: 44px; 160 | height: 44px; 161 | cursor: pointer; 162 | 163 | background-color: black; 164 | float: left; 165 | } 166 | .snap-content { 167 | background-color: whitesmoke; 168 | } 169 | .snap-drawers { 170 | background: gainsboro; 171 | width: 400px; 172 | } 173 | .snap-drawer { 174 | background: gainsboro; 175 | color: #eee; 176 | width: 400px; 177 | } 178 | .divLeftDrawer { 179 | margin-top: 30px; 180 | width: 80%; 181 | margin-left: 10%; 182 | } 183 | .divLeftDrawer h4 { 184 | color: black; 185 | font-size: 18px; 186 | font-weight: bold; 187 | } 188 | .divLinksToPosts { 189 | font-size: 18px; 190 | line-height: 1.5em; 191 | } 192 | .divLinksToPosts a { 193 | color: black; 194 | cursor: pointer; 195 | } 196 | .divLinksToPosts ul { 197 | margin: 0; 198 | list-style-type: none; 199 | } 200 | .divLinksToPosts li { 201 | margin-bottom: 10px; 202 | } 203 | 204 | .divFooterText { 205 | border-top: 1px solid silver; 206 | text-align: center; 207 | } 208 | .divFooterText p { 209 | margin-top: 5px; 210 | } 211 | 212 | 213 | code { 214 | font-size: .9em; 215 | background-color: inherit; 216 | color: inherit; 217 | } 218 | h1, h2, h3, h4 { 219 | font-family: "Open Sans"; 220 | } 221 | 222 | 223 | @media only screen 224 | and (min-device-width : 320px) 225 | and (max-device-width : 480px) { 226 | body { 227 | background-color: white; 228 | } 229 | .divBackgroundImage { 230 | height: 300px; 231 | } 232 | .divPageTopText { 233 | top: 50; 234 | width: 80%; 235 | left: 10%; 236 | } 237 | .divTextBackground { 238 | top: 100; 239 | width: 80%; 240 | left: 10%; 241 | height: 140px; 242 | } 243 | .divPageTitle { 244 | font-size: 44px; 245 | } 246 | .divPageDescription { 247 | font-size: 18px; 248 | line-height: 120%; 249 | } 250 | .divPageBody { 251 | top: 300; 252 | width: 90%; 253 | left: 5%; 254 | margin-top: 20px; 255 | } 256 | .divEssayText { 257 | font-size: 18px; 258 | } 259 | .divVersionNumber { 260 | position: absolute; 261 | padding: 2px; 262 | } 263 | } 264 | --------------------------------------------------------------------------------