[HN Gopher] Show HN: Auto-generate vanilla JavaScript alternativ...
       ___________________________________________________________________
        
       Show HN: Auto-generate vanilla JavaScript alternatives for jQuery
       methods
        
       Author : alokdubey007
       Score  : 115 points
       Date   : 2021-09-06 12:59 UTC (10 hours ago)
        
 (HTM) web link (github.com)
 (TXT) w3m dump (github.com)
        
       | franciscop wrote:
       | I made a tiny jQuery alternative a while back called Umbrella JS:
       | https://umbrellajs.com/
       | 
       | Seeing methods like addClass in "replace-jquery", I'm not fully
       | satisfied. I could make Umbrella JS tiny (1/2 of the alternative
       | listed elsewhere in the thread, Cash, and 10% the size of jQuery)
       | because of heavy method reusal. For instance, in Umbrella JS
       | addClass is just:                   u.prototype.addClass =
       | function () {           return this.eacharg(arguments, function
       | (el, name) {             el.classList.add(name);           });
       | };
       | 
       | In "replace-jquery" you are already depending on `this.`, so why
       | not making a couple of useful utils? Right now it is more
       | verbose, and doesn't accept e.g. an array of classes or classes
       | as arguments:                   addClass(classNames = '') {
       | this.each((el) => {             classNames.split('
       | ').forEach((className) => {
       | el.classList.add(className);             });           });
       | return this;         }
       | 
       | Cash JS's addClass (which is hidden behind toggleClass(cls,
       | true)) is nice, it's bigger BUT that's because it's 3
       | implementations at once (addClass, removeClass, toggleClass). It
       | properly uses a method to getSplitValues, which is very helpful
       | and flexible:                   fn.toggleClass = function ( this:
       | Cash, cls: string, force?: boolean ) {           const classes =
       | getSplitValues ( cls ),                 isForce = !isUndefined (
       | force );           return this.each ( ( i, ele ) => {
       | if ( !isElement ( ele ) ) return;             each ( classes, (
       | i, c ) => {               if ( isForce ) {                 force
       | ? ele.classList.add ( c ) : ele.classList.remove ( c );
       | } else {                 ele.classList.toggle ( c );
       | }             });           });         };
       | 
       | And I will spare you all jQuery's implementation, which is huge,
       | but it can be seen here:
       | 
       | https://github.com/jquery/jquery/blob/main/src/attributes/cl...
        
         | gunapologist99 wrote:
         | Does `cash` support jQuery-style `.ajax`?
         | 
         | When you consider replacing `$.ajax` with fetch, you'll quickly
         | found out that fetch is severely lacking with regards to:
         | 
         | * handling cookies,
         | 
         | * HTTP status codes (404, 403, etc),
         | 
         | * CORS,
         | 
         | * and even just simply readability when dealing with JSON.
         | 
         | jQuery's ajax (and its aliases like $.GET) handle all of these
         | (edge cases? are these really edge cases?!) with aplomb, so you
         | don't have to worry about it.
         | 
         | This is the issue with all of the jQuery alternatives, even
         | cash (which does look pretty awesome); you start using them,
         | and then development hits a halt because you suddenly realize
         | that you actually need something that jQuery already does quite
         | nicely, and has done so, quietly and politely, for more than a
         | decade.
        
           | shrew wrote:
           | I'm genuinely curious to know what you find severely lacking
           | in fetch compared to $.ajax.
           | 
           | - handling cookies
           | fetch('https://example.com', { credentials: 'include' })
           | 
           | - HTTP status codes
           | fetch('https://example.com').then(response => {
           | if (response.ok) {                 // Response is 200-299
           | }             if (response.status === 404) {
           | // Status code specific handling             }         });
           | 
           | - CORS                   fetch('https://example.com', { mode:
           | 'no-cors' });
           | 
           | - JSON handling
           | fetch('https://example.com').then(response =>
           | response.json()).then(json => {             // Do something
           | with a parsed JSON object         });
           | 
           | I've used all of the above patterns regularly for several
           | years now and never found any of them particularly cumbersome
           | and certainly not lacking feature wise. If async/await syntax
           | is available to you, it's even more succinct than the Promise
           | style above.
           | 
           | With that said, I've not used $.ajax in anger for a good long
           | while so I may be missing out, particularly as I note there
           | have been API changes in newer versions of jQuery. Are there
           | some specific use cases that you've found fetch to be
           | particularly inept in dealing with?
        
           | fabiospampinato wrote:
           | Cash's maintainer here. I think needing $.ajax is a fairly
           | niche thing (like ~nobody using vue/react/svelte is asking
           | for a jQuery-like ajax function, the world moved to fetch),
           | and it's not something that you'd suddenly stumble upon
           | either.
        
             | franciscop wrote:
             | Umbrella JS' creator here, I fully agree with that
             | statement. You'd normally use something like Axios/Got/etc
             | for a reusable API interface for API-heavy interfaces, and
             | for simple cases it's not too complex to add e.g.
             | `credentials: 'include'` for cookies.
        
         | fabiospampinato wrote:
         | Cash's maintainer here. I really don't think anybody can
         | squeeze 50% out of the 6kb min+gzip code that make up Cash,
         | quite a bit of thought went into squeezing as many bytes as
         | possible out of it. (while still preserving a large degree of
         | compatibility with jQuery, support for many methods and support
         | for partial compilation)
         | 
         | Your comparison isn't quite fair, our toggleClass function does
         | a lot more than a simple addClass, and writing it this way
         | lowers in fact the total min+gzip size.
         | 
         | I'd be pretty impressed if you can shave just 1kb out of those
         | 6kb to be honest.
        
           | franciscop wrote:
           | Agreed! Not a fair comparison either way; if I compare Cash'
           | addClass then it doesn't really "do anything" besides call
           | the other method, so I'd say the toggleClass is
           | representative of _the 3 methods together_, so 1/3rd? It's
           | nice code :)
           | 
           | Shaving off 1kb out of 6kb of optimized GZIP code is probably
           | impossible unless you missed something important.
           | 
           | I also went deep into optimizing Umbrella JS, it does a lot
           | less than Cash so hence it's a lot smaller as well; but I
           | went so far as comparing the GZIP output of calling `for ()`
           | vs `forEach`, etc:
           | 
           | https://medium.com/@fpresencia/understanding-gzip-
           | size-836c7...
           | 
           | That's why I am not concerned of repeating e.g. `function()
           | {}` (instead of the, back then, experimental () => {}) many
           | times, since with gzip in my experience that gets all totally
           | abstracted out.
           | 
           | Edit: edited the original, explaining that Cash it's doing it
           | nicely!
        
             | 22c wrote:
             | > but I went so far as comparing the GZIP output of calling
             | `for ()` vs `forEach`, etc
             | 
             | AFAIK Google Closure Compiler [1] does similar things
             | automagically.
             | 
             | [1] https://github.com/google/closure-compiler
        
       | mynegation wrote:
       | From the first glance it replaces the jquery calls to some $utils
       | implementation of the same methods. I guess final bundle contains
       | only methods in $utils that are actually used in the code base,
       | but it looks like re-implementation of jquery subset. Would not
       | just using jquery with tree-shaking achieve similar effect? (not
       | a JavaScript expert, so sorry if it is a dumb question).
       | 
       | Maybe another advantage of this is that new implementations
       | target runtime version that is new enough to avoid most of the
       | feature-checking and shims? Would be great if README addressed
       | those questions.
        
         | fabiospampinato wrote:
         | jQuery is not tree-shakeable because it uses a chainable API.
         | Like you don't import `toggleClass`, you access the
         | `toggleClass` property of a jQuery instance, so it can't be
         | tree-shaken off automatically.
         | 
         | Even if somebody made a bundler plugin for doing the heavy work
         | jQuery can only be partially compiled with whole modules
         | excluded, you can't exclude individual methods (e.g. you can't
         | just remove `toggleClass`, you have to remove also `addClass`,
         | `removeClass` etc.).
         | 
         | FYI I maintain a jQuery alternative that supports being
         | partially compiled with individual modules turned off, but it
         | requires manually listing them:
         | https://github.com/fabiospampinato/cash
        
         | timostamm wrote:
         | Tree shaking works well when you have ESM imports / exports.
         | 
         | jquery is/was usually loaded into the global scope. There is no
         | standard way for tree shakers to deduct which parts of the
         | library you are using.
         | 
         | Yes, browser APIs have matured significantly since jquery was
         | new and such a library doesn't add much value anymore.
         | 
         | I agree it would be great if the README gave a bit more
         | background. Whoever is looking to migrate away from jquery will
         | be aware of the background though (and will likely appreciate
         | this tool).
        
       | hardwaresofton wrote:
       | An old classic worth referencing if you're wondering whether you
       | need jQuery (or any alternatives like UmbrellaJS[0] or Cash[0]):
       | 
       | http://youmightnotneedjquery.com/
       | 
       | [0]: https://umbrellajs.com/
       | 
       | [1]: https://github.com/fabiospampinato/cash
        
       | rubyist5eva wrote:
       | I ditched this kinda manual DOM manipulation nonsense with
       | mithril components+streams years ago and never looked back.
        
       | fabiospampinato wrote:
       | I maintain an alternative to jQuery called Cash [0], this looks
       | cool! If you are interested in joining forces OP it'd be cool to
       | have this capability in Cash itself. IMO that'd be the best of
       | both world because this feature is cool and useful, and Cash's
       | methods are most probably closer to jQuery's, better tested and
       | there are more of them available (76 vs 44).
       | 
       | For example, your `on` method [1]:
       | 
       | - Doesn't support event delegation.
       | 
       | - Doesn't support event namespaces.
       | 
       | - Doesn't support receiving an argument mapping events to
       | callbacks like jQuery also can.
       | 
       | - It seems to have subtle bugs, like the way the events string is
       | split makes so that double consecutive spaces in it (which can
       | happen as a result of a typo) will result in listening to the
       | empty string event. Basically: 'foo bar'.split ( ' ' ) => ['foo',
       | '', 'bar'] (there are two spaces between foo and bar).
       | 
       | The `on` method we are using in Cash [2] is a lot more convoluted
       | than that. On one hand it requires more bytes, but on the other
       | the chances of it behaving exactly like jQuery's are much higher.
       | In fact we can also run jQuery's test suite with Cash to spot
       | issues.
       | 
       | Feel free to ping me if you are interested in joining forces.
       | 
       | [0]: https://github.com/fabiospampinato/cash
       | 
       | [1]: https://github.com/sachinchoolur/replace-jquery#on
       | 
       | [2]:
       | https://github.com/fabiospampinato/cash/blob/master/src/even...
        
         | snet0 wrote:
         | "I made a tool to remove a library"
         | 
         | "Would you like to integrate that tool into my library?"
        
           | fabiospampinato wrote:
           | Kinda accurate, but as soon as you realize that "tool" is
           | basically another name for "library" it makes sense. Like OP
           | replaced code with code at the end of the day, who cares if
           | you call it "library" or "tool" or something else.
        
         | sachinneravath wrote:
         | Sure, Cash is super cool. I wrote this library for my personal
         | use. As I was repeating the same work on multiple projects.
         | While removing jQuery dependency, the hardest part was finding
         | the jQuery methods in the existing project and writing the
         | alternative vanilla js methods without making much changes in
         | the codebase. Yes, I agree with you the events part needs to be
         | improved and well documented. (It actually supports
         | namespacing.) I fixed most of the things in
         | https://github.com/sachinchoolur/tiny-events.js and need to
         | make the changes here as well.
         | 
         | My intention was not to build another JavaScript utility
         | library. I just wanted to make my JavaScript libraries jQuery
         | independent.
        
         | labster wrote:
         | Seems pretty cool. The only Cash design decision I'm doubting
         | is $().append not accepting plain text. I get the reason why,
         | but the alternative is ugly. Maybe add an appendText method, or
         | a utility $.textNode?
         | 
         | I'm kind of assuming the underlying philosophy of the project
         | is to make DOM manipulation as easy as in jQuery but smaller by
         | removing seldom needed safeguards and relying on modern browser
         | selectors. I'm okay with the additional footguns but think it
         | should still preserve easy, concise syntax.
        
           | fabiospampinato wrote:
           | That's kind of a mistake of the library IMO, but changing
           | that requires pushing a major release and I'm not sure if
           | it's worth asking everybody to update their code to align
           | that with jQuery at this point.
        
       | manigandham wrote:
       | Vanilla JS refers to Javascript that uses native browser methods
       | instead of relying on a library. This is just replacing jQuery
       | with another library constructed on the fly, but there are
       | already minimal and modern alternatives with the jQuery API like
       | zepto and cash.
       | 
       | 1) https://github.com/fabiospampinato/cash 2)
       | https://github.com/madrobby/zepto
        
         | laurent92 wrote:
         | Actually, VanillaJS is now a library. It can be downloaded in
         | 0KB, or 25KB gzipped. Not to be confused with "Vanilla JS". It
         | comes with the famous "Math" library that Google and Strip use,
         | for 0KB additional.
         | 
         | http://vanilla-js.com/
        
         | q-rews wrote:
         | This. There's zero sense in using 1:1 replacements of jQuery
         | nowadays since jQuery already has a slim version.
         | 
         | If you want to drop jQuery, you have to rethink the code.
        
           | fabiospampinato wrote:
           | Cash's maintainer here. IMO it could be worth for some use
           | cases to use a replacement for jQuery, for the record Cash is
           | ~75% (~20kb minified and gzipped less) smaller than jQuery
           | Slim, that isn't/shouldn't be a rounding error for many use
           | cases.
        
             | vmception wrote:
             | These are good points, people are more so taking issue with
             | the title.
             | 
             | I was expecting a tool that just tells me how to do
             | something in Javascript - without any library - that I
             | would be tempted to use just include jQuery for.
             | 
             | Instead I'm presented with a library.
             | 
             | I expected the comment sections to be talking about how
             | AI/ML crowdsourced code aggregators keep messing up,
             | instead I'm reading about a simple additional convenience
             | library that a contractor wrote for themselves, and
             | described wrong.
        
       | ape4 wrote:
       | I'm going to keep jQuery for a while. Its not so big and even
       | though native js has "caught up" in places jQuery is still more
       | compact.
        
       | q-rews wrote:
       | People are confused about what vanilla JavaScript means. If
       | you're replacing jQuery for another loose library you're just
       | fooling yourself and wasting time doing it.
       | 
       | This is akin to all those StackOverflow answers suggesting to use
       | `any` for every TypeScript problem they encounter.
        
       | simondotau wrote:
       | It's worth remembering that jQuery is a ~30kb "cost" for the end
       | user. Once upon a time, that was a lot. And it was entirely
       | prudent to question its necessity on the basis of load times and
       | bandwidth consumption.
       | 
       | But now we live in a world where many common web pages have over
       | 1000kb of resources on them. And nobody blinks an eyelid.
        
         | Wowfunhappy wrote:
         | > And nobody blinks an eyelid.
         | 
         | But maybe they should?
         | 
         | Like, I agree that if your web page loads a full MB of
         | Javascript, eliminating JQuery should be the least of your
         | concerns; it's a drop in the bucket, so optimize elsewhere. But
         | web pages _really_ shouldn 't be loading 1 MB of Javascript in
         | the first place.
         | 
         | It's so nice when I come across a website that's actually slim,
         | I can really feel the difference...
        
         | manigandham wrote:
         | Unfortunately this kind of thought process is what leads to
         | pages with megabytes of JS. Every byte matters.
        
           | simondotau wrote:
           | But it isn't a one-way street. Using jquery can eliminate the
           | need for other, lengthier blobs of code. In fact that's
           | exactly what this "vanilla JavaScript alternative" does.
        
           | sanitycheck wrote:
           | But, we didn't used to have megabytes of JS on pages back
           | when everyone used jquery.
           | 
           | I think what leads to pages with megabytes of JS is when
           | people automatically assume that all projects must be a SPA
           | built on one the popular everything-but-the-kitchen-sink
           | frameworks.
        
           | mkoryak wrote:
           | This is especially true because now instead of all your
           | dependencies using jQuery, they are each implementing a small
           | subset of jQuery on their own.
           | 
           | The problem you thought you solved was actually made
           | exponentially worse when those dependencies dependencies also
           | rewrote some jQuery using different native methods
        
         | fabiospampinato wrote:
         | I'm not sure I buy this argument, one can't just say that ~30kb
         | isn't a lot because 30 is a number perceived as low, would 30kb
         | be justified for a library that allows you to toggle a class on
         | a node? Of course it wouldn't, you need to measure what you are
         | getting for 30kb.
         | 
         | I don't buy the second part of the argument either, you can
         | load a 1000kb image on a blog post and that won't have nearly
         | the same effect as loading 1000kb of JS. The JS needs to be
         | parsed and executed and maybe the site doesn't even work
         | without it, the image can probably be rendered progressively,
         | can be decoded in another thread, nothing is really waiting on
         | it to load, and if it doesn't load at all it's not the end of
         | the world anyway.
         | 
         | With ~4kb you can have Preact, is jQuery Slim (~26kb) giving
         | you ~6.5x times as much value as Preact really? Maybe it is,
         | probably not.
         | 
         | For some context I maintain a ~6kb rewrite of a subset of
         | jQuery (https://github.com/fabiospampinato/cash), which IMO is
         | much better value proposition for many use cases.
        
         | sanitycheck wrote:
         | Not just 1000kb.
         | 
         | netflix.com (which doesn't even do anything): 1.6MB
         | 
         | CNN: 3.4MB
         | 
         | NYT: 4.1MB
         | 
         | Guardian: 5.7MB
         | 
         | IGN: 69.5MB
         | 
         | I still use jquery when I'm targeting ancient devices. Some of
         | them still run Presto. Performance is fine, file size is fine -
         | and drastically smaller than any of the sites above!
         | 
         | (I can see why one might not want jquery as a dependency of a
         | dependency though.)
        
           | beebeepka wrote:
           | I used to work on low power/ancient devices running presto or
           | blink. Performance was much better on opera 12.80 or so.
           | 
           | At some point I had to support an outside team working on
           | external application running inside ours.
           | 
           | These guys used jQuery in ways it shouldn't be used when
           | performance is an issue. Had to spend a few hours reducing
           | their code to mostly CSS and just a tiny bit of honest to god
           | js that even presto can understand and render.
           | 
           | Point is, it's really easy to overuse and that can have an
           | effect on performance, even on presto
        
             | sanitycheck wrote:
             | We may know the same devices - designed to run Flash Lite
             | acceptably then made to just barely support HTML/JS with
             | heroic engineering efforts later. Nobody believes how slow
             | they are until they try.
             | 
             | I think if someone queries the DOM a lot on these things,
             | especially in response to keypresses, they're in trouble
             | whether or not they're doing it with jquery.
        
           | t0astbread wrote:
           | Are you sure you weren't loading a video on IGN?
        
             | Ardren wrote:
             | Yep, looks like it's a video.
             | 
             | But it's auto-preloaded and playing even when not
             | visible...
        
       | Lurkars wrote:
       | I first thought that it replaces jQuery with vanilla, but it
       | replaces jQuery functions with other functions? With this point
       | of view using the term vanilla would mean that there is no
       | difference to jQuery, because in the end jQuery is also written
       | in vanilla? Or do I get something wrong here?
        
         | codingdave wrote:
         | This is more like tree-shaking jQuery into just the pieces you
         | need from it, so you aren't loading up the full library when
         | you are only using 20% of it.
        
           | dheera wrote:
           | Maybe there should be a jQuery Nano with just the most
           | commonly used methods.
        
             | sverhagen wrote:
             | That seems fodder for endless debate, does it not?
        
       | warpspin wrote:
       | With all the people replacing jQuery manually in their projects,
       | you have to wonder whether the summed up costs of everyone
       | running their own replacements is not larger than jQuery itself,
       | once you use 3 or 4 such libraries :D
        
         | SOLAR_FIELDS wrote:
         | It's been awhile since I've worked in the frontend space, but
         | even 5 or 6 years ago if you were using a pretty popular hosted
         | CDN of JQuery with a pretty recent version there was a pretty
         | good chance it was already cached on the end user's device when
         | they hit your site. There's always a lot of talk about Jquery's
         | size but I wonder how many users don't even notice because
         | they've already got it on their system.
         | 
         | There's also of course the added cost of executing the library
         | on the site but I'm guessing V8 and other JS engines have
         | optimized the hell out of that too as to make it pretty
         | negligible in terms of time difference.
        
           | JZumun wrote:
           | Modern browsers partition caches by site nowadays - at least,
           | Chrome started doing it last year[0], and Firefox followed
           | soon after I think [1]. That means there's no longer any
           | caching benefit for multiple websites using jQuery - each
           | site will download and cache it separately.
           | 
           | [0] https://developers.google.com/web/updates/2020/10/http-
           | cache...
           | 
           | [1] https://developer.mozilla.org/en-
           | US/docs/Web/Privacy/State_P...
        
             | SOLAR_FIELDS wrote:
             | Interesting. Reading Mozilla's justification it does make a
             | lot of sense, however it is kind of unfortunate to lose
             | something that was pretty beneficial in terms of
             | performance because of some bad actors. It's not really
             | viable to maintain a whitelist of trusted libraries either,
             | and doing so would create a whole new class of problems.
             | 
             | Ideally CDN's are powerful and ubiquitous enough these days
             | that at least when you have the end user going and asking
             | for JQuery from Cloudflare or whoever the CDN probably has
             | a very fast location right next to them distance wise and
             | that data should get over the wire pretty dang quickly.
             | It's still another performance hit though since you know at
             | least the first time on the site they have to go and get it
             | and if you are hosting your own stuff it might make more
             | sense to just webpack everything together and hand it off
             | to the CDN instead of having something like JQuery be on
             | its own. Less overhead for opening another network request
             | and all that.
        
       | Etheryte wrote:
       | While this is technically interesting, I can't help but ask what
       | the benefit of this is? Instead of using a library that's battle-
       | tested and well documented you'll now have to maintain an ad hoc
       | replacement with unknown unknowns. Cutting down your bundle size
       | is a good goal, but this feels like much too steep of a tradeoff.
        
         | mkoryak wrote:
         | There is a trend in web dev right now to stop using jQuery
         | because reasons. These reasons make sense if you use a
         | framework and sometimes need to venture outside of it.
         | 
         | The problem is that the reasons are not often understood and
         | then a tool like this comes along and now we can easily replace
         | jQuery with a worse API adapter. Hooray!
        
           | byteface wrote:
           | I do feel the move against jquery comes from people that
           | surpass it, which seems a shame. They had to use it once.
           | It's like burning the rope for the people behind. Many low
           | level users don't care about 30kb to get the $ shorthand as
           | it's a habit they've had for a decade. It's funny almost to
           | see the hoops people jump through not to use it. Like having
           | a point to prove and not being able to prove it. Hey look,
           | you can replace jquery with these 10 unmemorable esoteric
           | steps.
        
           | cormacrelf wrote:
           | The repo says it is for lightGallery, which is trying to
           | offer a framework-agnostic lightbox component. Modern JS
           | setups simply don't play nice with jquery.min.js, and it's a
           | big download and hard to slice up. This helps folks reuse
           | more old work like lightGallery. That's a pretty good reason,
           | almost the best possible reason to build this, I think.
           | 
           | How it will likely be used is a different matter -- if you're
           | a company trying to hire young devs and don't like to admit
           | you have any legacy tech in the stack, then this is HOT code
           | right now. Same if you need some bullshit targets to hit
           | before the end of the quarter. Get on it!
        
           | masklinn wrote:
           | > There is a trend in web dev right now to stop using jQuery
           | because reasons.
           | 
           | One reason is that a lot of jquery's bulk is compatibility
           | concerns or BC in edges, if you decide that you don't support
           | old jQuery (mis)behaviour, older APIs, older browsers, ...
           | you can probably slim it down a fair bit.
           | 
           | A bunch of utilities you can also probably do without e.g.
           | your jQuery.map, animations stuff, ... jQuery is
           | progressively deprecating more and more things, but it's a
           | harder sell to actually remove them.
        
             | mkoryak wrote:
             | Like I said, all the reasons for removing jQuery are valid,
             | but only in the context of the codebase where they are
             | given.
             | 
             | You wouldn't want to remove jQuery from an app that is used
             | by IE9.
             | 
             | People end up hearing the reasons and don't stop to think,
             | hey does this actually apply to my code? What are the trade
             | offs for that 30k of code?
             | 
             | Many just end up rewriting what jQuery did in a non
             | reusable way that contributes to code bloat and bad
             | readability.
        
         | Deukhoofd wrote:
         | I guess it might be useful as a first step if you want to
         | reduce your dependencies on jQuery. First use this so you can
         | drop the dependency, then slowly replace the functions you're
         | using.
        
       | yawaworht1978 wrote:
       | Is there anything like this for react, Vue etc?
       | 
       | Now that would be a milestone.
        
         | t0astbread wrote:
         | Shouldn't that be handled by tree-shaking?
        
       ___________________________________________________________________
       (page generated 2021-09-06 23:01 UTC)