├── LICENSE ├── README.md ├── client └── opml.js ├── examples ├── addCategoryToOpml │ ├── addcategorytoopml.js │ ├── package.json │ ├── source.opml │ └── worknotes.md ├── browser │ ├── code.js │ ├── index.html │ └── styles.css ├── includes │ ├── includes.opml │ ├── package.json │ └── test.js ├── markdown │ ├── package.json │ ├── readme.md │ ├── states.md │ └── test.js └── parsing │ ├── package.json │ ├── states.opml │ └── test.js ├── opmlpackage.js ├── package.json ├── source.opml └── worknotes.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opml package 2 | 3 | A developer's toolkit for OPML support. Node and browser-based JavaScript code that reads and writes OPML. 4 | 5 | #### What is OPML and why should we use it? 6 | 7 | OPML is an XML-based format designed to store and exchange outlines with attributes. 8 | 9 | It's been around since the early 2000s, and is widely used in the RSS world to exchange subscription lists. 10 | 11 | It's also a standard for interop among outliners. If you support OPML, our products will interop, and our users will be able to use all our products on their data. 12 | 13 | #### Why this package? 14 | 15 | I wanted to make it really easy for developers to add basic OPML support to their apps. 16 | 17 | So I put the basic code you need to read and write OPML files, code that's debugged, used in other apps, including my own. It's known to work, to respect the format, and be easy to deploy. 18 | 19 | There are other ways to read OPML, and that's very good. The more support there is, the more interop and that's the goal. I'm going to accumulate links to resources for OPML developers. If you have something you think they could use, send me an email at the address in the package.json file above. 20 | 21 | I recorded a podcast explaining all this. :-) 22 | 23 | #### What's in this package? 24 | 25 | JavaScript code to parse and stringify OPML. 26 | 27 | * opml.parse -- turns OPML text into a JavaScript structure representing the OPML. 28 | 29 | * opml.stringify -- takes the JavaScript structure and turns it into OPML text. 30 | 31 | * opml.htmlify -- a simple routine to display outlines in HTML. 32 | 33 | * opml.visitAll -- a routine that visits all the nodes in an outline. 34 | 35 | #### The Node package 36 | 37 | It's on NPM, it's called OPML. 38 | 39 | Here's a snippet that reads an OPML file, converts it to a JavaScript object, displays it to the console via JSON.stringify. 40 | 41 | ```javascript const fs = require ("fs"); const opml = require ("opml"); fs.readFile ("states.opml", function (err, opmltext) { if (!err) { opml.parse (opmltext, function (err, theOutline) { if (!err) { console.log (JSON.stringify (theOutline, undefined, 4)); } }); } }); ``` 42 | 43 | The full Node example is here. 44 | 45 | #### OPML in the browser 46 | 47 | The same routines are available for JavaScript code running in the browser. 48 | 49 | See the example. You have to include opml.js in your app, as the example does. 50 | 51 | You can run the example right now, without downloading the repo. 52 | 53 | The example app includes the Bootstrap Toolkit, the Ubuntu font and jQuery. The OPML parsing and generating code does not depend on the first two, they're just used in the example app. opml.js uses jQuery, but otherwise should be completely self-contained, i.e. it does not depend on any other files. 54 | 55 | #### Other OPML projects 56 | 57 | I have a few OPML-related projects on GitHub and on the web. 58 | 59 | * Drummer -- Browser and Electron-based outliner that uses OPML as its native format. 60 | 61 | * OPML Developer checklist. Examples, source code and advice for people adapting outliners to read and write OPML. 62 | 63 | * instantOutlines -- Example code for sharing live-updated outlines between users, using websockets as the notification system. 64 | 65 | * OPML 2.0 spec. 66 | 67 | * etc. 68 | 69 | #### Updates 70 | 71 | #### v0.5.0 -- 10/25/22 by DW 72 | 73 | New function -- opml.readOutline. A simple bit of recurring code. Reads an outline over the web returns a standard outline object. 74 | 75 | It's time to start a fresh sequence of versions with 0.5.0. No breakage, of course. ;-) 76 | 77 | #### v0.4.24 -- 5/11/22 by DW 78 | 79 | New function -- opml.expandIncludes. 80 | 81 | Takes two params, an outline that could possibly have include nodes, and a callback, that receives a copy of the outline with includes expanded. 82 | 83 | It doesn't stop for errors. This was much-debated internally, but there is linkrot and an outline with includes should work as well as it possibly can, as a blog with one broken link still works. 84 | 85 | It is only available in the Node version, but it could be adapted to work in the client if opml.expandInclude (sic) is converted. 86 | 87 | BTW, sorry for the closeness in the names, expandInclude and expandIncludes, but it is correct, one is singular and the other is plural. 88 | 89 | #### v0.4.23 -- 3/18/22 by DW 90 | 91 | opml.visitAll is now defined in both the Node and browser versions. Previously it was only defined in browser version. 92 | 93 | #### v0.4.22 -- 1/12/22 by DW 94 | 95 | New optional param on markdownToOutline, options, an object. 96 | 97 | And options.flAddUnderscores, defaults true. If true, we add underscores before attribute names coming from the markdown, so we know to restore them when converting back to markdown. 98 | 99 | But sometimes the outline is going to OPML, and on to a processor where you want it to recognize its name without the underscore. 100 | 101 | When we're publishing a blog from a LogSeq markdown outline is an example. 102 | 103 | #### v0.4.21 -- 1/8/22 by DW 104 | 105 | There was some confusion about whether or not we should try to handle head-level atts in the markdown format, and in the end I decided not to try to do that at this time. Before doing this I have to understand much better what's going on on the other side of the interop. At this stage, whatever I do is going to be wrong, and will have to be grandfathered in for perpetuity. We have a pretty good ability to interop on the content of the outlines, but different products see the file-level metadata very differently. If there ever is an agreement on how this should work it's going to happen later. 106 | 107 | However I did fix a problem, if a head-level att does appear as we import, we don't try to attach it to undefined. 108 | 109 | #### v0.4.17 -- 1/4/22 by DW 110 | 111 | Added expandInclude in Node package. 112 | 113 | #### v0.4.15 -- 1/4/22 by DW 114 | 115 | Last night's release only worked in the client version. The Node package was broken. It should now be fixed. 116 | 117 | Also added a new example app that demonstrates the reading and writing of markdown/outline files in a Node app. 118 | 119 | #### v0.4.12 -- 1/3/22 by DW 120 | 121 | Two new routines, opml.markdownToOutline and opml.outlineToMarkdown, to read and write markdown files that are used to represent outlines. This is an extended Markdown that LogSeq generates. The format does not have a name at this time, or as far as I know, a spec, but at least now there is JavaScript code that reads and writes the format. 122 | 123 | We are using this code in a new version of Drummer in the works. 124 | 125 | It is being discussed in this thread. 126 | 127 | #### v0.4.10 -- 9/24/21 by DW 128 | 129 | New entry-point in the client, opml.read. 130 | 131 | Reads an OPML file, returns a JavaScript object with the outline head and structure. 132 | 133 | If options.flSubscribe is true, we ask to be notified when the file changes over a websocket. 134 | 135 | We call back to the same routine we called when the file was read, assuming it will do the same thing with the updated OPML. 136 | 137 | #### Questions, comments? 138 | 139 | If you have any questions or comments please post an issue here. 140 | 141 | -------------------------------------------------------------------------------- /client/opml.js: -------------------------------------------------------------------------------- 1 | const opml = { 2 | parse: opmlParse, 3 | stringify: opmlStringify, 4 | htmlify: getOutlineHtml, 5 | read: readOutline, //9/24/21 by DW 6 | visitAll: visitAll, 7 | markdownToOutline, //1/3/22 by DW 8 | outlineToMarkdown //1/3/22 by DW 9 | }; 10 | 11 | function filledString (ch, ct) { //6/4/14 by DW 12 | var s = ""; 13 | for (var i = 0; i < ct; i++) { 14 | s += ch; 15 | } 16 | return (s); 17 | } 18 | function encodeXml (s) { //7/15/14 by DW 19 | //Changes 20 | //12/14/15; 4:28:14 PM by DW 21 | //Check for undefined, return empty string. 22 | if (s === undefined) { 23 | return (""); 24 | } 25 | else { 26 | var charMap = { 27 | '<': '<', 28 | '>': '>', 29 | '&': '&', 30 | '"': '&'+'quot;' 31 | }; 32 | s = s.toString(); 33 | s = s.replace(/\u00A0/g, " "); 34 | var escaped = s.replace(/[<>&"]/g, function(ch) { 35 | return charMap [ch]; 36 | }); 37 | return escaped; 38 | } 39 | } 40 | function xmlCompile (xmltext) { //3/27/17 by DW 41 | return ($($.parseXML (xmltext))); 42 | } 43 | function xmlGatherAttributes (adrx, theTable) { 44 | if (adrx.attributes != undefined) { 45 | for (var i = 0; i < adrx.attributes.length; i++) { 46 | var att = adrx.attributes [i]; 47 | if (att.specified) { 48 | theTable [att.name] = att.value; 49 | } 50 | } 51 | } 52 | } 53 | function xmlGetAttribute (adrx, name) { 54 | return ($(adrx).attr (name)); 55 | } 56 | function xmlGetAddress (adrx, name) { 57 | return (adrx.find (name)); 58 | } 59 | function xmlGetSubValues (adrx) { //10/12/16 by DW 60 | //Changes 61 | //10/12/16; 11:25:15 AM by DW 62 | //Return a JS object with the values of all the sub-elements of adrx. 63 | var values = new Object (); 64 | $(adrx).children ().each (function () { 65 | var name = xmlGetNodeNameProp (this); 66 | if (name.length > 0) { 67 | var val = $(this).prop ("textContent"); 68 | //name = "opml" + string.upper (name [0]) + string.mid (name, 2, name.length - 1); 69 | values [name] = val; 70 | } 71 | }); 72 | return (values); 73 | } 74 | function xmlGetNodeNameProp (adrx) { //12/10/13 by DW 75 | return ($(adrx).prop ("nodeName")); 76 | } 77 | function xmlHasSubs (adrx) { 78 | return ($(adrx).children ().length > 0); //use jQuery to get answer -- 12/30/13 by DW 79 | }; 80 | 81 | function outlineToJson (adrx, nameOutlineElement) { //12/25/20 by DW 82 | //Changes 83 | //10/20/14; 5:54:44 PM by DW 84 | //Convert a structure from an RSS item into a jstruct. 85 | var theOutline = new Object (); 86 | if (nameOutlineElement === undefined) { 87 | nameOutlineElement = "outline"; 88 | } 89 | xmlGatherAttributes (adrx, theOutline); 90 | if (xmlHasSubs (adrx)) { 91 | theOutline.subs = []; 92 | $(adrx).children (nameOutlineElement).each (function () { 93 | theOutline.subs [theOutline.subs.length] = outlineToJson (this, nameOutlineElement); 94 | }); 95 | } 96 | return (theOutline); 97 | } 98 | function markdownToOutline (mdtext, options) { //1/3/22 by DW 99 | //Changes 100 | //1/12/22; 5:17:25 PM by DW 101 | //New optional param, options. 102 | //options.flAddUnderscores, defaults true. 103 | //1/8/22; 10:54:14 AM by DW 104 | //Any atts that show up at the beginning of a file are ignored. Previously they would cause the process to crash. 105 | //1/3/22; 5:50:36 PM by DW 106 | //Turn a markdown file as created by LogSeq or a compatible product 107 | //into an outline structure compatible with the one that is created from 108 | //parsing OPML text. 109 | var theOutline = { 110 | opml: { 111 | head: { 112 | }, 113 | body: { 114 | subs: new Array () 115 | } 116 | } 117 | }; 118 | 119 | if (options === undefined) { //1/12/22 by DW 120 | options = new Object (); 121 | } 122 | if (options.flAddUnderscores === undefined) { 123 | options.flAddUnderscores = true; 124 | } 125 | 126 | mdtext = mdtext.toString (); 127 | var lines = mdtext.split ("\n"), lastlevel = 0, lastnode = undefined, currentsubs = theOutline.opml.body.subs, stack = new Array (); 128 | lines.forEach (function (theLine) { 129 | var thislevel = 0, flInsert = true; 130 | while (theLine.length > 0) { 131 | if (theLine [0] != "\t") { 132 | break; 133 | } 134 | thislevel++; 135 | theLine = stringDelete (theLine, 1, 1); 136 | } 137 | if (beginsWith (theLine, "- ")) { 138 | theLine = stringDelete (theLine, 1, 2); 139 | } 140 | else { //is the line an attribute? 141 | if (stringContains (theLine, ":: ")) { 142 | let parts = theLine.split (":: "); 143 | if (lastnode !== undefined) { //1/8/22 by DW 144 | var name = (options.flAddUnderscores) ? "_" + parts [0] : parts [0]; //1/12/22 by DW 145 | lastnode [name] = parts [1]; 146 | //lastnode ["_" + parts [0]] = parts [1]; 147 | } 148 | flInsert = false; 149 | } 150 | } 151 | if (thislevel > lastlevel) { 152 | stack.push (currentsubs); 153 | lastnode.subs = new Array (); 154 | currentsubs = lastnode.subs; 155 | } 156 | else { 157 | if (thislevel < lastlevel) { 158 | var ctpops = lastlevel - thislevel; 159 | for (var i = 1; i <= ctpops; i++) { 160 | currentsubs = stack.pop (); 161 | } 162 | } 163 | } 164 | 165 | if (flInsert) { 166 | var newnode = { 167 | text: theLine 168 | } 169 | currentsubs.push (newnode); 170 | lastnode = newnode; 171 | lastlevel = thislevel; 172 | } 173 | }); 174 | return (theOutline); 175 | } 176 | function outlineToMarkdown (theOutline) { //1/3/22 by DW 177 | //Changes 178 | //1/3/22; 6:03:00 PM by DW 179 | //Generate markdown text from the indicated outline structure 180 | //that can be read by LogSeq and compatible apps. 181 | var mdtext = "", indentlevel = 0; 182 | function add (s) { 183 | mdtext += filledString ("\t", indentlevel) + s + "\n"; 184 | } 185 | function addAtts (atts) { 186 | for (var x in atts) { 187 | if ((x != "subs") && (x != "text")) { 188 | if (beginsWith (x, "_")) { 189 | add (stringDelete (x, 1, 1) + ":: " + atts [x]); 190 | } 191 | } 192 | } 193 | } 194 | function dolevel (theNode) { 195 | theNode.subs.forEach (function (sub) { 196 | add ("- " + sub.text); 197 | addAtts (sub); 198 | if (sub.subs !== undefined) { 199 | indentlevel++; 200 | dolevel (sub); 201 | indentlevel--; 202 | } 203 | }); 204 | } 205 | //addAtts (theOutline.opml.head); 206 | dolevel (theOutline.opml.body) 207 | return (mdtext); 208 | } 209 | 210 | function opmlParse (opmltext) { 211 | //Changes 212 | //12/16/21; 11:43:21 AM by DW 213 | //If opmltext is not valid XML, display a message in the console. 214 | //6/13/21; 9:49:51 AM by DW 215 | //Generate a JavaScript object from OPML text. 216 | var xstruct; 217 | try { 218 | xstruct = xmlCompile (opmltext); 219 | } 220 | catch (err) { 221 | console.log ("opmlParse: invalid XML."); 222 | throw err; 223 | } 224 | 225 | var adrhead = xmlGetAddress (xstruct, "head"); 226 | var adrbody = xmlGetAddress (xstruct, "body"); 227 | var theObject = { 228 | opml: { 229 | head: xmlGetSubValues (adrhead), 230 | body: outlineToJson (adrbody) 231 | } 232 | } 233 | return (theObject); 234 | } 235 | function opmlStringify (theOutline) { //returns the opmltext for the outline -- 8/6/17 by DW 236 | var opmltext = "", indentlevel = 0; 237 | function add (s) { 238 | opmltext += filledString ("\t", indentlevel) + s + "\n"; 239 | } 240 | function addSubs (subs) { 241 | if (subs !== undefined) { 242 | for (var i = 0; i < subs.length; i++) { 243 | let sub = subs [i], atts = ""; 244 | for (var x in sub) { 245 | if (x != "subs") { 246 | atts += " " + x + "=\"" + encodeXml (sub [x]) + "\""; 247 | } 248 | } 249 | if (sub.subs === undefined) { 250 | add (""); 251 | } 252 | else { 253 | add (""); indentlevel++; 254 | addSubs (sub.subs); 255 | add (""); indentlevel--; 256 | } 257 | } 258 | } 259 | } 260 | add (""); 261 | add (""); indentlevel++; 262 | //do head section 263 | add (""); indentlevel++; 264 | for (var x in theOutline.opml.head) { 265 | add ("<" + x + ">" + theOutline.opml.head [x] + ""); 266 | } 267 | add (""); indentlevel--; 268 | //do body section 269 | add (""); indentlevel++; 270 | addSubs (theOutline.opml.body.subs); 271 | add (""); indentlevel--; 272 | add (""); indentlevel--; 273 | //console.log ("opmlify: opmltext == \n" + opmltext); 274 | return (opmltext); 275 | } 276 | function getOutlineHtml (theOutline) { 277 | var htmltext = "", indentlevel = 0; //5/24/24 by DW 278 | function add (s) { 279 | htmltext += filledString ("\t", indentlevel) + s + "\n"; 280 | } 281 | function addSubsHtml (node) { 282 | add ("
    "); indentlevel++; 283 | node.subs.forEach (function (sub) { 284 | add ("
  • " + sub.text + "
  • "); 285 | if (sub.subs !== undefined) { 286 | addSubsHtml (sub); 287 | } 288 | }); 289 | add ("
