From: Dan Brown Date: Wed, 30 Jul 2025 08:47:21 +0000 (+0100) Subject: Final post release time X-Git-Url: http://source.bookstackapp.com/website/commitdiff_plain/HEAD?hp=8267312f94242733a112a240ac08f056dd89f727 Final post release time --- diff --git a/content/blog/2025/bookstack-release-v25-05.md b/content/blog/2025/bookstack-release-v25-05.md index 7ab5790..9e00284 100644 --- a/content/blog/2025/bookstack-release-v25-05.md +++ b/content/blog/2025/bookstack-release-v25-05.md @@ -239,7 +239,7 @@ we can do to celebrate this milestone. * Updated translations with latest Crowdin changes. ([#5622](https://github.com/BookStackApp/BookStack/pull/5622)) * Update codebase and packages to address php 8.4 depreactions. ([#5358](https://github.com/BookStackApp/BookStack/issues/5358)) -**Released in v24.02.5** +**Released in v25.02.5** * Fixed incorrect image directory permissions. ([#5609](https://github.com/BookStackApp/BookStack/pull/5609), [#5605](https://github.com/BookStackApp/BookStack/issues/5605)) * Updated translations with latest Crowdin changes. ([#5608](https://github.com/BookStackApp/BookStack/pull/5608)) @@ -249,18 +249,18 @@ we can do to celebrate this milestone. - Updated download-vendor command with extra clean-up handling. -**Released in v24.02.4** +**Released in v25.02.4** * Updated PHP dependency package versions to fix compatibility issue on systems with recent libxml versions (eg. Arch Linux). -**Released in v24.02.3** +**Released in v25.02.3** * Updated image file permission error handling for images to log instead of fail. ([#5601](https://github.com/BookStackApp/BookStack/pull/5601), [#5269](https://github.com/BookStackApp/BookStack/issues/5269)) * Fixed style issues in exports due to CSS variables being ignored. ([#5576](https://github.com/BookStackApp/BookStack/issues/5576)) * Updated translations with latest Crowdin changes. ([#5566](https://github.com/BookStackApp/BookStack/pull/5566)) * Updated PHP dependency package versions. -**Released in v24.02.2** +**Released in v25.02.2** * Updated name sort rule handling to consider accented characters. Thanks to [@bernardo-campos](https://github.com/BookStackApp/BookStack/pull/5550). ([#5550](https://github.com/BookStackApp/BookStack/pull/5550), [#5542](https://github.com/BookStackApp/BookStack/issues/5542)) @@ -273,7 +273,7 @@ we can do to celebrate this milestone. - Updated keyboard navigation to be more reliable around images & media embeds. * Fixed comment times not being shown. ([#5555](https://github.com/BookStackApp/BookStack/issues/5555)) -**Released in v24.02.1** +**Released in v25.02.1** * Added ipv6 database host address support. ([#5464](https://github.com/BookStackApp/BookStack/issues/5464)) * Updated translations with latest Crowdin changes. ([#5505](https://github.com/BookStackApp/BookStack/pull/5505)) diff --git a/content/blog/2025/bookstack-release-v25-07.md b/content/blog/2025/bookstack-release-v25-07.md new file mode 100644 index 0000000..6e9dfd9 --- /dev/null +++ b/content/blog/2025/bookstack-release-v25-07.md @@ -0,0 +1,218 @@ ++++ +categories = ["Releases"] +tags = ["Releases"] +title = "BookStack Release v25.07" +date = 2025-07-30T08:47:00Z +author = "Dan Brown" +image = "/images/blog-cover-images/cc-by-2/dmitry-djouce-cow.jpg" +slug = "bookstack-release-v25-07" +draft = false ++++ + +Today we release the July 2025 version of BookStack which brings a varied bundle +of improvements across the platform for better editing, extra customization capabilities and more! + +* [Update instructions](/docs/admin/updates) +* [GitHub release page](https://github.com/BookStackApp/BookStack/releases/tag/v25.07) + +{{}} + +### Markdown Plaintext Input + +The markdown text edit area has made use of [CodeMirror](https://codemirror.net/) to provide code-based features +like markdown syntax highlighting, line numbering and some autocomplete features, in addition to providing a nice +interface to work with from a development point of view. +Unfortunately though the use of a smarter editor like this could introduce compatibility issues with certain browser-level features +such as spell-checking, searching and other extensions. +To help avoid these issues, there's now an option in the markdown editor to use a plaintext text-area instead: + +![BookStack page markdown editor, with options dropdown showing and mouse hovering over a "Plaintext editor" checkbox](/images/2025/07/plaintext_markdown_input.png) + +While I originally thought this would be a simple input swap on option change, in the end I spent a lot of extra time matching the existing CodeMirror functionality as best as possible. This meant supporting the same range of editor shortcuts, drag & drop features, scroll syncing and editor action buttons. + +### New WYSIWYG Editor for Comments & Descriptions + +In the last release the new WYSIWYG editor [moved to a Beta](/blog/bookstack-release-v25-05/#new-wysiwyg-editor-updates-now-in-beta) status. +As part of its continued slow roll-out, book/chapter/shelf description boxes and comment inputs now use the new WYSIWYG editor: + +![View of a description input, showing an editor box with a few formatting controls: Bold, italic, link and lists](/images/2025/07/new_wysiwyg_description_editor.png) + +This is a cut-down version relative to its use for pages, and should match the prior TinyMCE based editor in terms of supported functionality, but this should get the fundamental new editor code used & exercised in a lot more cases to help test things further before it eventually becomes the default editor for pages. + +### New WYSIWYG Editor Improvements + +On the topic of the new WYSIWYG editor, it's received a whole load of improvements and fixes since the last feature release: + +- Added toolbar for media elements for easier menu access. +- Added ability to insert new paragraph on click under certain last hard-to-escape blocks (tables, drawings etc...). +- Updated media embed code field to show existing embed code for direct editing. +- Updated media resize handling to be more reliable and to retain focus after resize. +- Updated table resize handles to be more efficient, and prevented them wondering far away from tables so often. +- Updated source code popup with larger input. +- Updated source code generation with newlines between top-level blocks. +- Fixed buggy media selection scenarios. +- Fixed media form "src" field not working when video is using source elements. +- Fixed table resize handles overlapping table captions. +- Fixed text formatting being inconsistent on new paragraphs. +- Fixed tiny image resize square on image insert. +- Fixed text highlight action & updated label. +- Fixed unstable table cell background colours. +- Fixed incorrect header levels used via format shortcuts. +- Fixed UI menu not reflecting block format changes. +- Fixed URLs not allowing any protocol as per old editor. + +A big thanks again to all those that have provided valuable feedback via [our beta discussion thread](https://github.com/BookStackApp/BookStack/issues/5631), this is essential for progressing the editor to a stable position. +I continue to welcome further feedback in that thread regarding any issues found. + +### Changelog Input Changes + +The page changelog input has been improved to provide a little extra editing breathing room, +since a single line input could get a little cramped, while also showing a character counter to help +avoid scenarios where text would be unexpectedly cut down on save. + +![BookStack page editor view focused on an open changelog input, which contains three lines of text and a 93/180 character counter below the input](/images/2025/07/larger_changelog_input.png) + +Thanks to [@shresthkapoor7](https://github.com/BookStackApp/BookStack/pull/5663) for providing this enhancement. + +### ZIP Import/Export API Endpoints + +Back [in v24.12 we introduced](/blog/bookstack-release-v24-12/#new-importable-export-format) the importable ZIP export format. +Now that this has had some use without report of significant issues, it seemed a sensible time to now roll this out to BookStack's REST API. + +There's new `export-zip` endpoints for books, chapters, and pages in addition to a range of endpoints for listing, showing, uploading, and running imports. + +![List of import API endpoints: GET list, POST create, GET read, POST run, and DELETE delete](/images/2025/07/import_api_endpoints.png) + +Thanks to [@LM-Nishant](https://github.com/BookStackApp/BookStack/pull/5592) for starting off the work on this one. + +### Parent Tag Classes + +For a while tag classes have been available to provide an easy method of applying customizations. +These are simple CSS classes added to the `` of the HTML page so that customizations can perform tag-dependant +style or JavaScript code changes/actions depending on what tags are applied. +These have worked well for pages but it was hard to use them for wider-scoped customizations, like entire book/chapter customizations for example. + +In this release we now include classes from parent chapters & books. These are distinct from standard tag classes since these will include the name of the parent item type. As an example, A tag of name `Categorisation` with a value of `Important` set on a book will result on the following classes being set on the `` when viewing child chapters/pages: + +- `book-tag-pair-categorisation-important` +- `book-tag-name-categorisation` +- `book-tag-value-important` + +You could then use these to apply customizations to an entire book. For example, if you then wanted this book-level tag to add a banner on the book and all main child chapter/page views, you could use the following "Custom HTML Head Content": + +```html + +``` + +Details of tag classes can be [found in our docs](/docs/admin/hacking-bookstack/#tag-classes). + +### Multi-Column Layout Refinements + +In this release cycle I spent a little time refining the sidebar behaviour in the main +three-column layout used for most views. +Sometimes, especially at tablet & half-desktop sizes, you'd see sidebar content be cut with harsh edges. +Spacing has now been altered to make this far less frequent while also making better use of space at those smaller +screen sizes. + +I've also added a fade to the sidebars when content is scrolled out of view to provide an indication that there's +content there to uncover: + +![BookStack page sidebar showing a fade over the contents lists which is disappearing behind the header bar](/images/2025/07/sidebar_fade.png) + +### Updated Permission Generation Handling + +Upon certain action, such as item creation or permission change, BookStack will recalculate & store +view access rights. In some cases, where changes were made in parallel, this could fail leading to +content being hidden from everyone until permissions we regenerated. + +For this release careful consideration has been applied to these actions, wrapping them at a higher level +in transactions, in a way that allows parallel changes to not fail, instead allowing each change to block other changes when needed. + +### Translations + +Since the last feature release Nepali has been added as a new language option! +A big thanks to Angel Pandey, Supriya Shrestha and gprabhat for their contributions which quickly +made the translations for Nepali complete. + +And of course a huge thanks to all the professional passage processors below who have contributed translations since the last feature release: + +- Angel Pandey (angel-pandey) - *Nepali - 12227 words* +- Ngoc Lan Phung (lanpncz) - *Vietnamese - 4903 words* +- ahmad abbaspour (deshneh.dar.diss) - *Persian - 1184 words* +- Al Desrahim (aldesrahim) - *Indonesian - 888 words* +- Supriya Shrestha (supriyashrestha) - *Nepali - 754 words* +- gprabhat - *Nepali - 623 words* +- toras9000 - *Japanese - 131 words* +- Honza Nagy (honza.nagy) - *Czech - 128 words* +- TapioM - *Finnish - 83 words* +- scureza - *Italian - 34 words* +- m0uch0 - *Spanish - 33 words* +- cbridi - *Portuguese, Brazilian - 33 words* +- Tim (thegatesdev) - *Dutch - 31 words* +- David (david-prv) - *German - 30 words* +- CrazyComputer - *Chinese Simplified - 61 words* +- matthias4217 - *French - 19 words* +- CellCat - *Chinese Simplified - 12 words* +- lingb58 - *Chinese Traditional - 10 words* +- mabdullah - *Arabic - 6 words* +- Ehsan Sadeghi (ehsansadeghi) - *Persian - 5 words* +- Firr (FirrV) - *Russian - 2 words* +- LiZerui (iamzrli) - *Chinese Traditional - 2 words* +- Indrek Haav (IndrekHaav) - *Estonian - 1 words* + +*Word counts are those tracked by Crowdin, indicating original EN words translated.* + +### Next Steps + +For the next development cycle I'm going to start to think about the extension/developer capabilities of the new WYSIWYG editor, so that many of the existing hacks/customizations could potentially be transferred to the new WYSIWYG editor as this will be an important requirement before making it the default. + +A few months back I [built a concept](https://github.com/BookStackApp/BookStack/pull/5552#issuecomment-2884752947) implementation of somewhat native LLM integration into BookStack, allowing search and LLM-based querying. I plan to revisit this and take it further to better answer/understand the questionables & considerations I discovered in my first exploration. + +### Full List of Changes + +**Released in v25.07** + +* Added plaintext markdown page editor input option. ([#5725](https://github.com/BookStackApp/BookStack/pull/5725), https://github.com/BookStackApp/BookStack/issues/5705) +* Added ZIP Import/Export API endpoints. Thanks to [@LM-Nishant](https://github.com/BookStackApp/BookStack/pull/5592). ([#5721](https://github.com/BookStackApp/BookStack/pull/5721), [#5592](https://github.com/BookStackApp/BookStack/pull/5592)) +* Added tag-classes based upon parent book/chapter. ([#5681](https://github.com/BookStackApp/BookStack/pull/5681), [#5217](https://github.com/BookStackApp/BookStack/issues/5217)) +* Updated comment and description inputs to use the new WYSIWYG editor. ([#5676](https://github.com/BookStackApp/BookStack/pull/5676)) +* Updated 3-column layout with better usability. ([#5685](https://github.com/BookStackApp/BookStack/pull/5685)) +* Updated changelog input to large area with character counter. Thanks to [@shresthkapoor7](https://github.com/BookStackApp/BookStack/pull/5663). ([#5663](https://github.com/BookStackApp/BookStack/pull/5663), [#5434](https://github.com/BookStackApp/BookStack/issues/5434)) +* Updated mail logic to remove use of our custom patched Symfony mailer. ([#5636](https://github.com/BookStackApp/BookStack/issues/5636)) +* Updated translations with latest Crowdin changes. ([#5696](https://github.com/BookStackApp/BookStack/pull/5696)) +* Updated many actions to better handle parallel permission generation. ([#5689](https://github.com/BookStackApp/BookStack/pull/5689), [#4838](https://github.com/BookStackApp/BookStack/issues/4838)) +* Updated new WYSIWYG editor with improvements & fixes. ([#5731](https://github.com/BookStackApp/BookStack/pull/5731)) +* Updated PHP package versions. + +**Released in v25.05.2** + +* Added Nepali Language. ([#5677](https://github.com/BookStackApp/BookStack/issues/5677)) +* Updated translations with latest Crowdin changes. ([#5695](https://github.com/BookStackApp/BookStack/pull/5695)) +* Updated PHP package versions. +* Updated content diffs to better group non-ascii language characters into words. +* Fixed error when loading opensearch endpoint with certain PHP in some environments. ([#5673](https://github.com/BookStackApp/BookStack/issues/5673)) +* Fixed namespace for test case. Thanks to [@bumperbox](https://github.com/BookStackApp/BookStack/pull/5668). ([#5668](https://github.com/BookStackApp/BookStack/pull/5668)) + +**Released in v25.05.1** + +* Updated new WYSIWYG editor with a range of fixes. ([#5653](https://github.com/BookStackApp/BookStack/pull/5653)) +* Fixed comment updates showing incorrect notification text. ([#5642](https://github.com/BookStackApp/BookStack/issues/5642)) +* Fixed search system ignoring words adjacent to non-breaking spaces. ([#5640](https://github.com/BookStackApp/BookStack/issues/5640)) +* Updated translations with latest Crowdin changes. ([#5637](https://github.com/BookStackApp/BookStack/pull/5637)) + +---- + +Header Image Credits: Photo by Dmitry Djouce (CC-BY-2) - Image Modified diff --git a/content/blog/2025/decade-of-bookstack.md b/content/blog/2025/decade-of-bookstack.md new file mode 100644 index 0000000..d057d7a --- /dev/null +++ b/content/blog/2025/decade-of-bookstack.md @@ -0,0 +1,139 @@ ++++ +categories = ["News"] +tags = ["News"] +title = "A Decade of BookStack" +image = "/images/blog-cover-images/cc-by-sa-2/elephants_bernard_dupont.jpg" +author = "Dan Brown" +slug = "decade-of-bookstack" +draft = false +date = 2025-07-12T21:48:00Z ++++ + +BookStack is now over 10 years old! The [initial commit](https://github.com/BookStackApp/BookStack/commit/eaa1765c7a68cd671bcb37a666203210bf05d217) for the project was made on the 12th of July 2015, and here we are a decade later. +A massive thanks to all those who have contributed to the project. Whether that's via providing code, reporting issues, providing translations, sponsoring, purchasing support, creating content, interacting with the community, or even just mentioning BookStack to others; This all helps drive the project forward and build motivation to keep the platform evolving. + +When I started the project I was just building something to suit a need at work, I didn't envision I'd still be developing this "simple little documentation tool" 10 years down the road for a much bigger audience. +I'm so happy that the project has been able to provide value to so many during this time, and I believe there are [many things to be proud of](/blog/things-proud-of-in-bookstack/) in this decade-old open source project. + +I look forward to seeing how the project evolves over the next 10 years! + +### Video with Q&A + +As part of this milestone I thought it'd be fun to do a community Q&A session. +You can find the Q&A, in addition to discussions for the topics of this blog-post, within the following video: + +{{}} + +### Financials + +Here's a high-level monthly breakdown of BookStack revenue sources, starting from 2022: + +[![Monthly breakdown of project finances across Kofi, Support Services & GitHub sponsors. Shows a general trend upwards, with some spikes from support services in across 2024 and 2025](/images/2025/07/bookstack_finances_jun_25.png)](/images/2025/07/bookstack_finances_jun_25.png) + +This is just income, so excludes costs, fees, taxes and many other expenses. Older figures may be slightly different to past years' blogposts due to tweaking how I collate & roll up the numbers, to make reporting a bit easier. + +Once again we can see an increase from support services, thanks to a high rate of renewals building upon new sign-ups. +Some further enterprise support purchases (£4,500/year) have created additional spikes, especially when within months which had +received many other professional support purchases (£450/year) & renewals. +The average monthly support revenue has climbed from £1,906 to £2,522 which represents a 32% year-on-year increase. + +Income via GitHub sponsors has seen the biggest increase, averaging £1,670 over the last 12 months, up 57% from the previous 12 months at £1,065. This is mostly due to extra company-level sponsorships in addition to a build-up of smaller donations. +KoFi donations are up slightly with a 4% increase to a monthly average of £275. + +Looking at these numbers, they seem pretty unbelievable. Over the past 12 months, the total revenue pretty much matches +what I was earning in my professional lead developer role before I left to focus on open source work, which is just +incredible. +Once again, a mahoosive thanks to all those that have contributed to this, and therefore provided a stable income for me as I work on BookStack! + +#### Donations Deep Dive + +If you'd like to learn more about the donations aspect of the project, I recently put together a video guide about my experiences of donations in open source which might be of interest: + +{{}} + +This is intended to be guidance for other open source maintainers, but features BookStack as my core example while diving into visualising all donations across 2024. + + +#### Donation Forwarding + +In [last year's post](/blog/9-years-of-bookstack/) I mentioned wanting to expand upon the donations I forward on to other open source services & libraries used to help build BookStack, which was at around £100 per month at that time. +Since then I have over doubled this amount to around £225 per month, although this excludes larger one-off donations made where GitHub sponsors is not used. + +Going forward I'll look to scale this up further again, mostly via increasing existing donations as I believe we are now donating to almost all of our dependencies which accept GitHub sponsors, while making larger one-off donations to non-GitHub sponsor based projects. + +### BookStack, In Numbers + +Following our BookStack birthday tradition, we'll again dive into the numbers to see how BookStack has grown over the past year. + +The below figures were collected at the time of writing *(10th July 2025)*, with changes in red/green reflecting change upon last year's numbers. + +#### GitHub Figures + +- [16,926 GitHub stars](https://github.com/BookStackApp/BookStack/stargazers) +2,616 +- [2,140 forks on GitHub](https://github.com/BookStackApp/BookStack/network/members) +330 +- 5,704 GitHub issues and PRs opened +598 +- 4,047 GitHub issues closed (+378), 653 currently open (+113) +- [207 releases published](https://github.com/BookStackApp/BookStack/releases) +17 + +#### Code Repository Stats + +- [5,079 commits](https://github.com/BookStackApp/BookStack/commits/development) +485 +- 186 direct git contributors +11 + +#### Social + +- [3,895 Discord members](https://www.bookstackapp.com/links/discord) +342 +- [2,281 Subreddit members](https://www.reddit.com/r/BookStack/) +719 +- [2,810 YouTube channel subscribers](https://www.youtube.com/c/BookStackApp) +622 +- [55 PeerTube channel followers](https://foss.video/c/bookstack/videos) +11 +- [843 Mastodon Followers](https://www.bookstackapp.com/links/mastodon) +287 +- [598 Twitter Followers](https://twitter.com/bookstack_app) -22 + +#### Website Analytics + +Main bookstackapp.com site only, Averaged over last 90 days: + +- 1,790 unique users per day +373 +- 4,064 page views per day +522 +- Operating system breakdown: + - 55% Windows + - 19% Mac +1% + - 9% Android + - 9% Linux + - 8% iOS/iPadOS + +[Our full website analytics can be found here.](https://analytics.bookstackapp.com/bookstackapp.com) + +#### CrowdIn (Project Translations) Numbers + +- 52 languages +6 +- 8,679 words to translate +620 +- 443 project members +106 + +#### Thoughts on the Numbers + +Once again I'm surprised to see continued visitor growth on the website, with about a 20% year-on-year +increase, since I've done little additional marketing. Not something I'll complain about though! +Just confirms that we don't need to worry that much about extra marketing as natural growth seems to be enough. + +Twitter followers are down, which makes sense since I stopped posting updates there from January, +with Twitter links being removed from the BookStack site a while before. +Our Mastodon follower count has continued to climb though, growing far above what we ever had on Twitter. It's nice to see that we can have more success on a friendlier, open, decentralised, albeit more niche, platform. + +The rate of project releases has slowed in comparison to last year, but there's also been more commits, reflecting a slower release cycle but with just as much development effort, if not more, put into them overall. + +### Further Reading + +Here are the non-release/update posts that you may have missed over the last year: + +- [Nine Years of BookStack](/blog/9-years-of-bookstack/) +- [BookStack Project Update for September 2024](/blog/project-update-september-24/) +- [BookStack in 2024](/blog/bookstack-in-2024/) +- [Testing Better Dependency Management](/blog/php-dependency-improvements/) +- [Using BookStack as a Linux File System](/blog/bookstack-filesystem/) +- [Things I'm Proud of in the BookStack Project](/blog/things-proud-of-in-bookstack/) + + +--- + +Header Image Credits:  Photo by Bernard DUPONT (CC-BY-SA-2) - Image Modified diff --git a/content/blog/2025/things-proud-of-in-bookstack.md b/content/blog/2025/things-proud-of-in-bookstack.md new file mode 100644 index 0000000..e8bc9fb --- /dev/null +++ b/content/blog/2025/things-proud-of-in-bookstack.md @@ -0,0 +1,105 @@ ++++ +categories = ["News"] +tags = ["News"] +title = "Things I'm Proud of in the BookStack Project" +image = "/images/blog-cover-images/woodland_path_dan_brown.webp" +author = "Dan Brown" +slug = "things-proud-of-in-bookstack" +draft = false +date = 2025-07-08T09:22:00Z ++++ + +With the BookStack project soon to be a decade old, I thought it'd be a great time to take a positive +look back and assess the things I'm proud about regarding the project. + + +### Fully Open Source & Free + +I'm very much proud that the project remains fully open source and free after 10 years. +There are no paid features, there's no dual licensing, no "open core", no in-app up-sells. +Just 100% fully free (both as in beer & rights) and open source software, allowing the platform to +be used, modified and redistributed by anyone without cost or significant limitation. + +I'm especially proud on this element having now had a sustainable income from the +project over the last couple of years. It's frequent to see projects move away from +open source ideals as they move into becoming full time businesses, so I think it's +amazing I'm able to receive an income with the software being fully open source. +Going full time was an experiment, in which I was not willing to compromise on +being open source, and that experiment has succeeded so far. + +On that note, I want to once again re-iterate my thanks to those who have donated, sponsored, +or have paid for our support offerings as this has helped me sustain that income in a FOSS-friendly way! + +### Kind & Open Community + +The BookStack community has grown over the decade where many thousands +of users have interacted with the project somehow. +It's incredible how positive the community has been, across +GitHub, Reddit, Discord, YouTube and Fosstodon. +In the last decade, there's probably been less than 5 individuals +I've found problematic. + +This goes further to the wider open source and self-hosted communities too. +From blog posts, news sites, podcasts and YouTube creators, there's been so many +that have created content, spread the word, or just been positive about BookStack, +for which I'm proud and thankful! + +In addition, we've had many kind folks get directly involved by contributing content +and code to BookStack. +The translations in BookStack in particular reflect the continuous efforts of a wonderful group of people +spending their time to make the platform more accessible to additional audiences. + +### Continued Stable Evolution + +There are various ways to evolve a project and manage releases, each with their own +advantages and disadvantages. For example, some may create big infrequent versions with lots to shout about. +Early on for BookStack, I decided on a continuous yet steady evolution, to best suit the business +scenario that I built BookStack, where stability and compatibility are much appreciated. + +I'm happy we've been able to provide that over the last decade, with very minimal breaking changes +along a steady upgrade path. Still today, an instance on the original public release could update +following our standard update guidance, with the most trouble being having to meet modern PHP versions, +and they should find their content intact and the interface familiar, albeit quite evolved & +refined along with many new features. + +### Retained Balance of Usability & Flexibility + +When originally building BookStack, usability for a mixed-technical-skill audience +was a core priority. Post public release, it was quickly picked up by more technical +audiences such as selfhosters and IT teams. Upon that, by nature of where/how our +communities exist, most feedback we receive is from the more technical type of user. +This can put pressure on the balance of addressing feedback and retaining focus +on our core values. + +Looking back, I'm happy with the balance we've been able to retain. +The UI remains simple to use, with single paths to achieve things and limited forks & options +in user flows. But we've also added significant features and depth for those that +are skilled and willing to seek them out. Examples that come to mind are: The Logical and visual theme +systems, the REST API, tag classes, advanced search filters, page templates. + +If interested, I've produced a couple of videos featuring the lesser known power user features +that can be found in BookStack: + +- [Power User Features in BookStack](https://foss.video/w/7J3aDc9JDL9JZkdw5H4a6R) +- [More Power User Features in BookStack](https://foss.video/w/b4aTq3YzsTVjdEFBQtuHgZ) + +### Steady Natural User-base Growth + +Over the decade our growth has been rather steady. We'd sometimes have spikes of activity at certain events like when on the front of Hacker News, but otherwise our line of growth has remained at a fairly consistent upwards angle. +I'm proud of this as it's reflective of the project not chasing an audience with heavy marketing, or expanding to new trends or features just for more growth. We can instead focus on the existing audience and their use-cases, instead of sacrificing that focus as a result of pressure to expand. + +### Provided Support + +I've proud of the level of support that I, among others in the community, have been able to provide to those wanting to use & get involved with the project. +Providing support can consume a massive amount of time, and in my opinion it's the quickest way to burn-out with an open source project. Sometimes you can spend hours trying to support someone that just had a simple configuration issue. +I see some projects strongly limit support, closing issues out early or even disabling them completely, and I can't blame them for this. I even do this on some of my secondary projects so I can keep focus on BookStack. + +When I started BookStack, having not maintained a popular open source project before, I set a relatively high bar in trying to help anyone who asks for help. While I have struggled with this at times, especially when BookStack was a side-project, I'm happy we've been able to mostly retain this as something which continues today. As of writing the number of closed GitHub issues for the core project stands at 4,042. +Each one of those cases can take time to consider, respond to, and progress to their close. + +We've even been able to expand support via new means like [our range of update & technical guide videos](https://foss.video/c/bookstack/videos). +We did introduce [paid support](https://www.bookstackapp.com/support/) a few years back, which does offer a priority level of support, but this hasn't really taken away what I've been able to provide for free. If anything, it has helped sustain it. + +--- + +Header Image Credits:  Photo by Dan Brown diff --git a/content/docs/admin/hacking-bookstack.md b/content/docs/admin/hacking-bookstack.md index fe10584..f2fa302 100644 --- a/content/docs/admin/hacking-bookstack.md +++ b/content/docs/admin/hacking-bookstack.md @@ -98,6 +98,21 @@ As an example of usage, pages with the tag `Priority: Critical` could have their ``` +Chapters and pages will also receive tag classes from their parent book/chapter, prefixed with their item type. For example, a tag name/value pair of `Category: Animals` applied to a book will result in the following classes added to the body of child chapter/page views: + +- book-tag-name-category +- book-tag-value-animals +- book-tag-pair-category-animals + +This means that you could perform a book-wide customization by CSS targeting like so: + +```html + +``` + --- ### Export Classes diff --git a/hacks b/hacks index 9451d98..a7185d1 160000 --- a/hacks +++ b/hacks @@ -1 +1 @@ -Subproject commit 9451d98cea4f4c049e6b022ab6f24982bf3d1434 +Subproject commit a7185d10f9467b693ebe66dc20bfbcc26446f42d diff --git a/package.json b/package.json index 75b97dd..027eb48 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,15 @@ "build:css:dev": "sass ./themes/bookstack/sass:./themes/bookstack/static/css", "build:css:watch": "sass ./themes/bookstack/sass:./themes/bookstack/static/css --watch", "build:hugo:prod": "hugo", + "build:hugo:dev": "hugo -b http://localhost:8080", "build:hugo:watch": "hugo serve -DF", "build:search": "./search/webidx.pl public ./static/search.db", - "build:search:compress": "brotli -fZk ./static/search.db && gzip -fk9 ./static/search.db", + "build:dev": "npm-run-all --sequential build:css:dev build:hugo:dev", "build": "npm-run-all --sequential build:css:prod build:search build:hugo:prod", "serve": "npm-run-all build:hugo:watch", "dev": "npm-run-all --parallel build:hugo:watch build:css:watch", "deploy:server": "rsync -avx --delete --exclude '.git/' --exclude 'node_modules/' --exclude 'search/data/' ./ bs-site:/var/www/bookstackapp.com/", - "deploy": "npm-run-all --sequential build:css:prod build:search build:search:compress build:hugo:prod deploy:server", + "deploy": "npm-run-all --sequential build:css:prod build:search build:hugo:prod deploy:server", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Dan Brown", diff --git a/readme.md b/readme.md index 1ee17f3..2beca96 100644 --- a/readme.md +++ b/readme.md @@ -25,7 +25,7 @@ SCSS is used for the styling. Install NPM dependencies via `npm install` or `yar ### Search -Search is performed using [webidx](https://github.com/gbxyz/webidx), which essentially builds a sqlite database search index, that is then loaded to browser upon search then queried locally in-browser via [sql.js](https://github.com/sql-js/sql.js). +Search is performed using [webidx](https://github.com/gbxyz/webidx), which essentially builds a sqlite database search index. This is then queried via a php file (`static/search.php` in theme) with results passed back to the front-end JS logic. This files required are all in this repo, and hacked to suit our use-case. The script to build the index is located at `search/webidx.pl`, and can be ran via the npm script @@ -36,13 +36,6 @@ npm run build:search **Note:** you may need to install some dependencies to run the script see the `search/webidx.pl` for more information. -The above command will build the sqlite index database to `static/search.db`, intended to be deployed to production. There is also a `npm run build:search:compress` command to compress the database file using brotli and gzip (requires both to be installed). In production use, these compressed files should be deployed then served from their compressed state where possible. Here's relevant config for nginx: - -```nginx -location ~* \.(db)$ { - brotli_static on; - gzip_static on; -} -``` +The above command will build the sqlite index database to `static/search.db`, intended to be deployed to production. Much of the search UI handling logic can be found in our `themes/bookstack/static/js/scripts.js` file. diff --git a/static/images/2025/07/bookstack_finances_jun_25.png b/static/images/2025/07/bookstack_finances_jun_25.png new file mode 100644 index 0000000..a6e039e --- /dev/null +++ b/static/images/2025/07/bookstack_finances_jun_25.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6080f43fbeabc22812d632413e1e8e55c5ad960957f8b02221a7cf8adfa18fb9 +size 22768 diff --git a/static/images/2025/07/import_api_endpoints.png b/static/images/2025/07/import_api_endpoints.png new file mode 100644 index 0000000..952c6c9 --- /dev/null +++ b/static/images/2025/07/import_api_endpoints.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09974a416f56361b0605cefbfaaf736f5c3c1f2475f010df3ba48ebc27ec1a25 +size 6987 diff --git a/static/images/2025/07/larger_changelog_input.png b/static/images/2025/07/larger_changelog_input.png new file mode 100644 index 0000000..7b46464 --- /dev/null +++ b/static/images/2025/07/larger_changelog_input.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60fc1fde34d569e78b0ab6ffa1d5f5ed282552af3e9d256eb10e8b617db3140c +size 26860 diff --git a/static/images/2025/07/new_wysiwyg_description_editor.png b/static/images/2025/07/new_wysiwyg_description_editor.png new file mode 100644 index 0000000..4efc35d --- /dev/null +++ b/static/images/2025/07/new_wysiwyg_description_editor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d145cc0600e1ee5900bc1e120b31c0fc5629d61413f27ef52c2addd5ed8882c +size 22829 diff --git a/static/images/2025/07/plaintext_markdown_input.png b/static/images/2025/07/plaintext_markdown_input.png new file mode 100644 index 0000000..a8b79a3 --- /dev/null +++ b/static/images/2025/07/plaintext_markdown_input.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf7afe3346b109ef836e46d511a8c8d2d40a15894faf1ce3cbff8952591c674a +size 52826 diff --git a/static/images/2025/07/sidebar_fade.png b/static/images/2025/07/sidebar_fade.png new file mode 100644 index 0000000..3f782e6 --- /dev/null +++ b/static/images/2025/07/sidebar_fade.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e1943e7215619fcc51183d9519f881bcbaa87bff27bce11eeeaf8eb29814f1b +size 19033 diff --git a/static/images/blog-cover-images/cc-by-2/attribution.txt b/static/images/blog-cover-images/cc-by-2/attribution.txt index 372f337..6f06877 100644 --- a/static/images/blog-cover-images/cc-by-2/attribution.txt +++ b/static/images/blog-cover-images/cc-by-2/attribution.txt @@ -6,4 +6,9 @@ Image modified in usage. karen-roe-pond.jpg Copyright Karen Roe Source: https://www.flickr.com/photos/karen_roe/8247172011 -Image modified in usage: Cropped, upscaled, sharpened. \ No newline at end of file +Image modified in usage: Cropped, upscaled, sharpened. +------------------------------- +dmitry-djouce-cow.jpg +Copyright Dmitry Djouce +Source: https://www.flickr.com/photos/nothingpersonal/31959154448 +Image modified in usage: Cropped, sharpened. \ No newline at end of file diff --git a/static/images/blog-cover-images/cc-by-2/dmitry-djouce-cow.jpg b/static/images/blog-cover-images/cc-by-2/dmitry-djouce-cow.jpg new file mode 100644 index 0000000..b314e94 --- /dev/null +++ b/static/images/blog-cover-images/cc-by-2/dmitry-djouce-cow.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e0d7c8a5a2b95f2f1b3709f68b6ecb47c1724f2e5a6d58f76ccb61ffd6e47c8 +size 454804 diff --git a/static/images/blog-cover-images/cc-by-sa-2/attribution.txt b/static/images/blog-cover-images/cc-by-sa-2/attribution.txt index 06e3cfd..885ab17 100644 --- a/static/images/blog-cover-images/cc-by-sa-2/attribution.txt +++ b/static/images/blog-cover-images/cc-by-sa-2/attribution.txt @@ -1,4 +1,9 @@ burnieside-steven-brown.jpg Copyright Steven Brown Source: https://www.geograph.org.uk/photo/7714511 +Image modified in usage. +--- +elephants_bernard_dupont.jpg +Copyright Bernard DUPONT +Source: https://commons.wikimedia.org/wiki/File:Savanna_Elephants_(Loxodonta_africana)_family_coming_to_drink_..._(Photo_JC_PLE)_(52843889627).jpg Image modified in usage. \ No newline at end of file diff --git a/static/images/blog-cover-images/cc-by-sa-2/elephants_bernard_dupont.jpg b/static/images/blog-cover-images/cc-by-sa-2/elephants_bernard_dupont.jpg new file mode 100644 index 0000000..04b95b4 --- /dev/null +++ b/static/images/blog-cover-images/cc-by-sa-2/elephants_bernard_dupont.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73060fa27315a64bf364c71eb82cb5f80e2c0c334c619057295aa58dcfff8e0b +size 353139 diff --git a/static/images/blog-cover-images/woodland_path_dan_brown.webp b/static/images/blog-cover-images/woodland_path_dan_brown.webp new file mode 100644 index 0000000..df847fb --- /dev/null +++ b/static/images/blog-cover-images/woodland_path_dan_brown.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a29c11329ed0e91e680f4db53c85f13cc296e8fb914771e6fa5c884e70f24ad2 +size 415546 diff --git a/static/images/pt/ffZqEoZHLwzVwmKaGYsA3A.webp b/static/images/pt/ffZqEoZHLwzVwmKaGYsA3A.webp new file mode 100644 index 0000000..53fccc0 --- /dev/null +++ b/static/images/pt/ffZqEoZHLwzVwmKaGYsA3A.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c47260eda0dddc43f02012307659738b404c077fc74ca6f5460627dbf5a0328e +size 49344 diff --git a/static/images/pt/t8y648RHNLqYNs6J3rPHP7.webp b/static/images/pt/t8y648RHNLqYNs6J3rPHP7.webp new file mode 100644 index 0000000..43b0be0 --- /dev/null +++ b/static/images/pt/t8y648RHNLqYNs6J3rPHP7.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f8c21e795225e054f0de26d9fe7bf5fb2350c2766ca590187b031c75435a3b3 +size 51314 diff --git a/static/images/sponsors/sitespeak.png b/static/images/sponsors/sitespeak.png new file mode 100644 index 0000000..b95f1ce --- /dev/null +++ b/static/images/sponsors/sitespeak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db0a778d0bdab44548f21850e06ba991eaebd1049479d2a26203b9528d44f56a +size 12148 diff --git a/static/images/yt/hILUhL_l_bU.webp b/static/images/yt/hILUhL_l_bU.webp new file mode 100644 index 0000000..e8f4323 --- /dev/null +++ b/static/images/yt/hILUhL_l_bU.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4db56d9a68b3e552a2a1cbf2a58c8d1353344ca1d236c251d8548d8a87b7474c +size 59972 diff --git a/themes/bookstack/layouts/index.html b/themes/bookstack/layouts/index.html index a75f5ce..85a2ef0 100644 --- a/themes/bookstack/layouts/index.html +++ b/themes/bookstack/layouts/index.html @@ -173,49 +173,53 @@ -
Our Bronze Sponsors
+ + - diff --git a/themes/bookstack/layouts/partials/footer.html b/themes/bookstack/layouts/partials/footer.html index 4ac83b2..9054f31 100644 --- a/themes/bookstack/layouts/partials/footer.html +++ b/themes/bookstack/layouts/partials/footer.html @@ -57,9 +57,6 @@ - - - diff --git a/themes/bookstack/layouts/partials/header.html b/themes/bookstack/layouts/partials/header.html index c655ec1..031b9c2 100644 --- a/themes/bookstack/layouts/partials/header.html +++ b/themes/bookstack/layouts/partials/header.html @@ -93,7 +93,7 @@
- +
diff --git a/themes/bookstack/sass/styles.scss b/themes/bookstack/sass/styles.scss index 34427a2..7f8379c 100644 --- a/themes/bookstack/sass/styles.scss +++ b/themes/bookstack/sass/styles.scss @@ -93,6 +93,7 @@ display: block; margin: $-s $-xl; min-width: 240px; + text-align: center; } } diff --git a/themes/bookstack/static/js/script.js b/themes/bookstack/static/js/script.js index 1cc52bb..6b7e5a7 100644 --- a/themes/bookstack/static/js/script.js +++ b/themes/bookstack/static/js/script.js @@ -57,14 +57,12 @@ const searchInput = document.getElementById('site-search-input'); const searchDialog = searchForm.querySelector('dialog'); async function runSearch() { - const searchTerm = searchInput.value.toLowerCase(); + const searchTerm = searchInput.value; let pages = []; try { - pages = await window.webidx.search({ - dbfile:'/search.db', - query: searchTerm, - }); + const resp = await fetch(`/search.php?query=${encodeURIComponent(searchTerm)}`); + pages = await resp.json(); } catch (error) { searchDialog.innerHTML = 'Failed to load search results'; console.error(error); @@ -72,9 +70,10 @@ async function runSearch() { } // Sort pages to prioritise those with word in title + const lowerSearchTerm = searchTerm.toLowerCase(); pages.sort((a, b) => { - const aScore = (a.url.includes(searchTerm) || a.title.toLowerCase().includes(searchTerm)) ? 1 : 0; - const bScore = (b.url.includes(searchTerm) || b.title.toLowerCase().includes(searchTerm)) ? 1 : 0; + const aScore = (a.url.includes(lowerSearchTerm) || a.title.toLowerCase().includes(lowerSearchTerm)) ? 1 : 0; + const bScore = (b.url.includes(lowerSearchTerm) || b.title.toLowerCase().includes(lowerSearchTerm)) ? 1 : 0; return bScore - aScore; }); @@ -88,13 +87,15 @@ async function runSearch() { const categoryNames = Object.keys(categorised); for (const page of pages) { + let pageCategory = null; for (const categoryName of categoryNames) { const category = categorised[categoryName]; - if (page.url.includes(category.filter)) { - category.pages.push(page); + if (page.url.startsWith(category.filter)) { + pageCategory = category; break; } } + (pageCategory || categorised.other).pages.push(page); } const categoryResults = categoryNames.map(name => { @@ -114,8 +115,8 @@ async function runSearch() { const resultList = categoryResults.length ? categoryResults : [emptyResult]; const results = el('div', {}, resultList); - for (const child of searchDialog.children) { - child.remove(); + while (searchDialog.firstChild) { + searchDialog.removeChild(searchDialog.firstChild); } searchDialog.append(results); @@ -128,33 +129,59 @@ function showSearchDialog() { } searchDialog.show(); searchInput.focus(); - + const clickListener = e => { - if(!e.target.closest('dialog')) { + if (!e.target.closest('dialog')) { closeListener(); } }; const escListener = e => { if (e.key === 'Escape') { - closeListener(); + closeListener(); } }; + const arrowListener = e => { + if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') { + return; + } + e.preventDefault(); + + const links = Array.from(searchDialog.querySelectorAll('a')); + const focusables = [searchInput, ...links]; + if (focusables.length < 2) { // Only search input + return; + } + + const active = document.activeElement; + let currentIndex = focusables.indexOf(active); + + if (e.key === 'ArrowDown') { + currentIndex = (currentIndex + 1) % focusables.length; + } else { // ArrowUp + currentIndex = (currentIndex - 1 + focusables.length) % focusables.length; + } + + focusables[currentIndex].focus(); + }; + const mouseLeaveListener = e => { closeListener(); - } + }; const closeListener = () => { searchDialog.close(); document.removeEventListener('click', clickListener); document.removeEventListener('keydown', escListener); searchForm.removeEventListener('mouseleave', mouseLeaveListener); + searchForm.removeEventListener('keydown', arrowListener); }; document.addEventListener('click', clickListener); document.addEventListener('keydown', escListener); searchForm.addEventListener('mouseleave', mouseLeaveListener); + searchForm.addEventListener('keydown', arrowListener); } function showSearchLoading() { diff --git a/themes/bookstack/static/libs/sql-wasm.js b/themes/bookstack/static/libs/sql-wasm.js deleted file mode 100644 index b234f6b..0000000 --- a/themes/bookstack/static/libs/sql-wasm.js +++ /dev/null @@ -1,8 +0,0 @@ - -// sql.js 1.10.1 -// Copyright (c) 2017 sql.js authors (see AUTHORS) -// https://github.com/sql-js/sql.js/blob/master/LICENSE -// https://github.com/sql-js/sql.js/blob/master/AUTHORS -// MIT license -// Fetched via cdnjs: https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.1/sql-wasm.min.js -var initSqlJsPromise=void 0,initSqlJs=function(Dt){return initSqlJsPromise=initSqlJsPromise||new Promise(function(A,I){var B,S,x,O,G,R,a,e=void 0!==Dt?Dt:{},H=e.onAbort,t=(e.onAbort=function(e){I(new Error(e)),H&&H(e)},e.postRun=e.postRun||[],e.postRun.push(function(){A(e)}),module=void 0,(B||=void 0!==e?e:{}).onRuntimeInitialized=function(){function u(e,t){switch(typeof t){case"boolean":C(e,t?1:0);break;case"number":W(e,t);break;case"string":J(e,t,-1,-1);break;case"object":var r;null===t?R(e):null!=t.length?(r=Mt(t,kt),K(e,r,t.length,-1),Ht(r)):H(e,"Wrong API use : tried to return a value of an unknown type ("+t+").",-1);break;default:R(e)}}function l(e,t){for(var r=[],n=0;n>>0),null!=e){var t=this.filename,r=t;if((n="/")&&(n="string"==typeof n?n:He(n),r=t?X(n+"/"+t):n),r=Ke(r,4095&(void 0!==(t=Ae(!0,!0))?t:438)|32768,0),e){if("string"==typeof e){for(var n=Array(e.length),i=0,a=e.length;i(e=ue(e)?new URL(e):x.normalize(e),S.readFileSync(e,t?void 0:"utf8")),G=e=>e=(e=O(e,!0)).buffer?e:new Uint8Array(e),R=(e,r,n,i=!0)=>{e=ue(e)?new URL(e):x.normalize(e),S.readFile(e,i?void 0:"utf8",(e,t)=>{e?n(e):r(i?t.buffer:t)})},!B.thisProgram&&1"[Emscripten Module object]"):(N||j)&&(j?r=self.location.href:"undefined"!=typeof document&&document.currentScript&&(r=document.currentScript.src),r=0!==r.indexOf("blob:")?r.substr(0,r.replace(/[?#].*/,"").lastIndexOf("/")+1):"",O=e=>{var t=new XMLHttpRequest;return t.open("GET",e,!1),t.send(null),t.responseText},j&&(G=e=>{var t=new XMLHttpRequest;return t.open("GET",e,!1),t.responseType="arraybuffer",t.send(null),new Uint8Array(t.response)}),R=(e,t,r)=>{var n=new XMLHttpRequest;n.open("GET",e,!0),n.responseType="arraybuffer",n.onload=()=>{200==n.status||0==n.status&&n.response?t(n.response):r()},n.onerror=r,n.send(null)}),B.print||console.log.bind(console)),o=B.printErr||console.error.bind(console);Object.assign(B,t),B.thisProgram&&(L=B.thisProgram),B.wasmBinary&&(a=B.wasmBinary),"object"!=typeof WebAssembly&&i("no native wasm support detected");var P,Q,V,U,h,c,z,F,W=!1;function J(){var e=P.buffer;B.HEAP8=Q=new Int8Array(e),B.HEAP16=U=new Int16Array(e),B.HEAPU8=V=new Uint8Array(e),B.HEAPU16=new Uint16Array(e),B.HEAP32=h=new Int32Array(e),B.HEAPU32=c=new Uint32Array(e),B.HEAPF32=z=new Float32Array(e),B.HEAPF64=F=new Float64Array(e)}var K=[],C=[],ne=[];var s=0,ie=null,ae=null;function i(e){throw B.onAbort?.(e),o(e="Aborted("+e+")"),W=!0,new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.")}var oe,se=e=>e.startsWith("data:application/octet-stream;base64,"),ue=e=>e.startsWith("file://");function le(e){if(e==oe&&a)return new Uint8Array(a);if(G)return G(e);throw"both async and sync fetching of the wasm failed"}function fe(e,t,r){return function(r){if(!a&&(N||j)){if("function"==typeof fetch&&!ue(r))return fetch(r,{credentials:"same-origin"}).then(e=>{if(e.ok)return e.arrayBuffer();throw"failed to load wasm binary file at '"+r+"'"}).catch(()=>le(r));if(R)return new Promise((t,e)=>{R(r,e=>t(new Uint8Array(e)),e)})}return Promise.resolve().then(()=>le(r))}(e).then(e=>WebAssembly.instantiate(e,t)).then(e=>e).then(r,e=>{o("failed to asynchronously prepare wasm: "+e),i(e)})}se(oe="sql-wasm.wasm")||(t=oe,oe=B.locateFile?B.locateFile(t,r):r+t);var u,l,he=e=>{for(;0>0];case"i16":return U[e>>1];case"i32":return h[e>>2];case"i64":i("to do getValue(i64) use WASM_BIGINT");case"float":return z[e>>2];case"double":return F[e>>3];case"*":return c[e>>2];default:i("invalid type for getValue: "+t)}}function ce(e){var t="i32";switch(t=t.endsWith("*")?"*":t){case"i1":case"i8":Q[e>>0]=0;break;case"i16":U[e>>1]=0;break;case"i32":h[e>>2]=0;break;case"i64":i("to do setValue(i64) use WASM_BIGINT");case"float":z[e>>2]=0;break;case"double":F[e>>3]=0;break;case"*":c[e>>2]=0;break;default:i("invalid type for setValue: "+t)}}var de="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,d=(e,t,r)=>{var n=t+r;for(r=t;e[r]&&!(n<=r);)++r;if(16>10,56320|1023&o)))):n+=String.fromCharCode(o)}return n},be=(e,t)=>e?d(V,e,t):"",me=(e,t)=>{for(var r=0,n=e.length-1;0<=n;n--){var i=e[n];"."===i?e.splice(n,1):".."===i?(e.splice(n,1),r++):r&&(e.splice(n,1),r--)}if(t)for(;r;r--)e.unshift("..");return e},X=e=>{var t="/"===e.charAt(0),r="/"===e.substr(-1);return(e=(e=me(e.split("/").filter(e=>!!e),!t).join("/"))||t?e:".")&&r&&(e+="/"),(t?"/":"")+e},pe=e=>{var t=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(e).slice(1);return e=t[0],t=t[1],e||t?e+(t&&=t.substr(0,t.length-1)):"."},we=e=>{if("/"===e)return"/";var t=(e=(e=X(e)).replace(/\/$/,"")).lastIndexOf("/");return-1===t?e:e.substr(t+1)},_e=e=>(_e=(()=>{if("object"==typeof crypto&&"function"==typeof crypto.getRandomValues)return e=>crypto.getRandomValues(e);if(T)try{var t=require("crypto");if(t.randomFillSync)return e=>t.randomFillSync(e);var r=t.randomBytes;return e=>(e.set(r(e.byteLength)),e)}catch(e){}i("initRandomDevice")})())(e);function ve(){for(var e="",t=!1,r=arguments.length-1;-1<=r&&!t;r--){if("string"!=typeof(t=0<=r?arguments[r]:"/"))throw new TypeError("Arguments to path.resolve must be strings");if(!t)return"";e=t+"/"+e,t="/"===t.charAt(0)}return(t?"/":"")+(e=me(e.split("/").filter(e=>!!e),!t).join("/"))||"."}var ye=[],$=e=>{for(var t=0,r=0;r{if(!(0>6}else{if(o<=65535){if(n<=r+2)break;t[r++]=224|o>>12}else{if(n<=r+3)break;t[r++]=240|o>>18,t[r++]=128|o>>12&63}t[r++]=128|o>>6&63}t[r++]=128|63&o}}return t[r]=0,r-i};function ge(e,t){var r=Array($(e)+1);return e=Z(e,r,0,r.length),t&&(r.length=e),r}var qe=[];function Ee(e,t){qe[e]={input:[],output:[],Xa:t},We(e,ke)}var ke={open(e){var t=qe[e.node.rdev];if(!t)throw new m(43);e.tty=t,e.seekable=!1},close(e){e.tty.Xa.fsync(e.tty)},fsync(e){e.tty.Xa.fsync(e.tty)},read(e,t,r,n){if(!e.tty||!e.tty.Xa.sb)throw new m(60);for(var i=0,a=0;a>>0),0!=r&&(t=Math.max(t,256)),r=e.Ia,e.Ia=new Uint8Array(t),0=e.node.Ma)return 0;if(8<(e=Math.min(e.node.Ma-i,n))&&a.subarray)t.set(a.subarray(i,i+e),r);else for(n=0;n{var r=0;return e&&(r|=365),t&&(r|=146),r},Ie=null,Se={},xe=[],Oe=1,b=null,Ge=!0,m=null,Re={};function p(e,t={}){if(!(e=ve(e)))return{path:"",node:null};if(8<(t=Object.assign({qb:!0,kb:0},t)).kb)throw new m(32);e=e.split("/").filter(e=>!!e);for(var r=Ie,n="/",i=0;i>>0)%b.length}function Ne(e){var t=Le(e.parent.id,e.name);if(b[t]===e)b[t]=e.Wa;else for(t=b[t];t;){if(t.Wa===e){t.Wa=e.Wa;break}t=t.Wa}}function w(e,t){var r;if(r=(r=v(e,"x"))?r:e.Ga.lookup?0:2)throw new m(r,e);for(r=b[Le(e.id,t)];r;r=r.Wa){var n=r.name;if(r.parent.id===e.id&&n===t)return r}return e.Ga.lookup(e,t)}function je(e,t,r,n){return t=Le((e=new St(e,t,r,n)).parent.id,e.name),e.Wa=b[t],b[t]=e}function _(e){return 16384==(61440&e)}function Te(e){var t=["r","w","rw"][3&e];return 512&e&&(t+="w"),t}function v(e,t){return!Ge&&(t.includes("r")&&!(292&e.mode)||t.includes("w")&&!(146&e.mode)||t.includes("x")&&!(73&e.mode))?2:0}function De(e,t){try{return w(e,t),20}catch(e){}return v(e,"wx")}function Pe(e,t,r){try{var n=w(e,t)}catch(e){return e.Ka}if(e=v(e,"wx"))return e;if(r){if(!_(n.mode))return 54;if(n===n.parent||"/"===He(n))return 10}else if(_(n.mode))return 31;return 0}function y(e){if(e=xe[e])return e;throw new m(8)}function Ue(e,t=-1){return ut||((ut=function(){this.$a={}}).prototype={},Object.defineProperties(ut.prototype,{object:{get(){return this.node},set(e){this.node=e}},flags:{get(){return this.$a.flags},set(e){this.$a.flags=e}},position:{get(){return this.$a.position},set(e){this.$a.position=e}}})),e=Object.assign(new ut,e),-1==t&&(t=function(){for(var e=0;e<=4096;e++)if(!xe[e])return e;throw new m(33)}()),e.fd=t,xe[t]=e}var ze,Fe={open(e){e.Ha=Se[e.node.rdev].Ha,e.Ha.open?.(e)},Ta(){throw new m(70)}};function We(e,t){Se[e]={Ha:t}}function Je(e,t){var r="/"===t,n=!t;if(r&&Ie)throw new m(10);if(!r&&!n){var i=p(t,{qb:!1});if(t=i.path,(i=i.node).Va)throw new m(10);if(!_(i.mode))throw new m(54)}((e=e.Ra(t={type:e,Pb:{},tb:t,Cb:[]})).Ra=t).root=e,r?Ie=e:i&&(i.Va=t,i.Ra&&i.Ra.Cb.push(t))}function Ke(e,t,r){var n=p(e,{parent:!0}).node;if(!(e=we(e))||"."===e||".."===e)throw new m(28);var i=De(n,e);if(i)throw new m(i);if(n.Ga.ab)return n.Ga.ab(n,e,t,r);throw new m(63)}function n(e,t){return Ke(e,1023&(void 0!==t?t:511)|16384,0)}function Ce(e,t,r){void 0===r&&(r=t,t=438),Ke(e,8192|t,r)}function Be(e,t){if(!ve(e))throw new m(44);var r=p(t,{parent:!0}).node;if(!r)throw new m(44);var n=De(r,t=we(t));if(n)throw new m(n);if(!r.Ga.symlink)throw new m(63);r.Ga.symlink(r,t,e)}function Qe(e){var t=p(e,{parent:!0}).node,r=w(t,e=we(e)),n=Pe(t,e,!0);if(n)throw new m(n);if(!t.Ga.rmdir)throw new m(63);if(r.Va)throw new m(10);t.Ga.rmdir(t,e),Ne(r)}function Ve(e){var t=p(e,{parent:!0}).node;if(!t)throw new m(44);var r=w(t,e=we(e)),n=Pe(t,e,!1);if(n)throw new m(n);if(!t.Ga.unlink)throw new m(63);if(r.Va)throw new m(10);t.Ga.unlink(t,e),Ne(r)}function Ye(e){if(!(e=p(e).node))throw new m(44);if(e.Ga.readlink)return ve(He(e.parent),e.Ga.readlink(e));throw new m(28)}function Xe(e,t){if(!(e=p(e,{Sa:!t}).node))throw new m(44);if(e.Ga.Pa)return e.Ga.Pa(e);throw new m(63)}function $e(e){return Xe(e,!0)}function Ze(e,t){if(!(e="string"==typeof e?p(e,{Sa:!0}).node:e).Ga.Oa)throw new m(63);e.Ga.Oa(e,{mode:4095&t|-4096&e.mode,timestamp:Date.now()})}function et(e,t){if(t<0)throw new m(28);if(!(e="string"==typeof e?p(e,{Sa:!0}).node:e).Ga.Oa)throw new m(63);if(_(e.mode))throw new m(31);if(32768!=(61440&e.mode))throw new m(28);var r=v(e,"w");if(r)throw new m(r);e.Ga.Oa(e,{size:t,timestamp:Date.now()})}function ee(e,t,r){if(""===e)throw new m(44);if("string"==typeof t){var n={r:0,"r+":2,w:577,"w+":578,a:1089,"a+":1090}[t];if(void 0===n)throw Error("Unknown file open mode: "+t);t=n}if(r=64&t?4095&(void 0===r?438:r)|32768:0,"object"==typeof e)var i=e;else{e=X(e);try{i=p(e,{Sa:!(131072&t)}).node}catch(e){}}if(n=!1,64&t)if(i){if(128&t)throw new m(20)}else i=Ke(e,r,0),n=!0;if(!i)throw new m(44);if(8192==(61440&i.mode)&&(t&=-513),65536&t&&!_(i.mode))throw new m(54);if(!n&&(r=i?40960==(61440&i.mode)?32:_(i.mode)&&("r"!==Te(t)||512&t)?31:v(i,Te(t)):44))throw new m(r);return 512&t&&!n&&et(i,0),t&=-131713,(i=Ue({node:i,path:He(i),flags:t,seekable:!0,position:0,Ha:i.Ha,Fb:[],error:!1})).Ha.open&&i.Ha.open(i),!B.logReadFiles||1&t||(e in(lt||={})||(lt[e]=1)),i}function tt(e){if(null===e.fd)throw new m(8);e.hb&&(e.hb=null);try{e.Ha.close&&e.Ha.close(e)}catch(e){throw e}finally{xe[e.fd]=null}e.fd=null}function rt(e,t,r){if(null===e.fd)throw new m(8);if(!e.seekable||!e.Ha.Ta)throw new m(70);if(0!=r&&1!=r&&2!=r)throw new m(28);e.position=e.Ha.Ta(e,t,r),e.Fb=[]}function nt(e,t,r,n,i){if(n<0||i<0)throw new m(28);if(null===e.fd)throw new m(8);if(1==(2097155&e.flags))throw new m(8);if(_(e.node.mode))throw new m(31);if(!e.Ha.read)throw new m(28);var a=void 0!==i;if(a){if(!e.seekable)throw new m(70)}else i=e.position;return t=e.Ha.read(e,t,r,n,i),a||(e.position+=t),t}function it(e,t,r,n,i){if(n<0||i<0)throw new m(28);if(null===e.fd)throw new m(8);if(0==(2097155&e.flags))throw new m(8);if(_(e.node.mode))throw new m(31);if(!e.Ha.write)throw new m(28);e.seekable&&1024&e.flags&&rt(e,0,2);var a=void 0!==i;if(a){if(!e.seekable)throw new m(70)}else i=e.position;return t=e.Ha.write(e,t,r,n,i,void 0),a||(e.position+=t),t}function at(){m||((m=function(e,t){this.name="ErrnoError",this.node=t,this.Eb=function(e){this.Ka=e},this.Eb(e),this.message="FS error"}).prototype=Error(),m.prototype.constructor=m,[44].forEach(e=>{Re[e]=new m(e),Re[e].stack=""}))}function ot(e,s,a){e=X("/dev/"+e);var t=Ae(!!s,!!a),r=(st||=64,st++<<8|0);We(r,{open(e){e.seekable=!1},close(){a?.buffer?.length&&a(10)},read(e,t,r,n){for(var i=0,a=0;a>2]=n.dev,h[r+4>>2]=n.mode,c[r+8>>2]=n.nlink,h[r+12>>2]=n.uid,h[r+16>>2]=n.gid,h[r+20>>2]=n.rdev,l=[n.size>>>0,(u=n.size,1<=+Math.abs(u)?0>>0:~~+Math.ceil((u-(~~u>>>0))/4294967296)>>>0:0)],h[r+24>>2]=l[0],h[r+28>>2]=l[1],h[r+32>>2]=4096,h[r+36>>2]=n.blocks,e=n.atime.getTime(),t=n.mtime.getTime();var i=n.ctime.getTime();return l=[Math.floor(e/1e3)>>>0,(u=Math.floor(e/1e3),1<=+Math.abs(u)?0>>0:~~+Math.ceil((u-(~~u>>>0))/4294967296)>>>0:0)],h[r+40>>2]=l[0],h[r+44>>2]=l[1],c[r+48>>2]=e%1e3*1e3,l=[Math.floor(t/1e3)>>>0,(u=Math.floor(t/1e3),1<=+Math.abs(u)?0>>0:~~+Math.ceil((u-(~~u>>>0))/4294967296)>>>0:0)],h[r+56>>2]=l[0],h[r+60>>2]=l[1],c[r+64>>2]=t%1e3*1e3,l=[Math.floor(i/1e3)>>>0,(u=Math.floor(i/1e3),1<=+Math.abs(u)?0>>0:~~+Math.ceil((u-(~~u>>>0))/4294967296)>>>0:0)],h[r+72>>2]=l[0],h[r+76>>2]=l[1],c[r+80>>2]=i%1e3*1e3,l=[n.ino>>>0,(u=n.ino,1<=+Math.abs(u)?0>>0:~~+Math.ceil((u-(~~u>>>0))/4294967296)>>>0:0)],h[r+88>>2]=l[0],h[r+92>>2]=l[1],0}var ht=void 0;function ct(){var e=h[+ht>>2];return ht+=4,e}var dt,E,k,bt,mt,pt,wt=(e,t)=>t+2097152>>>0<4194305-!!e?(e>>>0)+4294967296*t:NaN,_t=[0,31,60,91,121,152,182,213,244,274,305,335],vt=[0,31,59,90,120,151,181,212,243,273,304,334],yt=e=>{var t=$(e)+1,r=Rt(t);return r&&Z(e,V,r,t),r},gt={},qt=()=>{if(!dt){var e,t={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:L||"./this.program"};for(e in gt)void 0===gt[e]?delete t[e]:t[e]=gt[e];var r=[];for(e in t)r.push(e+"="+t[e]);dt=r}return dt},Et=e=>{var t=$(e)+1,r=re(t);return Z(e,V,r,t),r},kt=0,Mt=(e,t)=>(t=(1==t?re:Rt)(e.length),e.subarray||e.slice||(e=new Uint8Array(e)),V.set(e,t),t),At=[],te=e=>{E.delete(k.get(e)),k.set(e,null),At.push(e)},It=(t,r)=>{if(!E){E=new WeakMap;var n=k.length;if(E)for(var i=0;i<0+n;i++){var a=k.get(i);a&&E.set(a,i)}}if(n=E.get(t)||0)return n;if(At.length)n=At.pop();else{try{k.grow(1)}catch(e){if(e instanceof RangeError)throw"Unable to grow wasm table. Set ALLOW_TABLE_GROWTH.";throw e}n=k.length-1}try{k.set(n,t)}catch(e){if(!(e instanceof TypeError))throw e;if("function"==typeof WebAssembly.Function){for(var i=WebAssembly.Function,a={i:"i32",j:"i64",f:"f32",d:"f64",e:"externref",p:"i32"},o={parameters:[],results:"v"==r[0]?[]:[a[r[0]]]},s=1;s>7),s=0;s>7),r.push.apply(r,i),r.push(2,7,1,1,101,1,102,0,0,7,5,1,1,102,0,0),r=new WebAssembly.Module(new Uint8Array(r)),r=new WebAssembly.Instance(r,{e:{f:t}}).exports.f}k.set(n,r)}return E.set(t,n),n};function St(e,t,r,n){this.parent=e||=this,this.Ra=e.Ra,this.Va=null,this.id=Oe++,this.name=t,this.mode=r,this.Ga={},this.Ha={},this.rdev=n}Object.defineProperties(St.prototype,{read:{get:function(){return 365==(365&this.mode)},set:function(e){e?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146==(146&this.mode)},set:function(e){e?this.mode|=146:this.mode&=-147}}}),at(),b=Array(4096),Je(f,"/"),n("/tmp"),n("/home"),n("/home/web_user"),n("/dev"),We(259,{read:()=>0,write:(e,t,r,n)=>n}),Ce("/dev/null",259),Ee(1280,r),Ee(1536,t),Ce("/dev/tty",1280),Ce("/dev/tty1",1536),bt=new Uint8Array(1024),mt=0,ot("random",r=()=>(0===mt&&(mt=_e(bt).byteLength),bt[--mt])),ot("urandom",r),n("/dev/shm"),n("/dev/shm/tmp"),n("/proc"),pt=n("/proc/self"),n("/proc/self/fd"),Je({Ra(){var e=je(pt,"fd",16895,73);return e.Ga={lookup(e,t){var r=y(+t);return(e={parent:null,Ra:{tb:"fake"},Ga:{readlink:()=>r.path}}).parent=e}},e}},"/proc/self/fd");var xt,Ot={a:(e,t,r,n)=>{i(`Assertion failed: ${e?d(V,e):""}, at: `+[t?d(V,t):"unknown filename",r,n?d(V,n):"unknown function"])},h:function(e,t){try{return Ze(e=e?d(V,e):"",t),0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},H:function(e,t,r){try{if(t=q(e,t=t?d(V,t):""),-8&r)return-28;var n=p(t,{Sa:!0}).node;return n?(e="",4&r&&(e+="r"),2&r&&(e+="w"),1&r&&(e+="x"),e&&v(n,e)?-2:0):-44}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},i:function(e,t){try{return Ze(y(e).node,t),0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},g:function(e){try{var t=y(e).node,r="string"==typeof t?p(t,{Sa:!0}).node:t;if(r.Ga.Oa)return r.Ga.Oa(r,{timestamp:Date.now()}),0;throw new m(63)}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},b:function(e,t,r){ht=r;try{var n=y(e);switch(t){case 0:var i=ct();if(i<0)return-28;for(;xe[i];)i++;return Ue(n,i).fd;case 1:case 2:return 0;case 3:return n.flags;case 4:return i=ct(),n.flags|=i,0;case 5:return i=ct(),U[i+0>>1]=2,0;case 6:case 7:return 0;case 16:case 8:return-28;case 9:return h[Gt()>>2]=28,-1;default:return-28}}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},f:function(e,t){try{return ft(Xe,y(e).path,t)}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},n:function(e,t,r){t=wt(t,r);try{if(isNaN(t))return 61;var n=y(e);if(0==(2097155&n.flags))throw new m(28);return et(n.node,t),0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},C:function(e,t){try{if(0===t)return-28;var r=$("/")+1;return t>2]+4294967296*h[r+4>>2])+h[r+8>>2]/1e6,1e3*(c[(r+=16)>>2]+4294967296*h[r+4>>2])+h[r+8>>2]/1e6):n=Date.now(),e=n;var n,i,a=p(t,{Sa:!0}).node;return a.Ga.Oa(a,{timestamp:Math.max(e,i)}),0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},l:function(e,t,r){e=new Date(1e3*wt(e,t)),h[r>>2]=e.getSeconds(),h[r+4>>2]=e.getMinutes(),h[r+8>>2]=e.getHours(),h[r+12>>2]=e.getDate(),h[r+16>>2]=e.getMonth(),h[r+20>>2]=e.getFullYear()-1900,h[r+24>>2]=e.getDay(),t=e.getFullYear(),h[r+28>>2]=(0!=t%4||0==t%100&&0!=t%400?vt:_t)[e.getMonth()]+e.getDate()-1|0,h[r+36>>2]=-60*e.getTimezoneOffset(),t=new Date(e.getFullYear(),6,1).getTimezoneOffset();var n=new Date(e.getFullYear(),0,1).getTimezoneOffset();h[r+32>>2]=0|(t!=n&&e.getTimezoneOffset()==Math.min(n,t))},j:function(e,t,r,n,i,a,o,s){i=wt(i,a);try{if(isNaN(i))return 61;var u=y(n);if(0!=(2&t)&&0==(2&r)&&2!=(2097155&u.flags))throw new m(2);if(1==(2097155&u.flags))throw new m(2);if(!u.Ha.bb)throw new m(43);var l=u.Ha.bb(u,e,i,t,r),f=l.Db;return h[o>>2]=l.ub,c[s>>2]=f,0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},k:function(e,t,r,n,i,a,o){a=wt(a,o);try{if(isNaN(a))return 61;var s,u=y(i);if(2&r){if(32768!=(61440&u.node.mode))throw new m(43);2&n||(s=V.slice(e,e+t),u.Ha.cb&&u.Ha.cb(u,s,a,t,n))}}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return-e.Ka}},s:(e,t,r)=>{function n(e){return(e=e.toTimeString().match(/\(([A-Za-z ]+)\)$/))?e[1]:"GMT"}var i=(new Date).getFullYear(),a=new Date(i,0,1),o=new Date(i,6,1),i=a.getTimezoneOffset(),s=o.getTimezoneOffset();c[e>>2]=60*Math.max(i,s),h[t>>2]=Number(i!=s),e=n(a),t=n(o),e=yt(e),t=yt(t),s>2]=e,c[r+4>>2]=t):(c[r>>2]=t,c[r+4>>2]=e)},d:()=>Date.now(),t:()=>2147483648,c:()=>performance.now(),o:e=>{var t=V.length;if(2147483648<(e>>>=0))return!1;for(var r=1;r<=4;r*=2){var n=t*(1+.2/r),n=Math.min(n,e+100663296),i=Math;n=Math.max(e,n);e:{i=(i.min.call(i,2147483648,n+(65536-n%65536)%65536)-P.buffer.byteLength+65535)/65536;try{P.grow(i),J();var a=1;break e}catch(e){}a=void 0}if(a)return!0}return!1},A:(n,i)=>{var a=0;return qt().forEach((e,t)=>{var r=i+a;for(t=c[n+4*t>>2]=r,r=0;r>0]=e.charCodeAt(r);Q[t>>0]=0,a+=e.length+1}),0},B:(e,t)=>{var r=qt(),n=(c[e>>2]=r.length,0);return r.forEach(e=>n+=e.length+1),c[t>>2]=n,0},e:function(e){try{return tt(y(e)),0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return e.Ka}},p:function(e,t){try{var r=y(e);return Q[t>>0]=r.tty?2:_(r.mode)?3:40960==(61440&r.mode)?7:4,U[t+2>>1]=0,l=[0,(u=0,1<=+Math.abs(u)?0>>0:~~+Math.ceil((u-(~~u>>>0))/4294967296)>>>0:0)],h[t+8>>2]=l[0],h[t+12>>2]=l[1],l=[0,(u=0,1<=+Math.abs(u)?0>>0:~~+Math.ceil((u-(~~u>>>0))/4294967296)>>>0:0)],h[t+16>>2]=l[0],h[t+20>>2]=l[1],0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return e.Ka}},x:function(e,t,r,n){try{e:{var i=y(e);e=t;for(var a,o=t=0;o>2],u=c[e+4>>2],l=(e+=8,nt(i,Q,s,u,a));if(l<0){var f=-1;break e}if(t+=l,l>2]=f,0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return e.Ka}},m:function(e,t,r,n,i){t=wt(t,r);try{if(isNaN(t))return 61;var a=y(e);return rt(a,t,n),l=[a.position>>>0,(u=a.position,1<=+Math.abs(u)?0>>0:~~+Math.ceil((u-(~~u>>>0))/4294967296)>>>0:0)],h[i>>2]=l[0],h[i+4>>2]=l[1],a.hb&&0===t&&0===n&&(a.hb=null),0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return e.Ka}},D:function(e){try{var t=y(e);return t.Ha?.fsync?t.Ha.fsync(t):0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return e.Ka}},u:function(e,t,r,n){try{e:{var i=y(e);e=t;for(var a,o=t=0;o>2],u=c[e+4>>2],l=(e+=8,it(i,Q,s,u,a));if(l<0){var f=-1;break e}t+=l,void 0!==a&&(a+=l)}f=t}return c[n>>2]=f,0}catch(e){if(void 0===g||"ErrnoError"!==e.name)throw e;return e.Ka}}},M=function(){function t(e){return M=e.exports,P=M.I,J(),k=M.Aa,C.unshift(M.J),s--,B.monitorRunDependencies?.(s),0==s&&(null!==ie&&(clearInterval(ie),ie=null),ae&&(e=ae,ae=null,e())),M}var r,n,i,e={a:Ot};if(s++,B.monitorRunDependencies?.(s),B.instantiateWasm)try{return B.instantiateWasm(e,t)}catch(e){return o("Module.instantiateWasm callback failed with error: "+e),!1}return r=e,n=function(e){t(e.instance)},i=oe,a||"function"!=typeof WebAssembly.instantiateStreaming||se(i)||ue(i)||T||"function"!=typeof fetch?fe(i,r,n):fetch(i,{credentials:"same-origin"}).then(e=>WebAssembly.instantiateStreaming(e,r).then(n,function(e){return o("wasm streaming compile failed: "+e),o("falling back to ArrayBuffer instantiation"),fe(i,r,n)})),{}}(),Gt=(B._sqlite3_free=e=>(B._sqlite3_free=M.K)(e),B._sqlite3_value_text=e=>(B._sqlite3_value_text=M.L)(e),()=>(Gt=M.M)()),Rt=(B._sqlite3_prepare_v2=(e,t,r,n,i)=>(B._sqlite3_prepare_v2=M.N)(e,t,r,n,i),B._sqlite3_step=e=>(B._sqlite3_step=M.O)(e),B._sqlite3_finalize=e=>(B._sqlite3_finalize=M.P)(e),B._sqlite3_reset=e=>(B._sqlite3_reset=M.Q)(e),B._sqlite3_clear_bindings=e=>(B._sqlite3_clear_bindings=M.R)(e),B._sqlite3_value_blob=e=>(B._sqlite3_value_blob=M.S)(e),B._sqlite3_value_bytes=e=>(B._sqlite3_value_bytes=M.T)(e),B._sqlite3_value_double=e=>(B._sqlite3_value_double=M.U)(e),B._sqlite3_value_int=e=>(B._sqlite3_value_int=M.V)(e),B._sqlite3_value_type=e=>(B._sqlite3_value_type=M.W)(e),B._sqlite3_result_blob=(e,t,r,n)=>(B._sqlite3_result_blob=M.X)(e,t,r,n),B._sqlite3_result_double=(e,t)=>(B._sqlite3_result_double=M.Y)(e,t),B._sqlite3_result_error=(e,t,r)=>(B._sqlite3_result_error=M.Z)(e,t,r),B._sqlite3_result_int=(e,t)=>(B._sqlite3_result_int=M._)(e,t),B._sqlite3_result_int64=(e,t,r)=>(B._sqlite3_result_int64=M.$)(e,t,r),B._sqlite3_result_null=e=>(B._sqlite3_result_null=M.aa)(e),B._sqlite3_result_text=(e,t,r,n)=>(B._sqlite3_result_text=M.ba)(e,t,r,n),B._sqlite3_aggregate_context=(e,t)=>(B._sqlite3_aggregate_context=M.ca)(e,t),B._sqlite3_column_count=e=>(B._sqlite3_column_count=M.da)(e),B._sqlite3_data_count=e=>(B._sqlite3_data_count=M.ea)(e),B._sqlite3_column_blob=(e,t)=>(B._sqlite3_column_blob=M.fa)(e,t),B._sqlite3_column_bytes=(e,t)=>(B._sqlite3_column_bytes=M.ga)(e,t),B._sqlite3_column_double=(e,t)=>(B._sqlite3_column_double=M.ha)(e,t),B._sqlite3_column_text=(e,t)=>(B._sqlite3_column_text=M.ia)(e,t),B._sqlite3_column_type=(e,t)=>(B._sqlite3_column_type=M.ja)(e,t),B._sqlite3_column_name=(e,t)=>(B._sqlite3_column_name=M.ka)(e,t),B._sqlite3_bind_blob=(e,t,r,n,i)=>(B._sqlite3_bind_blob=M.la)(e,t,r,n,i),B._sqlite3_bind_double=(e,t,r)=>(B._sqlite3_bind_double=M.ma)(e,t,r),B._sqlite3_bind_int=(e,t,r)=>(B._sqlite3_bind_int=M.na)(e,t,r),B._sqlite3_bind_text=(e,t,r,n,i)=>(B._sqlite3_bind_text=M.oa)(e,t,r,n,i),B._sqlite3_bind_parameter_index=(e,t)=>(B._sqlite3_bind_parameter_index=M.pa)(e,t),B._sqlite3_sql=e=>(B._sqlite3_sql=M.qa)(e),B._sqlite3_normalized_sql=e=>(B._sqlite3_normalized_sql=M.ra)(e),B._sqlite3_errmsg=e=>(B._sqlite3_errmsg=M.sa)(e),B._sqlite3_exec=(e,t,r,n,i)=>(B._sqlite3_exec=M.ta)(e,t,r,n,i),B._sqlite3_changes=e=>(B._sqlite3_changes=M.ua)(e),B._sqlite3_close_v2=e=>(B._sqlite3_close_v2=M.va)(e),B._sqlite3_create_function_v2=(e,t,r,n,i,a,o,s,u)=>(B._sqlite3_create_function_v2=M.wa)(e,t,r,n,i,a,o,s,u),B._sqlite3_open=(e,t)=>(B._sqlite3_open=M.xa)(e,t),B._malloc=e=>(Rt=B._malloc=M.ya)(e)),Ht=B._free=e=>(Ht=B._free=M.za)(e),Lt=(B._RegisterExtensionFunctions=e=>(B._RegisterExtensionFunctions=M.Ba)(e),(e,t)=>(Lt=M.Ca)(e,t)),Nt=()=>(Nt=M.Da)(),jt=e=>(jt=M.Ea)(e),re=e=>(re=M.Fa)(e);function Tt(){function e(){if(!xt&&(xt=!0,B.calledRun=!0,!W)){if(B.noFSInit||ze||(ze=!0,at(),B.stdin=B.stdin,B.stdout=B.stdout,B.stderr=B.stderr,B.stdin?ot("stdin",B.stdin):Be("/dev/tty","/dev/stdin"),B.stdout?ot("stdout",null,B.stdout):Be("/dev/tty","/dev/stdout"),B.stderr?ot("stderr",null,B.stderr):Be("/dev/tty1","/dev/stderr"),ee("/dev/stdin",0),ee("/dev/stdout",1),ee("/dev/stderr",1)),Ge=!1,he(C),B.onRuntimeInitialized&&B.onRuntimeInitialized(),B.postRun)for("function"==typeof B.postRun&&(B.postRun=[B.postRun]);B.postRun.length;){var e=B.postRun.shift();ne.unshift(e)}he(ne)}}if(!(0{var t=!h||h.every(e=>"number"===e||"boolean"===e);return"string"!==f&&t&&!e?B["_"+l]:function(){var e=l,t=f,r=h,n=arguments,i={string:e=>{var t=0;return t=null!=e&&0!==e?Et(e):t},array:e=>{var t=re(e.length);return Q.set(e,t),t}},a=(e=B["_"+e],[]),o=0;if(n)for(var s=0;s `/libs/${file}`}); - } - - if (webidx.hasOwnProperty('db')) { - return webidx.query(params.query); - } else if (webidx.loadingPromise) { - await webidx.loadingPromise; - await new Promise((res, rej) => setTimeout(res, 10)); - return webidx.query(params.query); - } else { - webidx.loadingPromise = webidx.loadDB(params); - - webidx.loadingPromise.then(() => { - webidx.loadingPromise = null; - }); - - return webidx.loadingPromise - } -}; - -webidx.loadDB = function (params) { - return new Promise((res, rej) => { - var xhr = new XMLHttpRequest(); - - xhr.open('GET', params.dbfile); - xhr.timeout = params.timeout ?? 5000; - xhr.responseType = 'arraybuffer'; - - xhr.ontimeout = function() { - rej('Unable to load index, please refresh the page.'); - }; - - xhr.onload = function() { - webidx.initializeDB(this.response); - const results = webidx.query(params.query); - res(results); - }; - - xhr.send(); - }); -}; - -webidx.initializeDB = function (arrayBuffer) { - webidx.db = new webidx.sql.Database(new Uint8Array(arrayBuffer)); - - // - // prepare statements - // - webidx.wordQuery = webidx.db.prepare("SELECT `id` FROM `words` WHERE (`word`=:word)"); - webidx.idxQuery = webidx.db.prepare("SELECT `page_id` FROM `index` WHERE (`word`=:word)"); - webidx.pageQuery = webidx.db.prepare("SELECT `url`,`title` FROM `pages` WHERE (`id`=:id)"); -}; - -webidx.getWordID = function (word) { - webidx.wordQuery.bind([word]); - webidx.wordQuery.step(); - var word_id = webidx.wordQuery.get().shift(); - - webidx.wordQuery.reset(); - - return word_id; -}; - -webidx.getPagesHavingWord = function (word_id) { - var pages = []; - - webidx.idxQuery.bind([word_id]); - - while (webidx.idxQuery.step()) { - pages.push(webidx.idxQuery.get().shift()); - } - - webidx.idxQuery.reset(); - - return pages; -}; - -webidx.getPage = function (page_id) { - webidx.pageQuery.bind([page_id]); - - webidx.pageQuery.step(); - - var page = webidx.pageQuery.getAsObject(); - - webidx.pageQuery.reset(); - - return page; -}; - -webidx.query = function (query) { - // - // split the search term into words - // - var words = query.toLowerCase().split(" "); - - // - // this array maps page ID to rank - // - var pageRank = []; - - // - // iterate over each word - // - while (words.length > 0) { - var word = words.shift(); - - var invert = false; - if (0 == word.indexOf("-")) { - invert = true; - word = word.substring(1); - } - - var word_id = webidx.getWordID(word); - - // - // if the word isn't present, ignore it - // - if (word_id) { - var pages = webidx.getPagesHavingWord(word_id); - - pages.forEach(function (page_id) { - if (invert) { - if (pageRank[page_id]) { - pageRank[page_id] -= 65535; - - } else { - pageRank[page_id] = -65535; - - } - - } else { - if (pageRank[page_id]) { - pageRank[page_id]++; - - } else { - pageRank[page_id] = 1; - - } - } - }); - } - } - - // - // transform the results into a format that can be sorted - // - var sortedPages = []; - - pageRank.forEach(function (rank, page_id) { - if (rank > 0) { - sortedPages.push({rank: rank, page_id: page_id}); - } - }) - - // - // sort the results in descending rank order - // - sortedPages.sort(function(a, b) { - return b.rank - a.rank; - }); - - // - // this will be populated with the actual pages - // - var pages = []; - - // - // get page data for each result - // - sortedPages.forEach(function(result) { - pages.push(webidx.getPage(result.page_id)); - }); - - return pages; -}; - -webidx.regExpQuote = function (str) { - return str.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); -}; \ No newline at end of file diff --git a/themes/bookstack/static/search.php b/themes/bookstack/static/search.php new file mode 100644 index 0000000..e04d46b --- /dev/null +++ b/themes/bookstack/static/search.php @@ -0,0 +1,101 @@ + 'Search database not found.'], 500); +} + +try { + $db = new PDO('sqlite:' . $db_path); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +} catch (PDOException) { + send_json(['error' => 'Failed to connect to the database.'], 500); +} + +$words = preg_split('/\s+/', strtolower($query)); +$pageRank = []; + +$wordQuery = $db->prepare("SELECT `id` FROM `words` WHERE `word` = :word"); +$idxQuery = $db->prepare("SELECT `page_id` FROM `index` WHERE `word` = :word_id"); + +foreach ($words as $word) { + if (empty($word)) continue; + + $invert = str_starts_with($word, '-'); + $word = $invert ? substr($word, 1) : $word; + + if (empty($word)) continue; + + $wordQuery->execute([':word' => $word]); + $wordId = $wordQuery->fetchColumn(); + + if ($wordId) { + $idxQuery->execute([':word_id' => $wordId]); + $pages = $idxQuery->fetchAll(PDO::FETCH_COLUMN); + + foreach ($pages as $page_id) { + $pageRank[$page_id] = ($pageRank[$page_id] ?? 0) + ($invert ? -65535 : 1); + } + } +} + +$positiveRankPages = array_filter($pageRank, fn($rank) => $rank > 0); + +if (empty($positiveRankPages)) { + send_json([]); +} + +$sortedPages = []; +foreach ($positiveRankPages as $page_id => $rank) { + $sortedPages[] = ['rank' => $rank, 'page_id' => $page_id]; +} + +usort($sortedPages, fn($a, $b) => $b['rank'] <=> $a['rank']); + +$pageIds = array_column($sortedPages, 'page_id'); + +// Extract page details from the database +$placeholders = str_repeat('?,', count($pageIds) - 1) . '?'; +$pageQuery = $db->prepare("SELECT id, url, title FROM pages WHERE id IN ($placeholders)"); +$pageQuery->execute($pageIds); +$pagesData = $pageQuery->fetchAll(PDO::FETCH_ASSOC); + +// Create a map for efficient lookup +$pagesById = array_column($pagesData, null, 'id'); + +// Build final results in the correct rank order +$results = []; +foreach ($pageIds as $id) { + if (isset($pagesById[$id])) { + unset($pagesById[$id]['id']); + $results[] = $pagesById[$id]; + } +} + +send_json($results); +