"); indentlevel--; 290 | } 291 | addSubsHtml (theOutline.opml.body); 292 | return (htmltext); 293 | } 294 | function visitAll (theOutline, callback) { 295 | function visitSubs (theNode) { 296 | if (theNode.subs !== undefined) { 297 | for (var i = 0; i < theNode.subs.length; i++) { 298 | var theSub = theNode.subs [i]; 299 | if (!callback (theSub)) { 300 | return (false); 301 | } 302 | if (!visitSubs (theSub)) { //9/7/24 by DW -- see worknotes comment 303 | return (false); 304 | } 305 | } 306 | } 307 | return (true); 308 | } 309 | visitSubs (theOutline.opml.body); 310 | } 311 | 312 | function readOutline (urlOpmlFile, options, callback) { //9/24/21 by DW 313 | //Changes 314 | //9/27/21; 1:57:08 PM by DW 315 | //If options is not defined, initialize it to a default object. 316 | //9/24/21; 1:51:52 PM by DW 317 | //Read the outline over HTTP. If options.flSubscribe is present and true, we set up a websockets connection if the outline supports it, and calll back when it updates. 318 | var mySocket = undefined, urlSocketServer; 319 | function beginsWith (s, possibleBeginning, flUnicase) { 320 | if (s === undefined) { //7/15/15 by DW 321 | return (false); 322 | } 323 | if (s.length == 0) { //1/1/14 by DW 324 | return (false); 325 | } 326 | if (flUnicase === undefined) { 327 | flUnicase = true; 328 | } 329 | if (flUnicase) { 330 | for (var i = 0; i < possibleBeginning.length; i++) { 331 | if (stringLower (s [i]) != stringLower (possibleBeginning [i])) { 332 | return (false); 333 | } 334 | } 335 | } 336 | else { 337 | for (var i = 0; i < possibleBeginning.length; i++) { 338 | if (s [i] != possibleBeginning [i]) { 339 | return (false); 340 | } 341 | } 342 | } 343 | return (true); 344 | } 345 | function readHttpFile (url, timeoutInMilliseconds, headers, callback) { 346 | if (timeoutInMilliseconds === undefined) { 347 | timeoutInMilliseconds = 5000; 348 | } 349 | if (headers === undefined) { 350 | headers = new Object (); 351 | } 352 | var jxhr = $.ajax ({ 353 | url: url, 354 | dataType: "text", 355 | headers: headers, 356 | timeout: timeoutInMilliseconds 357 | }) 358 | .success (function (data, status) { 359 | callback (undefined, data); 360 | }) 361 | .error (function (status) { 362 | callback (status); 363 | }); 364 | } 365 | function wsWatchForChange () { //connect with socket server, if not already connected 366 | if (mySocket === undefined) { 367 | mySocket = new WebSocket (urlSocketServer); 368 | mySocket.onopen = function (evt) { 369 | var msg = "watch " + urlOpmlFile; 370 | mySocket.send (msg); 371 | console.log ("wsWatchForChange: socket is open. sent msg == " + msg); 372 | }; 373 | mySocket.onmessage = function (evt) { 374 | var s = evt.data; 375 | if (s !== undefined) { //no error 376 | const updatekey = "update\r"; 377 | if (beginsWith (s, updatekey)) { //it's an update 378 | var opmltext = stringDelete (s, 1, updatekey.length); 379 | console.log ("wsWatchForChange: update received along with " + opmltext.length + " chars of OPML text."); 380 | callback (undefined, opmlParse (opmltext)); 381 | } 382 | } 383 | }; 384 | mySocket.onclose = function (evt) { 385 | mySocket = undefined; 386 | }; 387 | mySocket.onerror = function (evt) { 388 | console.log ("wsWatchForChange: socket for outline " + urlOpmlFile + " received an error."); 389 | }; 390 | } 391 | } 392 | 393 | if (options === undefined) { //9/27/21 by DW 394 | options = { 395 | flSubscribe: false 396 | }; 397 | } 398 | 399 | readHttpFile (urlOpmlFile, undefined, undefined, function (err, opmltext) { 400 | if (err) { 401 | callback (err); 402 | } 403 | else { 404 | if (options.flSubscribe) { 405 | var theOutline = opmlParse (opmltext); 406 | urlSocketServer = theOutline.opml.head.urlUpdateSocket; 407 | wsWatchForChange (); //connect with socket server 408 | self.setInterval (wsWatchForChange, 1000); //make sure we stay connected 409 | callback (undefined, theOutline); 410 | } 411 | else { 412 | callback (undefined, opmlParse (opmltext)); 413 | } 414 | } 415 | }); 416 | } 417 | -------------------------------------------------------------------------------- /examples/addCategoryToOpml/addcategorytoopml.js: -------------------------------------------------------------------------------- 1 | var myProductName = "addcategorytoopml", myVersion = "0.4.0"; 2 | 3 | const fs = require ("fs"); 4 | const utils = require ("daveutils"); 5 | const opml = require ("opml"); 6 | 7 | const fsource = "wpspecialprojects.opml"; 8 | const theCategory = "all,wp-projects"; 9 | const fdest = "/users/davewiner/dropbox/portableDave/publicFolder/a8c/subscriptionLists/wpspecialprojects.opml"; 10 | 11 | 12 | 13 | function notComment (item) { //8/21/22 by DW 14 | return (!utils.getBoolean (item.isComment)); 15 | } 16 | 17 | fs.readFile (fsource, function (err, opmltext) { 18 | if (err) { 19 | console.log (err.message); 20 | } 21 | else { 22 | opml.parse (opmltext, function (err, theOutline) { 23 | if (err) { 24 | console.log (err.message); 25 | } 26 | else { 27 | opml.visitAll (theOutline, function (node) { 28 | if (notComment (node)) { 29 | if (node.type == "rss") { 30 | if (node.xmlUrl !== undefined) { 31 | node.category = theCategory; 32 | } 33 | } 34 | } 35 | return (true); //keep visiting 36 | }); 37 | const opmltext = opml.stringify (theOutline); 38 | console.log (opmltext); 39 | fs.writeFile (fdest, opmltext, function (err) { 40 | if (err) { 41 | console.log (err.message); 42 | } 43 | }); 44 | } 45 | }); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /examples/addCategoryToOpml/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "addcategorytoopml", 3 | "description": "Add a category att to every element in an outline.", 4 | "version": "0.4.0", 5 | "main": "addcategorytoopml.js", 6 | "dependencies" : { 7 | "opml": "*", 8 | "daveutils": "*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/addCategoryToOpml/source.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | nodeEditor: addCategoryToOpml 18 | Sun, 20 Aug 2023 14:39:07 GMT 19 | Sun, 20 Aug 2023 14:57:24 GMT 20 | Dave Winer 21 | http://davewiner.com/ 22 | 1, 2, 5, 24, 26, 28, 30, 40 23 | 1 24 | 175 25 | 744 26 | 1125 27 | 2047 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 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /examples/addCategoryToOpml/worknotes.md: -------------------------------------------------------------------------------- 1 | #### 8/20/23; 10:39:33 AM by DW 2 | 3 | Started. Configure it with constants in the source code. It takes an OPML subscription list and adds a category att to each node of type rss, with the specified value. 4 | 5 | -------------------------------------------------------------------------------- /examples/browser/code.js: -------------------------------------------------------------------------------- 1 | //7/3/21; 3:27:01 PM by DW 2 | //Read an OPML file over the web, parse it into a JavaScript object. 3 | //1. Display the JSON text for the object in the first box. 4 | //2. Convert the JavaScript object to OPML text, and display that in the second box. 5 | //3. Generate an HTML rendering of the JavaScript object and display in third box. 6 | //Also display, in the JavaScript console: 7 | //The outline's title from the head section and 8 | //The text of the third child of the second top level subhead of the United States. 9 | //Illustrates how you get data from the compiled structure. 10 | //Once you've compiled the OPML, you process it as a JavaScript object. 11 | //When you're done, you can serialize it with opml.stringify. 12 | //By design, works like JSON, so every JS programmer should find this familiar. 13 | //Visit every node in the outline, and convert the text to upper case. 14 | const urlOpmlFile = "http://drummer.scripting.com/davewiner/states.opml"; 15 | 16 | function filledString (ch, ct) { //6/4/14 by DW 17 | var s = ""; 18 | for (var i = 0; i < ct; i++) { 19 | s += ch; 20 | } 21 | return (s); 22 | } 23 | function encodeXml (s) { //7/15/14 by DW 24 | //Changes 25 | //12/14/15; 4:28:14 PM by DW 26 | //Check for undefined, return empty string. 27 | if (s === undefined) { 28 | return (""); 29 | } 30 | else { 31 | var charMap = { 32 | '<': '<', 33 | '>': '>', 34 | '&': '&', 35 | '"': '&'+'quot;' 36 | }; 37 | s = s.toString(); 38 | s = s.replace(/\u00A0/g, " "); 39 | var escaped = s.replace(/[<>&"]/g, function(ch) { 40 | return charMap [ch]; 41 | }); 42 | return escaped; 43 | } 44 | } 45 | function readHttpFile (url, callback, timeoutInMilliseconds, headers) { //5/27/14 by DW 46 | //Changes 47 | //7/17/15; 10:43:16 AM by DW 48 | //New optional param, headers. 49 | //12/14/14; 5:38:18 PM by DW 50 | //Add optional timeoutInMilliseconds param. 51 | //5/29/14; 11:13:28 AM by DW 52 | //On error, call the callback with an undefined parameter. 53 | //5/27/14; 8:31:21 AM by DW 54 | //Simple asynchronous file read over http. 55 | if (timeoutInMilliseconds === undefined) { 56 | timeoutInMilliseconds = 30000; 57 | } 58 | var jxhr = $.ajax ({ 59 | url: url, 60 | dataType: "text", 61 | headers: headers, 62 | timeout: timeoutInMilliseconds 63 | }) 64 | .success (function (data, status) { 65 | callback (data); 66 | }) 67 | .error (function (status) { 68 | //for info about timeous see this page. 69 | //http://stackoverflow.com/questions/3543683/determine-if-ajax-error-is-a-timeout 70 | console.log ("readHttpFile: url == " + url + ", error == " + jsonStringify (status)); 71 | callback (undefined); 72 | }); 73 | } 74 | function startup () { 75 | console.log ("startup"); 76 | readHttpFile (urlOpmlFile, function (opmltext) { 77 | if (opmltext !== undefined) { 78 | var theOutline = opml.parse (opmltext); 79 | 80 | var jsontext = JSON.stringify (theOutline, undefined, 4); 81 | $("#idJsonViewer").text (jsontext); 82 | 83 | var xmltext = opml.stringify (theOutline); 84 | $("#idOpmlViewer").text (xmltext); 85 | 86 | var htmltext = opml.htmlify (theOutline); 87 | $("#idOutlineViewer").html (htmltext); 88 | 89 | console.log ("\nThe outline's title is \"" + theOutline.opml.head.title + ".\""); //see comment at top 90 | console.log ("The third state in the Great Plains is: \"" + theOutline.opml.body.subs [0].subs [1].subs [2].text + ".\""); 91 | 92 | opml.visitAll (theOutline, function (node) { 93 | node.text = node.text.toUpperCase (); 94 | return (true); //keep visiting 95 | }); 96 | console.log (opml.stringify (theOutline)); //view the uppercased outline in the JS console 97 | } 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /examples/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | OPML client demo 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

This is part of the OPML package.

16 |

OPML client demo

17 |

Below, we display the States outline as a JavaScript object, a simple HTML rendering and as an OPML file.

18 |
19 | 				
20 |
21 |
22 |
23 | 				
24 |
25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/browser/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Ubuntu; 3 | font-size: 16px; 4 | background-color: whitesmoke; 5 | } 6 | .divPageBody { 7 | width: 60%; 8 | margin-top: 30px; 9 | margin-left: auto; 10 | margin-right: auto; 11 | margin-bottom: 400px; 12 | } 13 | .divPageBody h1 { 14 | margin-top: 20px; 15 | margin-bottom: 20px; 16 | } 17 | .divPageBody p { 18 | line-height: 140%; 19 | margin-bottom: 25px; 20 | } 21 | 22 | .divOutlineViewer, .divJsonViewer, .divOpmlViewer { 23 | margin-top: 15px; 24 | padding: 3px; 25 | border: 1px solid silver; 26 | } 27 | .divOutlineViewer li { 28 | padding-top: 3px; 29 | padding-bottom: 3px; 30 | } 31 | .divOutlineViewer ul { 32 | list-style-type: circle; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /examples/includes/includes.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | examples/includes/includes.opml 5 | Mon, 09 Dec 2024 14:22:58 GMT 6 | 7 | 1 8 | 300 9 | 700 10 | 900 11 | 1500 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/includes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "description": "Test opml.expandIncludes.", 4 | "author": "Dave Winer ", 5 | "version": "0.4.0", 6 | "license": "MIT", 7 | "main": "test.js", 8 | "dependencies" : { 9 | "opml": "*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/includes/test.js: -------------------------------------------------------------------------------- 1 | //5/11/22; 4:59:24 PM by DW 2 | //Read an OPML file that has includes. 3 | //Pass it through opml.expandIncludes. 4 | //Display the resulting outline, with the includes expanded. 5 | 6 | const fs = require ("fs"); 7 | const opml = require ("opml"); 8 | 9 | fs.readFile ("includes.opml", function (err, opmltext) { 10 | if (err) { 11 | console.log (err.message); 12 | } 13 | else { 14 | opml.parse (opmltext, function (err, theOutline) { //convert OPML text into a JavaScript structure 15 | opml.expandIncludes (theOutline, function (theNewOutline) { 16 | console.log (JSON.stringify (theNewOutline, undefined, 4)); 17 | }); 18 | }); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /examples/markdown/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "description": "Test reading and writing markdown/outline files.", 4 | "author": "Dave Winer ", 5 | "version": "0.4.0", 6 | "license": "MIT", 7 | "main": "test.js", 8 | "dependencies" : { 9 | "daveutils": "*", 10 | "opml": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/markdown/readme.md: -------------------------------------------------------------------------------- 1 | # Markdown/outline demo app 2 | 3 | This app illustrates the reading and writing of outlines from an extended version of Markdown that some outliners use to exchange user structures. 4 | 5 | ### The name of the format 6 | 7 | This format doesn't appear to have a name, so we're using the name markdown/outline in the docs and code. 8 | 9 | ### What test.js does 10 | 11 | There's an example file, states.md, in the folder with the app. It was produced in LogSeq, an outliner that uses this format. 12 | 13 | The app reads the file, then converts it to a JavaScript oijbect called theOutline, by calling opml.markdownToOutline, a routine provided by the OPML package. 14 | 15 | Then it writes the structure to two files: newStates.md and newStates.opml, by calling two routines provided by the OPML package. There are no differences in the data stored in these files, the two formats are exactly equivalent. 16 | 17 | ### Where to discuss 18 | 19 | A thread on the DrummerRFC site where this work is being discussed. 20 | 21 | -------------------------------------------------------------------------------- /examples/markdown/states.md: -------------------------------------------------------------------------------- 1 | - United States 2 | - Far West 3 | - Alaska 4 | - California 5 | capital:: Sacramento 6 | - Hawaii 7 | - Nevada 8 | - Oregon 9 | - Washington 10 | - Great Plains 11 | - Kansas 12 | - Nebraska 13 | - North Dakota 14 | - Oklahoma 15 | - South Dakota 16 | - Mid-Atlantic 17 | - Delaware 18 | - Maryland 19 | - New Jersey 20 | - New York 21 | capital:: Albany 22 | - Pennsylvania 23 | - Midwest 24 | - Illinois 25 | - Indiana 26 | - Iowa 27 | - Kentucky 28 | - Michigan 29 | - Minnesota 30 | - Missouri 31 | - Ohio 32 | - West Virginia 33 | - Wisconsin 34 | capital:: Madison 35 | - Mountains 36 | - Colorado 37 | - Idaho 38 | - Montana 39 | - Utah 40 | - Wyoming 41 | - New England 42 | - Connecticut 43 | - Maine 44 | - Massachusetts 45 | - New Hampshire 46 | - Rhode Island 47 | - Vermont 48 | - South 49 | - Alabama 50 | - Arkansas 51 | - Florida 52 | - Georgia 53 | - Louisiana 54 | capital:: Baton Rouge 55 | - Mississippi 56 | - North Carolina 57 | - South Carolina 58 | - Tennessee 59 | - Virginia 60 | - Southwest 61 | - Arizona 62 | - New Mexico 63 | - Texas 64 | -------------------------------------------------------------------------------- /examples/markdown/test.js: -------------------------------------------------------------------------------- 1 | //1/4/22; 12:08:54 PM by DW 2 | 3 | const fs = require ("fs"); 4 | const opml = require ("opml"); 5 | 6 | fs.readFile ("states.md", function (err, mdtext) { 7 | if (err) { 8 | console.log (err.message); 9 | } 10 | else { 11 | var theOutline = opml.markdownToOutline (mdtext.toString ()); 12 | fs.writeFile ("newStates.opml", opml.stringify (theOutline), function (err) { 13 | if (err) { 14 | console.log ("There was an error writing states.md: " + err.message); 15 | } 16 | }); 17 | fs.writeFile ("newStates.md", opml.outlineToMarkdown (theOutline), function (err) { 18 | if (err) { 19 | console.log ("There was an error writing states.md: " + err.message); 20 | } 21 | }); 22 | console.log ("\nLook for newStates.md and newStates.opml in the same directory as test.js.\n"); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /examples/parsing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "description": "Test opml.parse and opml.stringify.", 4 | "author": "Dave Winer ", 5 | "version": "0.4.2", 6 | "license": "MIT", 7 | "main": "test.js", 8 | "dependencies" : { 9 | "daveutils": "*", 10 | "opml": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/parsing/states.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | States 5 | Tue, 15 Mar 2005 16:35:45 GMT 6 | true 7 | http://drummer.scripting.com/davewiner/states.opml 8 | http://drummer.scripting.com/davewiner/states.json 9 | davewiner 10 | Dave Winer 11 | http://twitter.com/davewiner 12 | ws://drummer.scripting.com:1232/ 13 | Sat, 03 Jul 2021 15:19:39 GMT 14 | 1,4 15 | 5 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 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /examples/parsing/test.js: -------------------------------------------------------------------------------- 1 | //7/3/21; 3:27:01 PM by DW 2 | //Read and write an OPML file from a file. 3 | //Also display: 4 | //The outline's title from the head section and 5 | //The text of the third child of the second top level subhead of the United States. 6 | //Illustrates how you get data from the compiled structure. 7 | //Once you've compiled the OPML, you process it as a JavaScript object. 8 | //When you're done, you can serialize it with opml.stringify. 9 | //By design, works like JSON, so every JS programmer should find this familiar. 10 | //Visit every node in the outline, and convert the text to upper case. 11 | //Display OPML text of the uppercased outline in the console. 12 | 13 | const fs = require ("fs"); 14 | const opml = require ("opml"); 15 | 16 | fs.readFile ("states.opml", function (err, opmltext) { 17 | if (err) { 18 | console.log (err.message); 19 | } 20 | else { 21 | opml.parse (opmltext, function (err, theOutline) { //convert OPML text into a JavaScript structure 22 | console.log ("\nThe outline's title is \"" + theOutline.opml.head.title + ".\""); //see comment at top 23 | console.log ("The third state in the Great Plains is: \"" + theOutline.opml.body.subs [0].subs [1].subs [2].text + ".\""); 24 | fs.writeFile ("states.json", JSON.stringify (theOutline, undefined, 4), function (err) { 25 | if (err) { 26 | console.log (err.message); 27 | } 28 | else { 29 | console.log ("states.json was saved."); 30 | } 31 | }); 32 | fs.writeFile ("statescopy.opml", opml.stringify (theOutline), function (err) { 33 | if (err) { 34 | console.log (err.message); 35 | } 36 | else { 37 | console.log ("statescopy.opml was saved."); 38 | } 39 | }); 40 | opml.visitAll (theOutline, function (node) { 41 | node.text = node.text.toUpperCase (); 42 | return (true); //keep visiting 43 | }); 44 | console.log (opml.stringify (theOutline)); //view the uppercased outline in the JS console 45 | }); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /opmlpackage.js: -------------------------------------------------------------------------------- 1 | const myVersion = "0.5.7", myProductName = "opmlPackage"; 2 | const generatorForHead = "opml v" + myVersion + " (npmjs.com/package/opml)"; 3 | 4 | exports.parse = parse; 5 | exports.stringify = stringify; 6 | exports.htmlify = getOutlineHtml; 7 | exports.markdownToOutline = markdownToOutline; //1/3/22 by DW 8 | exports.outlineToMarkdown = outlineToMarkdown; //1/3/22 by DW 9 | exports.expandInclude = expandInclude; //1/4/22 by DW 10 | exports.visitAll = visitAll; //3/18/22 by DW 11 | exports.expandIncludes = expandIncludes; //5/11/22 by DW 12 | exports.readOutline = readOutline; //10/25/22 by DW 13 | 14 | const utils = require ("daveutils"); 15 | const opmltojs = require ("opmltojs"); 16 | const xml2js = require ("xml2js"); 17 | const request = require ("request"); 18 | 19 | function parse (opmltext, callback) { //returns a JavaScript object with all the info in the opmltext 20 | //Changes 21 | //12/9/24; 9:21:04 AM by DW 22 | //Under some circumstances, sourcestruct in the convert routine will be undefined, so we check for it instead of crashing. 23 | //12/27/21; 10:06:12 AM by DW 24 | //Under some circumstances, xml2js.parseString will return a result of null. It shows up in Daytona's log. So we check for it, and if it comes up, return an error. 25 | //1/18/21; 10:21:27 AM by DW 26 | //I created an OPML format that added a "subs" attribute to each headline that had subs. This was an error, but was still valid OPML, but it caused this code to fail, because subs was the wrong type. It is always a mistake, if it's possible that your OPML will be converted to a JS object. So I protected against it here, and don't copy an attribute called subs if it's present. It's possible that this fix could cause problems too, btw. The code is in Old School, look for saveDayInOpml. 27 | //4/18/20; 5:43:20 PM by DW 28 | //Changed the callback to return the standard format, with an err first, and theOutline second. 29 | //I didn't want to break all the apps that use this as it was configured, but in the future, use this entry point not the one without the error. 30 | function isScalar (obj) { 31 | if (typeof (obj) == "object") { 32 | return (false); 33 | } 34 | return (true); 35 | } 36 | function addGenerator (theOpml) { //follow the example of RSS 2.0 37 | try { 38 | theOpml.head.generator = generatorForHead; //8/20/23 by DW 39 | //theOpml.head.generator = myProductName + " v" + myVersion; 40 | } 41 | catch (err) { 42 | } 43 | } 44 | function convert (sourcestruct, deststruct) { 45 | if (sourcestruct !== undefined) { //12/9/24 by DW 46 | var atts = sourcestruct ["$"]; 47 | if (atts !== undefined) { 48 | for (var x in atts) { 49 | if (x != "subs") { //1/18/21 by DW 50 | deststruct [x] = atts [x]; 51 | } 52 | } 53 | delete sourcestruct ["$"]; 54 | } 55 | for (var x in sourcestruct) { 56 | var obj = sourcestruct [x]; 57 | if (isScalar (obj)) { 58 | deststruct [x] = obj; 59 | } 60 | else { 61 | if (x == "outline") { 62 | if (deststruct.subs === undefined) { 63 | deststruct.subs = new Array (); 64 | } 65 | if (Array.isArray (obj)) { 66 | for (var i = 0; i < obj.length; i++) { 67 | var newobj = new Object (); 68 | convert (obj [i], newobj); 69 | deststruct.subs.push (newobj); 70 | } 71 | } 72 | else { 73 | var newobj = new Object (); 74 | convert (obj, newobj); 75 | deststruct.subs.push (newobj); 76 | } 77 | } 78 | else { 79 | deststruct [x] = new Object (); 80 | convert (obj, deststruct [x]); 81 | } 82 | } 83 | } 84 | } 85 | } 86 | var options = { 87 | explicitArray: false 88 | }; 89 | xml2js.parseString (opmltext, options, function (err, jstruct) { 90 | if (err) { 91 | callback (err); 92 | } 93 | else { 94 | if (jstruct == null) { //12/27/21 by DW 95 | let err = {message: "Internal error: xml2js.parseString returned null."}; 96 | callback (err); 97 | } 98 | else { 99 | var theOutline = { 100 | opml: new Object () 101 | } 102 | convert (jstruct.opml, theOutline.opml); 103 | addGenerator (theOutline.opml); //8/6/17 by DW 104 | if (isScalar (theOutline.opml.head)) { //8/6/17 by DW 105 | theOutline.opml.head = new Object (); 106 | } 107 | if (isScalar (theOutline.opml.body)) { //8/6/17 by DW 108 | theOutline.opml.body = new Object (); 109 | } 110 | callback (undefined, theOutline); 111 | } 112 | } 113 | }); 114 | //xml2js.parseString (opmltext, options, function (err, jstruct) { 115 | //if (err) { //4/18/20 by DW 116 | //callback (err); 117 | //} 118 | //else { 119 | //var theOutline = { 120 | //opml: new Object () 121 | //} 122 | //convert (jstruct.opml, theOutline.opml); 123 | //addGenerator (theOutline.opml); //8/6/17 by DW 124 | //if (isScalar (theOutline.opml.head)) { //8/6/17 by DW 125 | //theOutline.opml.head = new Object (); 126 | //} 127 | //if (isScalar (theOutline.opml.body)) { //8/6/17 by DW 128 | //theOutline.opml.body = new Object (); 129 | //} 130 | //callback (undefined, theOutline); 131 | //} 132 | //}); 133 | } 134 | function stringify (theOutline) { //returns the opmltext for the outline 135 | var opmltext = opmltojs.opmlify (theOutline); 136 | return (opmltext); 137 | } 138 | function getOutlineHtml (theOutline) { 139 | var htmltext = "", indentlevel = 0; //5/24/24 by DW 140 | function add (s) { 141 | htmltext += utils.filledString ("\t", indentlevel) + s + "\n"; //5/25/24 by DW 142 | } 143 | function addSubsHtml (node) { 144 | add ("
    "); indentlevel++; 145 | node.subs.forEach (function (sub) { 146 | add ("
  • " + sub.text + "
  • "); 147 | if (sub.subs !== undefined) { 148 | addSubsHtml (sub); 149 | } 150 | }); 151 | add ("
"); indentlevel--; 152 | } 153 | addSubsHtml (theOutline.opml.body); 154 | return (htmltext); 155 | } 156 | function visitAll (theOutline, callback) { 157 | function visitSubs (theNode) { 158 | if (theNode.subs !== undefined) { 159 | for (var i = 0; i < theNode.subs.length; i++) { 160 | var theSub = theNode.subs [i]; 161 | if (!callback (theSub)) { 162 | return (false); 163 | } 164 | if (!visitSubs (theSub)) { //9/7/24 by DW -- see worknotes comment 165 | return (false); 166 | } 167 | } 168 | } 169 | return (true); 170 | } 171 | visitSubs (theOutline.opml.body); 172 | } 173 | 174 | function markdownToOutline (mdtext, options) { //1/3/22 by DW 175 | //Changes 176 | //1/12/22; 5:17:25 PM by DW 177 | //New optional param, options. 178 | //options.flAddUnderscores, defaults true. 179 | //1/8/22; 10:54:14 AM by DW 180 | //Any atts that show up at the beginning of a file are ignored. Previously they would cause the process to crash. 181 | //1/3/22; 5:50:36 PM by DW 182 | //Turn a markdown file as created by LogSeq or a compatible product 183 | //into an outline structure compatible with the one that is created from 184 | //parsing OPML text. 185 | var theOutline = { 186 | opml: { 187 | head: { 188 | }, 189 | body: { 190 | subs: new Array () 191 | } 192 | } 193 | }; 194 | 195 | if (options === undefined) { //1/12/22 by DW 196 | options = new Object (); 197 | } 198 | if (options.flAddUnderscores === undefined) { 199 | options.flAddUnderscores = true; 200 | } 201 | 202 | mdtext = mdtext.toString (); 203 | var lines = mdtext.split ("\n"), lastlevel = 0, stack = new Array ();; 204 | var lastnode = undefined, currentsubs = theOutline.opml.body.subs;; 205 | lines.forEach (function (theLine) { 206 | var thislevel = 0, flInsert = true; 207 | while (theLine.length > 0) { 208 | if (theLine [0] != "\t") { 209 | break; 210 | } 211 | thislevel++; 212 | theLine = utils.stringDelete (theLine, 1, 1); 213 | } 214 | if (utils.beginsWith (theLine, "- ")) { 215 | theLine = utils.stringDelete (theLine, 1, 2); 216 | } 217 | else { //is the line an attribute? 218 | if (utils.stringContains (theLine, ":: ")) { 219 | let parts = theLine.split (":: "); 220 | if (lastnode !== undefined) { //1/8/22 by DW 221 | var name = (options.flAddUnderscores) ? "_" + parts [0] : parts [0]; //1/12/22 by DW 222 | lastnode [name] = parts [1]; 223 | } 224 | flInsert = false; 225 | } 226 | } 227 | if (thislevel > lastlevel) { 228 | stack.push (currentsubs); 229 | lastnode.subs = new Array (); 230 | currentsubs = lastnode.subs; 231 | } 232 | else { 233 | if (thislevel < lastlevel) { 234 | var ctpops = lastlevel - thislevel; 235 | for (var i = 1; i <= ctpops; i++) { 236 | currentsubs = stack.pop (); 237 | } 238 | } 239 | } 240 | 241 | if (flInsert) { 242 | var newnode = { 243 | text: theLine 244 | } 245 | currentsubs.push (newnode); 246 | lastnode = newnode; 247 | lastlevel = thislevel; 248 | } 249 | }); 250 | return (theOutline); 251 | } 252 | function outlineToMarkdown (theOutline) { //1/3/22 by DW 253 | //Changes 254 | //1/3/22; 6:03:00 PM by DW 255 | //Generate markdown text from the indicated outline structure 256 | //that can be read by LogSeq and compatible apps. 257 | var mdtext = "", indentlevel = 0; 258 | function add (s) { 259 | mdtext += utils.filledString ("\t", indentlevel) + s + "\n"; 260 | } 261 | function addAtts (atts) { 262 | for (var x in atts) { 263 | if ((x != "subs") && (x != "text")) { 264 | if (utils.beginsWith (x, "_")) { 265 | add (utils.stringDelete (x, 1, 1) + ":: " + atts [x]); 266 | } 267 | } 268 | } 269 | } 270 | function dolevel (theNode) { 271 | theNode.subs.forEach (function (sub) { 272 | add ("- " + sub.text); 273 | addAtts (sub); 274 | if (sub.subs !== undefined) { 275 | indentlevel++; 276 | dolevel (sub); 277 | indentlevel--; 278 | } 279 | }); 280 | } 281 | //addAtts (theOutline.opml.head); 282 | dolevel (theOutline.opml.body) 283 | return (mdtext); 284 | } 285 | 286 | function httpRequest (url, callback) { 287 | request (url, function (err, response, data) { 288 | if (err) { 289 | callback (err); 290 | } 291 | else { 292 | var code = response.statusCode; 293 | if ((code < 200) || (code > 299)) { 294 | const message = "The request returned a status code of " + response.statusCode + "."; 295 | callback ({message}); 296 | } 297 | else { 298 | callback (undefined, data) 299 | } 300 | } 301 | }); 302 | } 303 | function expandInclude (theNode, callback) {//1/4/22 by DW 304 | //Changes 305 | //5/11/22; 8:52:08 AM by DW 306 | //If the node is an include, return the body of the OPML file it points to. 307 | //If it's not an include, return the node itself. 308 | //It's used in the app that converts docserver outlines to markdown for uploading to github. 309 | //https://github.com/scripting/docServer/blob/main/markdownapp/docservertomarkdown.js#L124 310 | if ((theNode.type == "include") && (theNode.url !== undefined)) { 311 | httpRequest (theNode.url, function (err, opmltext) { 312 | if (err) { 313 | callback (err); 314 | } 315 | else { 316 | parse (opmltext, function (err, theOutline) { 317 | if (err) { 318 | callback (err); 319 | } 320 | else { 321 | callback (undefined, theOutline.opml.body); 322 | } 323 | }); 324 | } 325 | }) 326 | } 327 | else { 328 | callback (undefined, theNode); 329 | } 330 | } 331 | function expandIncludes (theOutline, callback) { //5/11/22 by DW 332 | function expandBody (theBody, callback) { 333 | var theNewBody = new Object (), lastNewNode = theNewBody, stack = new Array (), currentOutline; 334 | function getNameAtt (theNode) { 335 | var nameatt = theNode.name; 336 | if (nameatt === undefined) { 337 | nameatt = utils.innerCaseName (theNode.text); 338 | } 339 | return (nameatt); 340 | } 341 | function inlevelcallback () { 342 | stack [stack.length] = currentOutline; 343 | currentOutline = lastNewNode; 344 | if (currentOutline.subs === undefined) { 345 | currentOutline.subs = new Array (); 346 | } 347 | } 348 | function nodecallback (theNode, path) { 349 | var newNode = new Object (); 350 | utils.copyScalars (theNode, newNode); 351 | currentOutline.subs [currentOutline.subs.length] = newNode; 352 | lastNewNode = newNode; 353 | } 354 | function outlevelcallback () { 355 | currentOutline = stack [stack.length - 1]; 356 | stack.length--; //pop the stack 357 | } 358 | function bodyVisiter (theOutline, visitcompletecallback) { 359 | function readInclude (theIncludeNode, callback) { 360 | console.log ("readInclude: url == " + theIncludeNode.url); 361 | expandInclude (theIncludeNode, function (err, theBody) { 362 | if (err) { 363 | callback (undefined); 364 | } 365 | else { 366 | expandBody (theBody, function (expandedBody) { 367 | callback (expandedBody); 368 | }); 369 | } 370 | }); 371 | } 372 | function doLevel (head, path, levelcompletecallback) { 373 | function doOneSub (head, ixsub) { 374 | if ((head.subs !== undefined) && (ixsub < head.subs.length)) { 375 | var sub = head.subs [ixsub], subpath = path + getNameAtt (sub); 376 | if (!utils.getBoolean (sub.iscomment)) { 377 | if (sub.type == "include") { 378 | nodecallback (sub, subpath); 379 | readInclude (sub, function (theIncludedOutline) { 380 | if (theIncludedOutline !== undefined) { 381 | doLevel (theIncludedOutline, subpath + "/", function () { 382 | outlevelcallback (); 383 | doOneSub (head, ixsub +1); 384 | }); 385 | } 386 | else { //6/25/15 by DW -- don't let errors derail us 387 | doOneSub (head, ixsub +1); 388 | } 389 | }); 390 | } 391 | else { 392 | nodecallback (sub, subpath); 393 | if (sub.subs !== undefined) { 394 | doLevel (sub, subpath + "/", function () { 395 | outlevelcallback (); 396 | doOneSub (head, ixsub +1); 397 | }); 398 | } 399 | else { 400 | doOneSub (head, ixsub +1); 401 | } 402 | } 403 | } 404 | else { 405 | doOneSub (head, ixsub +1); 406 | } 407 | } 408 | else { 409 | levelcompletecallback (); 410 | } 411 | } 412 | inlevelcallback (); 413 | if (head.type == "include") { 414 | readInclude (head, function (theIncludedOutline) { 415 | if (theIncludedOutline !== undefined) { 416 | doOneSub (theIncludedOutline, 0); 417 | } 418 | }); 419 | } 420 | else { 421 | doOneSub (head, 0); 422 | } 423 | } 424 | 425 | doLevel (theBody, "", function () { 426 | outlevelcallback (); 427 | visitcompletecallback (); 428 | }); 429 | } 430 | 431 | bodyVisiter (theOutline, function () { 432 | callback (theNewBody); 433 | }); 434 | } 435 | expandBody (theOutline.opml.body, function (theNewBody) { 436 | var theNewOutline = { 437 | opml: { 438 | head: { 439 | }, 440 | body: theNewBody 441 | } 442 | } 443 | utils.copyScalars (theOutline.opml.head, theNewOutline.opml.head); 444 | callback (theNewOutline); 445 | }); 446 | } 447 | 448 | function readOutline (urlOpmlFile, callback) { //10/25/22; 12:30:31 PM by DW -- copied from Daytona 449 | httpRequest (urlOpmlFile, function (err, opmltext) { 450 | if (err) { 451 | callback (err); 452 | } 453 | else { 454 | parse (opmltext, function (err, theOutline) { 455 | if (err) { 456 | callback (err); 457 | } 458 | else { 459 | callback (undefined, theOutline); 460 | } 461 | }); 462 | } 463 | }) 464 | } 465 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opml", 3 | "description": "Node and browser-based JavaScript code that reads and writes OPML", 4 | "author": "Dave Winer ", 5 | "version": "0.5.7", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/scripting/opmlpackage.git" 10 | }, 11 | "files": [ 12 | "opmlpackage.js" 13 | ], 14 | "main": "opmlpackage.js", 15 | "dependencies" : { 16 | "request": "*", 17 | "xml2js": "*", 18 | "daveutils": "*", 19 | "opmltojs": "*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /source.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | nodeEditor: opmlPackage 18 | Mon, 28 Jun 2021 20:23:08 GMT 19 | Mon, 09 Dec 2024 14:23:00 GMT 20 | Dave Winer 21 | http://davewiner.com/ 22 | 1, 2, 3, 5, 20, 21, 32, 51, 52, 53, 55, 57, 62, 63, 70, 72, 73, 77, 91, 100, 101, 102, 115, 118, 125, 126, 134, 136, 142, 148, 150, 156, 169, 170, 171, 189, 190, 191, 206, 211, 217, 219, 226 23 | 1 24 | 84 25 | 703 26 | 1080 27 | 1778 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 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 1134 | 1135 | 1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | 1171 | 1172 | 1173 | 1174 | 1175 | 1176 | 1177 | 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | 1185 | 1186 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | 1207 | 1208 | 1209 | 1210 | 1211 | 1212 | 1213 | 1214 | 1215 | 1216 | 1217 | 1218 | 1219 | 1220 | 1221 | 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 1245 | 1246 | 1247 | 1248 | 1249 | 1250 | 1251 | 1252 | 1253 | 1254 | 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | 1267 | 1268 | 1269 | 1270 | 1271 | 1272 | 1273 | 1274 | 1275 | 1276 | 1277 | 1278 | 1279 | 1280 | 1281 | 1282 | 1283 | 1284 | 1285 | 1286 | 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 | 1294 | 1295 | 1296 | 1297 | 1298 | 1299 | 1300 | 1301 | 1302 | 1303 | 1304 | 1305 | 1306 | 1307 | 1308 | 1309 | 1310 | 1311 | 1312 | 1313 | 1314 | 1315 | 1316 | 1317 | 1318 | 1319 | 1320 | 1321 | 1322 | 1323 | 1324 | 1325 | 1326 | 1327 | 1328 | 1329 | 1330 | 1331 | 1332 | 1333 | 1334 | 1335 | 1336 | 1337 | 1338 | 1339 | 1340 | 1341 | 1342 | 1343 | 1344 | 1345 | 1346 | 1347 | 1348 | 1349 | 1350 | 1351 | 1352 | 1353 | 1354 | 1355 | 1356 | 1357 | 1358 | 1359 | 1360 | 1361 | 1362 | 1363 | 1364 | 1365 | 1366 | 1367 | 1368 | 1369 | 1370 | 1371 | 1372 | 1373 | 1374 | 1375 | 1376 | 1377 | 1378 | 1379 | 1380 | 1381 | 1382 | 1383 | 1384 | 1385 | 1386 | 1387 | 1388 | 1389 | 1390 | 1391 | 1392 | 1393 | 1394 | 1395 | 1396 | 1397 | 1398 | 1399 | 1400 | 1401 | 1402 | 1403 | 1404 | 1405 | 1406 | 1407 | 1408 | 1409 | 1410 | 1411 | 1412 | 1413 | 1414 | 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | 1421 | 1422 | 1423 | 1424 | 1425 | 1426 | 1427 | 1428 | 1429 | 1430 | 1431 | 1432 | 1433 | 1434 | 1435 | 1436 | 1437 | 1438 | 1439 | 1440 | 1441 | 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 | 1458 | 1459 | 1460 | 1461 | 1462 | 1463 | 1464 | 1465 | 1466 | 1467 | 1468 | 1469 | 1470 | 1471 | 1472 | 1473 | 1474 | 1475 | 1476 | 1477 | 1478 | 1479 | 1480 | 1481 | 1482 | 1483 | 1484 | 1485 | 1486 | 1487 | 1488 | 1489 | 1490 | 1491 | 1492 | 1493 | 1494 | 1495 | 1496 | 1497 | 1498 | 1499 | 1500 | 1501 | 1502 | 1503 | 1504 | 1505 | 1506 | 1507 | 1508 | 1509 | 1510 | 1511 | 1512 | 1513 | 1514 | 1515 | 1516 | 1517 | 1518 | 1519 | 1520 | 1521 | 1522 | 1523 | 1524 | 1525 | 1526 | 1527 | 1528 | 1529 | 1530 | 1531 | 1532 | 1533 | 1534 | 1535 | 1536 | 1537 | 1538 | 1539 | 1540 | 1541 | 1542 | 1543 | 1544 | 1545 | 1546 | 1547 | 1548 | 1549 | 1550 | 1551 | 1552 | 1553 | 1554 | 1555 | 1556 | 1557 | 1558 | 1559 | 1560 | 1561 | 1562 | 1563 | 1564 | 1565 | 1566 | 1567 | 1568 | 1569 | 1570 | 1571 | 1572 | 1573 | 1574 | 1575 | 1576 | 1577 | 1578 | 1579 | 1580 | 1581 | 1582 | 1583 | 1584 | 1585 | 1586 | 1587 | 1588 | 1589 | 1590 | 1591 | 1592 | 1593 | 1594 | 1595 | 1596 | 1597 | 1598 | 1599 | 1600 | 1601 | 1602 | 1603 | 1604 | 1605 | 1606 | 1607 | 1608 | 1609 | 1610 | 1611 | 1612 | 1613 | 1614 | 1615 | 1616 | 1617 | 1618 | 1619 | 1620 | 1621 | 1622 | 1623 | 1624 | 1625 | 1626 | 1627 | 1628 | 1629 | 1630 | 1631 | 1632 | 1633 | 1634 | 1635 | 1636 | 1637 | 1638 | 1639 | 1640 | 1641 | 1642 | 1643 | 1644 | 1645 | 1646 | 1647 | 1648 | 1649 | 1650 | 1651 | 1652 | 1653 | 1654 | 1655 | 1656 | 1657 | 1658 | 1659 | 1660 | 1661 | 1662 | 1663 | 1664 | 1665 | 1666 | 1667 | 1668 | 1669 | 1670 | 1671 | 1672 | 1673 | 1674 | 1675 | 1676 | 1677 | 1678 | 1679 | 1680 | 1681 | 1682 | 1683 | 1684 | 1685 | 1686 | 1687 | 1688 | 1689 | 1690 | 1691 | 1692 | 1693 | 1694 | 1695 | 1696 | 1697 | 1698 | 1699 | 1700 | 1701 | 1702 | 1703 | 1704 | 1705 | 1706 | 1707 | 1708 | 1709 | 1710 | 1711 | 1712 | 1713 | 1714 | 1715 | 1716 | 1717 | 1718 | 1719 | 1720 | 1721 | 1722 | 1723 | 1724 | 1725 | 1726 | 1727 | 1728 | 1729 | 1730 | 1731 | 1732 | 1733 | 1734 | 1735 | 1736 | 1737 | 1738 | 1739 | 1740 | 1741 | 1742 | 1743 | 1744 | 1745 | 1746 | 1747 | 1748 | 1749 | 1750 | 1751 | 1752 | 1753 | 1754 | 1755 | 1756 | 1757 | 1758 | 1759 | 1760 | 1761 | 1762 | 1763 | 1764 | 1765 | 1766 | 1767 | 1768 | 1769 | 1770 | 1771 | 1772 | 1773 | 1774 | 1775 | 1776 | 1777 | 1778 | 1779 | 1780 | 1781 | 1782 | 1783 | 1784 | 1785 | -------------------------------------------------------------------------------- /worknotes.md: -------------------------------------------------------------------------------- 1 | #### 12/9/24; 9:21:04 AM by DW 2 | 3 | Under some circumstances, sourcestruct in the parse routine will be undefined, so we check for it instead of crashing. 4 | 5 | #### 9/7/24; 9:59:57 AM by DW 6 | 7 | visitAll in both client and server versions had a serious bug. 8 | 9 | if a function returns false it's supposed to stop visiting, but it doesn't. 10 | 11 | perhaps i've never encountered a situation where the logic depended on this. 12 | 13 | i have to fix it, i don't see any way around it. 14 | 15 | #### 5/25/24; 3:47:32 PM by DW 16 | 17 | Fixed another problem in getOutlineHtml. We were calling filledString and really needed to call utils.filledString. 18 | 19 | #### 5/24/24; 8:12:30 AM by DW 20 | 21 | Fixed a problem reported on GitHub where we were declaring htmltext and indentlevel incorrectly. 22 | 23 | * `var htmltext = ""; indentlevel = 0;` 24 | 25 | Replaced the first semicolon with a comma. 26 | 27 | The problem appeared in two places, in the Node package and in the code to be included with a client app. 28 | 29 | #### 8/20/23; 10:31:38 AM by DW 30 | 31 | Changed the generator message on OPML files we generate to include the address of the NPM package. 32 | 33 | Changed the private notes file to this file, worknotes.md. The format changed, so some of the earlier notes might not be as pretty as they were. This is becoming standard practice in all my projects. 34 | 35 | #### 1/8/22; 10:57:35 AM by DW 36 | 37 | I undid the changes made on the 7th. 38 | 39 | I had decided earlier to not try to handle head-level atts in the first attempt at interop. There are too many variables, and I don't understand enough of the issues. It's the proverbial can of worms. 40 | 41 | #### 1/7/22; 1:43:05 PM by DW 42 | 43 | Support for head-level atts when reading a markdown file. 44 | 45 | #### 1/4/22; 5:40:31 PM by DW 46 | 47 | Added example code for markdown/outline functions. 48 | 49 | Added expandInclude in Node package. 50 | 51 | #### 1/3/22; 5:52:48 PM by DW 52 | 53 | Added commoncode.js because some code can run equally well on server and client. 54 | 55 | #### 9/24/21; 2:18:33 PM by DW 56 | 57 | New entry-point in client code, opml.read. 58 | 59 | Started an Updates section in the readme. 60 | 61 | #### 7/1/21; 11:45:06 AM by DW 62 | 63 | opml.parse has to take a callback because xml2js.parse does. 64 | 65 | #### 6/28/21; 3:25:22 PM by DW 66 | 67 | this is where the toolkit for supporting instant outlines and other stuff will go 68 | 69 | may incorporate features from other packages 70 | 71 | the idea is to make it easy for Node devs to support OPML in an interoperable way 72 | 73 | --------------------------------------------------------------------------------