diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 3069c43..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -source/javascripts/lib/* linguist-vendored diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..df3498a --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing + +## HTML + +- Semantic elements over generic `div`/`span` +- Minimal wrappers; avoid nesting without purpose +- Minimal classes; prefer element and attribute selectors +- ARIA attributes where native semantics fall short +- No inline styles; keep presentation in CSS +- Use `` for fenced code examples + +## CSS + +- Native CSS nesting; no preprocessors +- `light-dark()` for theming via `color-scheme` +- CSS custom properties for shared values +- Specific `transition` properties, not `all` +- Gate motion behind `prefers-reduced-motion` +- Mobile breakpoint at `768px` + +## JavaScript + +- Functional style; `const`, arrow functions, expressions +- Minimal intermediates; prefer composition +- No frameworks or external dependencies +- Handle async errors (e.g. `.catch` on clipboard) +- No DOM writes from user input without escaping + +## General + +- Avoid dependencies if possible; prefer `npx` for external tools +- Single file; CSS and JS are embedded in `index.html` +- Keep the page under 1000 lines total +- Test in both light and dark mode +- Verify mobile layout before committing +- Fix lint warnings in prose, not in the linter config + +## Server + +### Add a new page + +1. Place an `.html` file in the project root +2. Register it in `app.js`: `.get('/path', 'file.html')` + +### Serve custom content + +1. Add a text route in `app.js`: `.get('/path', 'body')` + +Omit the body to respond with `ok`: `.get('/health')` + +### Serve static files + +1. Place files in a directory (e.g. `assets/`) +2. Register in `app.js`: `.static('/assets')` + +### Notes + +- Only `GET` requests; everything else returns `405` +- File extensions must have a known MIME type (see `Server.DEFAULTS.mime`) +- Pages are loaded into memory at startup; restart to pick up changes +- Static directories must be within the project root +- `index.html` at root is auto-detected and served at `/` diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..dd1aa9b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,17 @@ +name: test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + - run: npm test diff --git a/.gitignore b/.gitignore index 56501dc..b44838c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ -*.DS_STORE +.DS_Store tmp +.claude/ diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 55dfc05..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,286 +0,0 @@ -# Changelog - -## Version 2.10.0 - -*April 13, 2021* - -* Add support for Ruby 3.0 (thanks @shaun-scale) -* Add requirement for Git on installing dependencies -* Bump nokogiri from 1.11.2 to 1.11.3 -* Bump middleman from 4.3.11 to [`d180ca3`](https://github.com/middleman/middleman/commit/d180ca337202873f2601310c74ba2b6b4cf063ec) - -## Version 2.9.2 - -*March 30, 2021* - -* __[Security]__ Bump kramdown from 2.3.0 to 2.3.1 -* Bump nokogiri from 1.11.1 to 1.11.2 - -## Version 2.9.1 - -*February 27, 2021* - -* Fix Slate language tabs not working if localStorage is disabled - -## Version 2.9.0 - -*January 19, 2021* - -* __Drop support for Ruby 2.3 and 2.4__ -* __[Security]__ Bump nokogiri from 1.10.10 to 1.11.1 -* __[Security]__ Bump redcarpet from 3.5.0 to 3.5.1 -* Specify slate is not supported on Ruby 3.x -* Bump rouge from 3.24.0 to 3.26.0 - -## Version 2.8.0 - -*October 27, 2020* - -* Remove last trailing newline when using the copy code button -* Rework docker image and make available at slatedocs/slate -* Improve Dockerfile layout to improve caching (thanks @micvbang) -* Bump rouge from 3.20 to 3.24 -* Bump nokogiri from 1.10.9 to 1.10.10 -* Bump middleman from 4.3.8 to 4.3.11 -* Bump lunr.js from 2.3.8 to 2.3.9 - -## Version 2.7.1 - -*August 13, 2020* - -* __[security]__ Bumped middleman from 4.3.7 to 4.3.8 - -_Note_: Slate uses redcarpet, not kramdown, for rendering markdown to HTML, and so was unaffected by the security vulnerability in middleman. -If you have changed slate to use kramdown, and with GFM, you may need to install the `kramdown-parser-gfm` gem. - -## Version 2.7.0 - -*June 21, 2020* - -* __[security]__ Bumped rack in Gemfile.lock from 2.2.2 to 2.2.3 -* Bumped bundled jQuery from 3.2.1 to 3.5.1 -* Bumped bundled lunr from 0.5.7 to 2.3.8 -* Bumped imagesloaded from 3.1.8 to 4.1.4 -* Bumped rouge from 3.17.0 to 3.20.0 -* Bumped redcarpet from 3.4.0 to 3.5.0 -* Fix color of highlighted code being unreadable when printing page -* Add clipboard icon for "Copy to Clipboard" functionality to code boxes (see note below) -* Fix handling of ToC selectors that contain punctutation (thanks @gruis) -* Fix language bar truncating languages that overflow screen width -* Strip HTML tags from ToC title before displaying it in title bar in JS (backup to stripping done in Ruby code) (thanks @atic) - -To enable the new clipboard icon, you need to add `code_clipboard: true` to the frontmatter of source/index.html.md. -See [this line](https://github.com/slatedocs/slate/blame/main/source/index.html.md#L19) for an example of usage. - -## Version 2.6.1 - -*May 30, 2020* - -* __[security]__ update child dependency activesupport in Gemfile.lock to 5.4.2.3 -* Update Middleman in Gemfile.lock to 4.3.7 -* Replace Travis-CI with GitHub actions for continuous integration -* Replace Spectrum with GitHub discussions - -## Version 2.6.0 - -*May 18, 2020* - -__Note__: 2.5.0 was "pulled" due to a breaking bug discovered after release. It is recommended to skip it, and move straight to 2.6.0. - -* Fix large whitespace gap in middle column for sections with codeblocks -* Fix highlighted code elements having a different background than rest of code block -* Change JSON keys to have a different font color than their values -* Disable asset hashing for woff and woff2 elements due to middleman bug breaking woff2 asset hashing in general -* Move Dockerfile to Debian from Alpine -* Converted repo to a [GitHub template](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-template-repository) -* Update sassc to 2.3.0 in Gemfile.lock - -## Version 2.5.0 - -*May 8, 2020* - -* __[security]__ update nokogiri to ~> 1.10.8 -* Update links in example docs to https://github.com/slatedocs/slate from https://github.com/lord/slate -* Update LICENSE to include full Apache 2.0 text -* Test slate against Ruby 2.5 and 2.6 on Travis-CI -* Update Vagrantfile to use Ubuntu 18.04 (thanks @bradthurber) -* Parse arguments and flags for deploy.sh on script start, instead of potentially after building source files -* Install nodejs inside Vagrantfile (thanks @fernandoaguilar) -* Add Dockerfile for running slate (thanks @redhatxl) -* update middleman-syntax and rouge to ~>3.2 -* update middleman to 4.3.6 - -## Version 2.4.0 - -*October 19, 2019* - -- Move repository from lord/slate to slatedocs/slate -- Fix documentation to point at new repo link, thanks to [Arun](https://github.com/slash-arun), [Gustavo Gawryszewski](https://github.com/gawry), and [Daniel Korbit](https://github.com/danielkorbit) -- Update `nokogiri` to 1.10.4 -- Update `ffi` in `Gemfile.lock` to fix security warnings, thanks to [Grey Baker](https://github.com/greysteil) and [jakemack](https://github.com/jakemack) -- Update `rack` to 2.0.7 in `Gemfile.lock` to fix security warnings, thanks to [Grey Baker](https://github.com/greysteil) and [jakemack](https://github.com/jakemack) -- Update middleman to `4.3` and relax constraints on middleman related gems, thanks to [jakemack](https://github.com/jakemack) -- Add sass gem, thanks to [jakemack](https://github.com/jakemack) -- Activate `asset_cache` in middleman to improve cacheability of static files, thanks to [Sam Gilman](https://github.com/thenengah) -- Update to using bundler 2 for `Gemfile.lock`, thanks to [jakemack](https://github.com/jakemack) - -## Version 2.3.1 - -*July 5, 2018* - -- Update `sprockets` in `Gemfile.lock` to fix security warnings - -## Version 2.3 - -*July 5, 2018* - -- Allows strikethrough in markdown by default. -- Upgrades jQuery to 3.2.1, thanks to [Tomi Takussaari](https://github.com/TomiTakussaari) -- Fixes invalid HTML in `layout.erb`, thanks to [Eric Scouten](https://github.com/scouten) for pointing out -- Hopefully fixes Vagrant memory issues, thanks to [Petter Blomberg](https://github.com/p-blomberg) for the suggestion -- Cleans HTML in headers before setting `document.title`, thanks to [Dan Levy](https://github.com/justsml) -- Allows trailing whitespace in markdown files, thanks to [Samuel Cousin](https://github.com/kuzyn) -- Fixes pushState/replaceState problems with scrolling not changing the document hash, thanks to [Andrey Fedorov](https://github.com/anfedorov) -- Removes some outdated examples, thanks [@al-tr](https://github.com/al-tr), [Jerome Dahdah](https://github.com/jdahdah), and [Ricardo Castro](https://github.com/mccricardo) -- Fixes `nav-padding` bug, thanks [Jerome Dahdah](https://github.com/jdahdah) -- Code style fixes thanks to [Sebastian Zaremba](https://github.com/vassyz) -- Nokogiri version bump thanks to [Grey Baker](https://github.com/greysteil) -- Fix to default `index.md` text thanks to [Nick Busey](https://github.com/NickBusey) - -Thanks to everyone who contributed to this release! - -## Version 2.2 - -*January 19, 2018* - -- Fixes bugs with some non-roman languages not generating unique headers -- Adds editorconfig, thanks to [Jay Thomas](https://github.com/jaythomas) -- Adds optional `NestingUniqueHeadCounter`, thanks to [Vladimir Morozov](https://github.com/greenhost87) -- Small fixes to typos and language, thx [Emir Ribić](https://github.com/ribice), [Gregor Martynus](https://github.com/gr2m), and [Martius](https://github.com/martiuslim)! -- Adds links to Spectrum chat for questions in README and ISSUE_TEMPLATE - -## Version 2.1 - -*October 30, 2017* - -- Right-to-left text stylesheet option, thanks to [Mohammad Hossein Rabiee](https://github.com/mhrabiee) -- Fix for HTML5 history state bug, thanks to [Zach Toolson](https://github.com/ztoolson) -- Small styling changes, typo fixes, small bug fixes from [Marian Friedmann](https://github.com/rnarian), [Ben Wilhelm](https://github.com/benwilhelm), [Fouad Matin](https://github.com/fouad), [Nicolas Bonduel](https://github.com/NicolasBonduel), [Christian Oliff](https://github.com/coliff) - -Thanks to everyone who submitted PRs for this version! - -## Version 2.0 - -*July 17, 2017* - -- All-new statically generated table of contents - - Should be much faster loading and scrolling for large pages - - Smaller Javascript file sizes - - Avoids the problem with the last link in the ToC not ever highlighting if the section was shorter than the page - - Fixes control-click not opening in a new page - - Automatically updates the HTML title as you scroll -- Updated design - - New default colors! - - New spacings and sizes! - - System-default typefaces, just like GitHub -- Added search input delay on large corpuses to reduce lag -- We even bumped the major version cause hey, why not? -- Various small bug fixes - -Thanks to everyone who helped debug or wrote code for this version! It was a serious community effort, and I couldn't have done it alone. - -## Version 1.5 - -*February 23, 2017* - -- Add [multiple tabs per programming language](https://github.com/lord/slate/wiki/Multiple-language-tabs-per-programming-language) feature -- Upgrade Middleman to add Ruby 1.4.0 compatibility -- Switch default code highlighting color scheme to better highlight JSON -- Various small typo and bug fixes - -## Version 1.4 - -*November 24, 2016* - -- Upgrade Middleman and Rouge gems, should hopefully solve a number of bugs -- Update some links in README -- Fix broken Vagrant startup script -- Fix some problems with deploy.sh help message -- Fix bug with language tabs not hiding properly if no error -- Add `!default` to SASS variables -- Fix bug with logo margin -- Bump tested Ruby versions in .travis.yml - -## Version 1.3.3 - -*June 11, 2016* - -Documentation and example changes. - -## Version 1.3.2 - -*February 3, 2016* - -A small bugfix for slightly incorrect background colors on code samples in some cases. - -## Version 1.3.1 - -*January 31, 2016* - -A small bugfix for incorrect whitespace in code blocks. - -## Version 1.3 - -*January 27, 2016* - -We've upgraded Middleman and a number of other dependencies, which should fix quite a few bugs. - -Instead of `rake build` and `rake deploy`, you should now run `bundle exec middleman build --clean` to build your server, and `./deploy.sh` to deploy it to Github Pages. - -## Version 1.2 - -*June 20, 2015* - -**Fixes:** - -- Remove crash on invalid languages -- Update Tocify to scroll to the highlighted header in the Table of Contents -- Fix variable leak and update search algorithms -- Update Python examples to be valid Python -- Update gems -- More misc. bugfixes of Javascript errors -- Add Dockerfile -- Remove unused gems -- Optimize images, fonts, and generated asset files -- Add chinese font support -- Remove RedCarpet header ID patch -- Update language tabs to not disturb existing query strings - -## Version 1.1 - -*July 27, 2014* - -**Fixes:** - -- Finally, a fix for the redcarpet upgrade bug - -## Version 1.0 - -*July 2, 2014* - -[View Issues](https://github.com/tripit/slate/issues?milestone=1&state=closed) - -**Features:** - -- Responsive designs for phones and tablets -- Started tagging versions - -**Fixes:** - -- Fixed 'unrecognized expression' error -- Fixed #undefined hash bug -- Fixed bug where the current language tab would be unselected -- Fixed bug where tocify wouldn't highlight the current section while searching -- Fixed bug where ids of header tags would have special characters that caused problems -- Updated layout so that pages with disabled search wouldn't load search.js -- Cleaned up Javascript diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index cc17fd9..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@lord.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a7ccb15..0000000 --- a/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM node:24-slim - -WORKDIR /srv/docs - -COPY package.json server.js ./ -COPY build/ build/ - -EXPOSE 4567 - -CMD ["node", "server.js"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/Procfile b/Procfile deleted file mode 100644 index 489b270..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: node server.js diff --git a/README.md b/README.md index 7cc0b9b..5b98264 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,58 @@ -# Bitpaper API Docs +# bitpaper-api-docs -Bitpaper's public REST API docs, built using [Slate][slate]. +[![test](https://github.com/TheProfs/bitpaper-api-docs/actions/workflows/test.yml/badge.svg)][ci] -## Install +Static docs for the [Bitpaper REST API][site]. -Ensure you have all [system dependencies installed][slate-getting-started], -clone this repository, cd into it and then: +## Setup ```bash -$ gem update --system -$ gem install bundler -$ bundle install +npm run dev ``` -## Usage +Local development runs at `http://localhost:3007`. +Override the port with `PORT=3017 npm run dev`. -## Run development server +## Authoring -```bash -bundle exec middleman server -# then visit http://192.168.10.1:4567 -``` - -## Edit documentation +It's just a single file: `index.html`. -Simply edit `src/index.html.md`. -More info on [Slate's][slate] homepage. +1. Read the [contribution guide][contrib] +2. Edit `index.html` accordingly +3. Push or merge to the default branch -Some sections are `includes` found in `src/includes/` +Changes deploy automatically to [developers.bitpaper.io][site]. -## Deploy to production +## Test ```bash -# Build docs -$ bundle exec middleman build - -# Push to main branch: -$ git add --all -$ git commit -am"Added POST/ user" -$ git push origin main +npm test ``` -Pushing to `main` branch automatically deploys on Heroku as -[bitpaper-api-docs][heroku-bitpaper-api-docs], -which is publicly accessible at https://developers.bitpaper.io +## Lint + +Deployments also trigger [lint.yml][lint], +essentially [vale.sh][vale] prose checks. + +## Structure + +``` +├── index.html # documentation page +├── app.js # entry point +├── assets/ # static files +├── lib/ +│ └── server/ # HTTP server module +└── test/ # smoke tests +``` -[slate]:https://github.com/slatedocs/slate -[slate-getting-started]:https://github.com/slatedocs/slate/wiki/Using-Slate-Natively -[heroku-bitpaper-api-docs]:https://dashboard.heroku.com/apps/bitpaper-api-docs +Initial structure borrowed from [Slate][gh-slate]. -## Authors +[MIT][mit] - Bitpaper LTD -Bitpaper LTD +[ci]: https://github.com/TheProfs/bitpaper-api-docs/actions/workflows/test.yml +[site]: https://developers.bitpaper.io +[contrib]: ./.github/CONTRIBUTING.md +[gh-slate]: https://github.com/slatedocs/slate +[lint]: ./.github/workflows/lint.yml +[vale]: https://vale.sh +[mit]: https://opensource.org/licenses/MIT diff --git a/app.js b/app.js new file mode 100644 index 0000000..5ef6d98 --- /dev/null +++ b/app.js @@ -0,0 +1,14 @@ +import { Server } from '#lib/server' + +const port = Number.parseInt(process.env.PORT ?? '', 10) + +if (!Number.isInteger(port)) + throw new TypeError('missing PORT') + +Server.create({ root: import.meta.dirname }) + .get('/health') + .get('/robots.txt', 'User-agent: *\nAllow: /\n') + .static('/assets') + .listen(port, function() { + console.log(`listening: ${this.address().port}`) + }) diff --git a/assets/og.webp b/assets/og.webp new file mode 100644 index 0000000..581fd90 Binary files /dev/null and b/assets/og.webp differ diff --git a/bitpaper-home.png b/bitpaper-home.png new file mode 100644 index 0000000..49dce1b Binary files /dev/null and b/bitpaper-home.png differ diff --git a/build/favicon-7c63c2e1.ico b/build/favicon-7c63c2e1.ico deleted file mode 100644 index 9c8cb52..0000000 Binary files a/build/favicon-7c63c2e1.ico and /dev/null differ diff --git a/build/fonts/slate-7b7da4fe.ttf b/build/fonts/slate-7b7da4fe.ttf deleted file mode 100644 index ace9a46..0000000 Binary files a/build/fonts/slate-7b7da4fe.ttf and /dev/null differ diff --git a/build/fonts/slate-cfc9d06b.eot b/build/fonts/slate-cfc9d06b.eot deleted file mode 100644 index 13c4839..0000000 Binary files a/build/fonts/slate-cfc9d06b.eot and /dev/null differ diff --git a/build/fonts/slate-e55b8307.svg b/build/fonts/slate-e55b8307.svg deleted file mode 100644 index 5f34982..0000000 --- a/build/fonts/slate-e55b8307.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - -Generated by IcoMoon - - - - - - - - - - diff --git a/build/fonts/slate.woff b/build/fonts/slate.woff deleted file mode 100644 index 1e72e0e..0000000 Binary files a/build/fonts/slate.woff and /dev/null differ diff --git a/build/fonts/slate.woff2 b/build/fonts/slate.woff2 deleted file mode 100644 index 7c585a7..0000000 Binary files a/build/fonts/slate.woff2 and /dev/null differ diff --git a/build/images/logo-22aa55c6.png b/build/images/logo-22aa55c6.png deleted file mode 100644 index 93c209d..0000000 Binary files a/build/images/logo-22aa55c6.png and /dev/null differ diff --git a/build/images/logo-bbe8927d.svg b/build/images/logo-bbe8927d.svg deleted file mode 100644 index df4c6f5..0000000 --- a/build/images/logo-bbe8927d.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - Layer 1 - image/svg+xmlGroup 26 - image/svg+xmlGroup 26 - - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/build/images/navbar-cad8cdcb.png b/build/images/navbar-cad8cdcb.png deleted file mode 100644 index df38e90..0000000 Binary files a/build/images/navbar-cad8cdcb.png and /dev/null differ diff --git a/build/index.html b/build/index.html deleted file mode 100644 index 3fca540..0000000 --- a/build/index.html +++ /dev/null @@ -1,722 +0,0 @@ - - - - - - - - - API Reference - - - - - - - - - - - - - - NAV - - - -
- - - - - -
-
-
-
-

Introduction

-

Welcome to the Bitpaper API!

- -

The following guide explain ways how you can create and manage papers -programmatically, using a REST API, and how to integrate -Bitpaper in your website.

- -

You can view code examples in the dark area to the right.

- -

You can get full access to our REST API by subscribing to the 'Enterprise/API' -plan on our Pricing page

-

Usage

-

The Bitpaper API is organized around REST.

- -

It uses predictable resource-oriented URLs, accepts -JSON-encoded request bodies, returns -JSON-encoded responses and uses -standard HTTP response codes, -authentication and verbs.

- -
    -
  • All requests must be made via HTTPS.
  • -
  • Requests which require a request body use JSON as the request body format, -therefore they must include a Content-Type: application/json header.
  • -
- -

API versioning follows the Semantic Versioning -guidelines.

-

Rate Limiting

-

The Bitpaper API employs a rate limiter -to guard against bursts of incoming traffic in order to maximise its stability. -Users who send too many requests in quick succession may see error responses -that show up as HTTP status code 429.

- -

All requests to the Bitpaper API are limited to 600 requests per hour.

- -

Contact us if you need to increase -this limit.

-

Authentication

# An example of an authorized API call
-curl "<api-endpoint-here>" \
-  -H "Authorization: Bearer <my-secret-api-token>"
-
-
-

Make sure to replace <my-secret-api-token> with your actual API token.

-
- -

Bitpaper uses API tokens to allow access to the API.

- -

You can view your API token in your Bitpaper -account.

- -

Bitpaper requires your API token included in all API requests in a header that -looks like this:

- -

Authorization: Bearer <my-secret-api-token>

- - -

Test Mode

-

The Bitpaper API allows simulating requests using your test API token so you -can test the API without incurring paper creation charges.

- -

Bitpaper provides you with a set of 2 API tokens, -found here:

- -
    -
  • Production API token: Creates functioning papers and charges them to your -account. You should use this in your production/live app.
  • -
  • Test API token: Creates non-functioning papers which are not charged. -You should use this token to test API requests.
  • -
- - -

Papers

-

Papers are the primary resource in Bitpaper.

- -

A Paper is a collaborative whiteboard instance accessible via a unique -and permanent URL.

- -

Creating a paper programatically will return URLs that users can visit to -join and collaborate on a whiteboard instance.

- -

A paper (i.e URL) can have up to a maximum of 100 individual pages. -Pages can be created and switched manually using the page toolbar on the -top-right of a paper.

-

API Papers

-

By default API created papers have the full set of whiteboard tools and -functionality including audio/video calls and screensharing.

- -

You can control whether users have access to the paid audio/video calls and -screensharing features using the Calls toggle in your -Call Settings.

- -

API created Bitpapers do not display any references to the Bitpaper brand such -as our logo or brand name.

- -

To mask the URL you will need to follow the -Whitelabelling guide below.

-

Create a Paper

curl "https://api.bitpaper.io/public/api/v1/paper" \
--X POST \
--H "Content-Type: application/json" \
--H "Authorization: Bearer <my-secret-api-token>" \
---data '{"name":"Maths"}'
-
-
-

Returns JSON structured like this:

-
-
{
-  "id_saved_paper": "ddc95912-2a81-a25e-b589-8de1d472f6e8",
-  "name": "Maths",
-  "created_at": "2021-01-01T01:00:00.000Z",
-  "is_test_paper": false,
-  "urls": {
-    "admin": "https://bitpaper.io/go/Maths/xdXfoI?access-token=eyJhbGciO",
-    "guest": "https://bitpaper.io/go/Maths/xdXfoI?access-token=iJIUzI9.e"
-  }
-}
-
-

Creates a paper and returns the paper information and URLs which can be used -to access the paper.

- -

Visiting any of the URLs provided in the response would take you directly to -the created paper.

-

HTTP Request

-

POST https://api.bitpaper.io/public/api/v1/paper

-

Body Parameters

- - - - - - - - - - - - -
ParameterTypeDescription
nameString: (URL-safe, between 4-64 chars)A name for the paper. Does not have to be unique.
-

Response

-

Responds with HTTP 201 if successful.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterDescription
id_saved_paperString: (36 chars)
nameString: (URL-safe, between 4-64 chars)
is_test_paperBoolean: true if paper was created using test API token
created_atString: Timestamp of paper creation
urlsObject: Contains the URLs for accessing the papers
urls.adminBoolean: URL which accesses the paper as an administrator/owner
urls.guestBoolean: URL which accesses the paper as a guest
- - -

Access a Paper

-

Created papers contain 2 URLs that can be used to join them, one for -administrators and another one for guests.

- -
    -
  • Users who join the administrator URL will have full whiteboard -privileges.
  • -
  • Users who join the guest URL will have restricted whiteboard privileges.
  • -
- -

A paper can have more than 1 administrator or guest. If you redirect -2 users using either the url.admin or url.guest both would have the same -whiteboard privileges.

- - -

Give names to your users

-

Papers include features which display the name of the user. -For example the chat function displays the name of each user posting a message.

- -

To give a name to the user simply attach a user_name=<name> URL query -parameter to the URL like so:

- -

https://bitpaper.io/go/Hello%20World/xdXfoI?access-token=foo&user_name=John%20Doe

-

Delete a Paper

curl "https://api.bitpaper.io/public/api/v1/paper/ddc95912-2a81-a25e-b589-8de1d472f6e8" \
-  -X DELETE \
-  -H "Authorization: Bearer <my-secret-api-token>"
-
-
-

Responds with HTTP 204 if successful or an HTTP error otherwise.

-
- -

Deletes a specific paper. The content of the paper is permanently deleted and -it's URL is made permanently inaccessible.

-

HTTP Request

-

DELETE https://api.bitpaper.io/public/api/v1/paper/<id_saved_paper>

-

URL Parameters

- - - - - - - - - - -
ParameterDescription
id_saved_paperString: The id_saved_paper of the paper to delete
-

Response

-

Responds with HTTP 204 if successful.

-

Whitelabelling

-

Bitpaper allows embedding the whiteboard in your website, under your own domain -and without any reference to the Bitpaper brand.

- -

This allows a seamless UX for your users without any hints of using an external, -third-party whiteboard.

-

Use your own domain

<!--
-  Embed this in your site on URL:
-  https://whiteboard.yourdomain.com/Maths/xdXfoI
- -->
-<iframe
-  src="https://bitpaper.io/go/Maths/xdXfoI?access-token=eyJhbGciO&user_name=John%20Doe"
-  allow="camera; microphone; display-capture; clipboard-read; clipboard-write;"
-  style="
-    position: fixed;
-    top: 0px;
-    bottom: 0px;
-    right: 0px;
-    width: 100%;
-    border: none;
-    margin: 0;
-    padding: 0;
-    overflow: hidden;
-    z-index: 999999;
-    height: 100%;
-  ">
-</iframe>
-
-

Bitpaper can be embedded into your website using an <iframe>. -This allows you to display papers to your users in a webpage that is rendered -on your own domain.

- -

The src of the iframe can be set to either of the URLs returned in your -created paper.

-

Status Codes

-

The Bitpaper API uses the following HTTP status codes:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CodeMeaning
200OK - Your request has succeeded and has response data.
201Created - Your request has succeeded and has led to the creation of the resource.
400No Content - Your request has succeeded and has no response data.
400Bad Request - Your request is not formatted correctly (i.e has validation errors).
401Unauthorised - The authorisation header is not formatted correctly or the API token is invalid or revoked.
403Forbidden - Your subscription is not active or doesn't have sufficient privileges to perform the request.
404Not Found - The specified entity could not be found.
405Method Not Allowed - You tried to access an entity with an invalid method.
406Not Acceptable - You requested a format that isn't JSON.
410Gone - The entity requested has been removed from our servers.
429Too Many Requests - You're issuing too many requests. Slow down!
500Internal Server Error - We had a problem with our server. Try again later.
503Service Unavailable - We're temporarily offline for maintenance. Check Bitpaper Status for more info or try again later.
-

Changelog

v1.0.0

-

August 20 2021

- -
    -
  • Initial release
  • -
- -
-
-
-
- - diff --git a/build/javascripts/all-a9fde460.js b/build/javascripts/all-a9fde460.js deleted file mode 100644 index deb96ef..0000000 --- a/build/javascripts/all-a9fde460.js +++ /dev/null @@ -1,120 +0,0 @@ -function copyToClipboard(e){const t=document.createElement("textarea");t.value=e.textContent.replace(/\n$/,""),document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}function setupCodeCopy(){$("pre.highlight").prepend('
Copy to Clipboard
'),$(".copy-clipboard").on("click",function(){copyToClipboard(this.parentNode.children[1])})}function adjustLanguageSelectorWidth(){const e=$(".dark-box > .lang-selector");e.width(e.parent().width())}!function(){if("ontouchstart"in window){var e,t,n,r,i,o,s={};e=function(e,t){return Math.abs(e[0]-t[0])>5||Math.abs(e[1]-t[1])>5},t=function(e){this.startXY=[e.touches[0].clientX,e.touches[0].clientY],this.threshold=!1},n=function(t){if(this.threshold)return!1;this.threshold=e(this.startXY,[t.touches[0].clientX,t.touches[0].clientY])},r=function(t){if(!this.threshold&&!e(this.startXY,[t.changedTouches[0].clientX,t.changedTouches[0].clientY])){var n=t.changedTouches[0],r=document.createEvent("MouseEvents");r.initMouseEvent("click",!0,!0,window,0,n.screenX,n.screenY,n.clientX,n.clientY,!1,!1,!1,!1,0,null),r.simulated=!0,t.target.dispatchEvent(r)}},i=function(e){var t=Date.now(),n=t-s.time,r=e.clientX,i=e.clientY,a=[Math.abs(s.x-r),Math.abs(s.y-i)],u=o(e.target,"A")||e.target,l=u.nodeName,c="A"===l,f=window.navigator.standalone&&c&&e.target.getAttribute("href");if(s.time=t,s.x=r,s.y=i,(!e.simulated&&(n<500||n<1500&&a[0]<50&&a[1]<50)||f)&&(e.preventDefault(),e.stopPropagation(),!f))return!1;f&&(window.location=u.getAttribute("href")),u&&u.classList&&(u.classList.add("energize-focus"),window.setTimeout(function(){u.classList.remove("energize-focus")},150))},o=function(e,t){for(var n=e;n!==document.body;){if(!n||n.nodeName===t)return n;n=n.parentNode}return null},document.addEventListener("touchstart",t,!1),document.addEventListener("touchmove",n,!1),document.addEventListener("touchend",r,!1),document.addEventListener("click",i,!0)}}(),/*! - * jQuery JavaScript Library v3.5.1 - * https://jquery.com/ - * - * Includes Sizzle.js - * https://sizzlejs.com/ - * - * Copyright JS Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2020-05-04T22:49Z - */ -function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";function n(e,t,n){n=n||we;var r,i,o=n.createElement("script");if(o.text=e,t)for(r in Te)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function r(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?pe[he.call(e)]||"object":typeof e}function i(e){var t=!!e&&"length"in e&&e.length,n=r(e);return!xe(e)&&!be(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}function o(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}function s(e,t,n){return xe(t)?Ce.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?Ce.grep(e,function(e){return e===t!==n}):"string"!=typeof t?Ce.grep(e,function(e){return de.call(t,e)>-1!==n}):Ce.filter(t,e,n)}function a(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}function u(e){var t={};return Ce.each(e.match(Pe)||[],function(e,n){t[n]=!0}),t}function l(e){return e}function c(e){throw e}function f(e,t,n,r){var i;try{e&&xe(i=e.promise)?i.call(e).done(t).fail(n):e&&xe(i=e.then)?i.call(e,t,n):t.apply(undefined,[e].slice(r))}catch(e){n.apply(undefined,[e])}}function d(){we.removeEventListener("DOMContentLoaded",d),e.removeEventListener("load",d),Ce.ready()}function p(e,t){return t.toUpperCase()}function h(e){return e.replace(Fe,"ms-").replace($e,p)}function g(){this.expando=Ce.expando+g.uid++}function m(e){return"true"===e||"false"!==e&&("null"===e?null:e===+e+""?+e:Be.test(e)?JSON.parse(e):e)}function y(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(We,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n=m(n)}catch(e){}Me.set(e,t,n)}else n=undefined;return n}function v(e,t,n,r){var i,o,s=20,a=r?function(){return r.cur()}:function(){return Ce.css(e,t,"")},u=a(),l=n&&n[3]||(Ce.cssNumber[t]?"":"px"),c=e.nodeType&&(Ce.cssNumber[t]||"px"!==l&&+u)&&Ve.exec(Ce.css(e,t));if(c&&c[3]!==l){for(u/=2,l=l||c[3],c=+u||1;s--;)Ce.style(e,t,c+l),(1-o)*(1-(o=a()/u||.5))<=0&&(s=0),c/=o;c*=2,Ce.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}function x(e){var t,n=e.ownerDocument,r=e.nodeName,i=Ke[r];return i||(t=n.body.appendChild(n.createElement(r)),i=Ce.css(t,"display"),t.parentNode.removeChild(t),"none"===i&&(i="block"),Ke[r]=i,i)}function b(e,t){for(var n,r,i=[],o=0,s=e.length;o-1)o&&o.push(s);else if(c=Ye(s),a=w(d.appendChild(s),"script"),c&&T(a),n)for(f=0;s=a[f++];)tt.test(s.type||"")&&n.push(s);return d}function C(){return!0}function k(){return!1}function S(e,t){return e===L()==("focus"===t)}function L(){try{return we.activeElement}catch(e){}}function j(e,t,n,r,i,o){var s,a;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=undefined);for(a in t)j(e,a,n,r,t[a],o);return e}if(null==r&&null==i?(i=n,r=n=undefined):null==i&&("string"==typeof n?(i=r,r=undefined):(i=r,r=n,n=undefined)),!1===i)i=k;else if(!i)return e;return 1===o&&(s=i,i=function(e){return Ce().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=Ce.guid++)),e.each(function(){Ce.event.add(this,t,i,r,n)})}function N(e,t,n){if(!n)return void(He.get(e,t)===undefined&&Ce.event.add(e,t,C));He.set(e,t,!1),Ce.event.add(e,t,{namespace:!1,handler:function(e){var r,i,o=He.get(this,t);if(1&e.isTrigger&&this[t]){if(o.length)(Ce.event.special[t]||{}).delegateType&&e.stopPropagation();else if(o=le.call(arguments),He.set(this,t,o),r=n(this,t),this[t](),i=He.get(this,t),o!==i||r?He.set(this,t,!1):i={},o!==i)return e.stopImmediatePropagation(),e.preventDefault(),i.value}else o.length&&(He.set(this,t,{value:Ce.event.trigger(Ce.extend(o[0],Ce.Event.prototype),o.slice(1),this)}),e.stopImmediatePropagation())}})}function A(e,t){return o(e,"table")&&o(11!==t.nodeType?t:t.firstChild,"tr")?Ce(e).children("tbody")[0]||e:e}function D(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function I(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function O(e,t){var n,r,i,o,s,a,u;if(1===t.nodeType){if(He.hasData(e)&&(o=He.get(e),u=o.events)){He.remove(t,"handle events");for(i in u)for(n=0,r=u[i].length;n1&&"string"==typeof h&&!ve.checkClone&&ut.test(h))return e.each(function(n){var o=e.eq(n);g&&(t[0]=h.call(this,n,o.html())),Q(o,t,r,i)});if(d&&(o=E(t,e[0].ownerDocument,!1,e,i),s=o.firstChild,1===o.childNodes.length&&(o=s),s||i)){for(a=Ce.map(w(o,"script"),D),u=a.length;f=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-a-.5))||0),u}function B(e,t,n){var r=ft(e),i=!ve.boxSizingReliable()||n,s=i&&"border-box"===Ce.css(e,"boxSizing",!1,r),a=s,u=q(e,t,r),l="offset"+t[0].toUpperCase()+t.slice(1);if(ct.test(u)){if(!n)return u;u="auto"}return(!ve.boxSizingReliable()&&s||!ve.reliableTrDimensions()&&o(e,"tr")||"auto"===u||!parseFloat(u)&&"inline"===Ce.css(e,"display",!1,r))&&e.getClientRects().length&&(s="border-box"===Ce.css(e,"boxSizing",!1,r),(a=l in e)&&(u=e[l])),(u=parseFloat(u)||0)+M(e,t,n||(s?"border":"content"),a,r,u)+"px"}function W(e,t,n,r,i){return new W.prototype.init(e,t,n,r,i)}function z(){Tt&&(!1===we.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(z):e.setTimeout(z,Ce.fx.interval),Ce.fx.tick())}function V(){return e.setTimeout(function(){wt=undefined}),wt=Date.now()}function U(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)n=Ue[r],i["margin"+n]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function X(e,t,n){for(var r,i=(J.tweeners[t]||[]).concat(J.tweeners["*"]),o=0,s=i.length;o=0&&nE.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[_]=!0,e}function i(e){var t=O.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=n.length;r--;)E.attrHandle[n[r]]=t}function s(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function a(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function u(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function l(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&Se(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function c(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),s=o.length;s--;)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}function f(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function d(){}function p(e){for(var t=0,n=e.length,r="";t1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function m(e,n,r){for(var i=0,o=n.length;i-1&&(r[l]=!(s[l]=f))}}else x=y(x===s?x.splice(h,x.length):x),o?o(null,s,x,u):Z.apply(s,x)})}function x(e){for(var t,n,r,i=e.length,o=E.relative[e[0].type],s=o||E.relative[" "],a=o?1:0,u=h(function(e){return e===t},s,!0),l=h(function(e){return te(t,e)>-1},s,!0),c=[function(e,n,r){var i=!o&&(r||n!==N)||((t=n).nodeType?u(e,n,r):l(e,n,r));return t=null,i}];a1&&g(c),a>1&&p(e.slice(0,a-1).concat({value:" "===e[a-2].type?"*":""})).replace(ue,"$1"),n,a0,o=e.length>0,s=function(r,s,a,u,l){var c,f,d,p=0,h="0",g=r&&[],m=[],v=N,x=r||o&&E.find.TAG("*",l),b=M+=null==v?1:Math.random()||.1,w=x.length;for(l&&(N=s==O||s||l);h!==w&&null!=(c=x[h]);h++){if(o&&c){for(f=0,s||c.ownerDocument==O||(I(c),a=!Q);d=e[f++];)if(d(c,s||O,a)){u.push(c);break}l&&(M=b)}i&&((c=!d&&c)&&p--,r&&g.push(c))}if(p+=h,i&&h!==p){for(f=0;d=n[f++];)d(g,m,s,a);if(r){if(p>0)for(;h--;)g[h]||m[h]||(m[h]=J.call(u));m=y(m)}Z.apply(u,m),l&&!r&&m.length>0&&p+n.length>1&&t.uniqueSort(u)}return l&&(M=b,N=v),g};return i?r(s):s}var w,T,E,C,k,S,L,j,N,A,D,I,O,P,Q,R,q,F,$,_="sizzle"+1*new Date,H=e.document,M=0,B=0,W=n(),z=n(),V=n(),U=n(),X=function(e,t){return e===t&&(D=!0),0},Y={}.hasOwnProperty,G=[],J=G.pop,K=G.push,Z=G.push,ee=G.slice,te=function(e,t){for(var n=0,r=e.length;n+~]|"+re+")"+re+"*"),fe=new RegExp(re+"|>"),de=new RegExp(se),pe=new RegExp("^"+ie+"$"),he={ID:new RegExp("^#("+ie+")"),CLASS:new RegExp("^\\.("+ie+")"),TAG:new RegExp("^("+ie+"|[*])"),ATTR:new RegExp("^"+oe),PSEUDO:new RegExp("^"+se),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+re+"*(even|odd|(([+-]|)(\\d*)n|)"+re+"*(?:([+-]|)"+re+"*(\\d+)|))"+re+"*\\)|)","i"),bool:new RegExp("^(?:"+ne+")$","i"),needsContext:new RegExp("^"+re+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+re+"*((?:-\\d)?\\d*)"+re+"*\\)|)(?=[^-]|$)","i")},ge=/HTML$/i,me=/^(?:input|select|textarea|button)$/i,ye=/^h\d$/i,ve=/^[^{]+\{\s*\[native \w/,xe=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,be=/[+~]/,we=new RegExp("\\\\[\\da-fA-F]{1,6}"+re+"?|\\\\([^\\r\\n\\f])","g"),Te=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},Ee=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,Ce=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},ke=function(){I()},Se=h(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{Z.apply(G=ee.call(H.childNodes),H.childNodes),G[H.childNodes.length].nodeType}catch(e){Z={apply:G.length?function(e,t){K.apply(e,ee.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}T=t.support={},k=t.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!ge.test(t||n&&n.nodeName||"HTML")},I=t.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:H;return r!=O&&9===r.nodeType&&r.documentElement?(O=r,P=O.documentElement,Q=!k(O),H!=O&&(n=O.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",ke,!1):n.attachEvent&&n.attachEvent("onunload",ke)),T.scope=i(function(e){return P.appendChild(e).appendChild(O.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),T.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),T.getElementsByTagName=i(function(e){return e.appendChild(O.createComment("")),!e.getElementsByTagName("*").length}),T.getElementsByClassName=ve.test(O.getElementsByClassName),T.getById=i(function(e){return P.appendChild(e).id=_,!O.getElementsByName||!O.getElementsByName(_).length}),T.getById?(E.filter.ID=function(e){var t=e.replace(we,Te);return function(e){return e.getAttribute("id")===t}},E.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&Q){var n=t.getElementById(e);return n?[n]:[]}}):(E.filter.ID=function(e){var t=e.replace(we,Te);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},E.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&Q){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];for(i=t.getElementsByName(e),r=0;o=i[r++];)if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),E.find.TAG=T.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):T.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},E.find.CLASS=T.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&Q)return t.getElementsByClassName(e)},q=[],R=[],(T.qsa=ve.test(O.querySelectorAll))&&(i(function(e){var t;P.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&R.push("[*^$]="+re+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||R.push("\\["+re+"*(?:value|"+ne+")"),e.querySelectorAll("[id~="+_+"-]").length||R.push("~="),t=O.createElement("input"),t.setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||R.push("\\["+re+"*name"+re+"*="+re+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||R.push(":checked"),e.querySelectorAll("a#"+_+"+*").length||R.push(".#.+[+~]"),e.querySelectorAll("\\\f"),R.push("[\\r\\n\\f]")}),i(function(e){e.innerHTML="";var t=O.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&R.push("name"+re+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&R.push(":enabled",":disabled"),P.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&R.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),R.push(",.*:")})),(T.matchesSelector=ve.test(F=P.matches||P.webkitMatchesSelector||P.mozMatchesSelector||P.oMatchesSelector||P.msMatchesSelector))&&i(function(e){T.disconnectedMatch=F.call(e,"*"),F.call(e,"[s!='']:x"),q.push("!=",se)}),R=R.length&&new RegExp(R.join("|")),q=q.length&&new RegExp(q.join("|")),t=ve.test(P.compareDocumentPosition),$=t||ve.test(P.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},X=t?function(e,t){if(e===t)return D=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&n||!T.sortDetached&&t.compareDocumentPosition(e)===n?e==O||e.ownerDocument==H&&$(H,e)?-1:t==O||t.ownerDocument==H&&$(H,t)?1:A?te(A,e)-te(A,t):0:4&n?-1:1)}:function(e,t){if(e===t)return D=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],u=[t];if(!i||!o)return e==O?-1:t==O?1:i?-1:o?1:A?te(A,e)-te(A,t):0;if(i===o)return s(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)u.unshift(n);for(;a[r]===u[r];)r++;return r?s(a[r],u[r]):a[r]==H?-1:u[r]==H?1:0},O):O},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if(I(e),T.matchesSelector&&Q&&!U[n+" "]&&(!q||!q.test(n))&&(!R||!R.test(n)))try{var r=F.call(e,n);if(r||T.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){U(n,!0)}return t(n,O,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!=O&&I(e),$(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!=O&&I(e);var n=E.attrHandle[t.toLowerCase()],r=n&&Y.call(E.attrHandle,t.toLowerCase())?n(e,t,!Q):undefined;return r!==undefined?r:T.attributes||!Q?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.escape=function(e){return(e+"").replace(Ee,Ce)},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(D=!T.detectDuplicates,A=!T.sortStable&&e.slice(0),e.sort(X),D){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return A=null,e},C=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=C(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=C(t);return n},E=t.selectors={cacheLength:50,createPseudo:r,match:he,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(we,Te),e[3]=(e[3]||e[4]||e[5]||"").replace(we,Te),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return he.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&de.test(n)&&(t=S(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(we,Te).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=W[e+" "];return t||(t=new RegExp("(^|"+re+")"+e+"("+re+"|$)"))&&W(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:!n||(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o.replace(ae," ")+" ").indexOf(r)>-1:"|="===n&&(o===r||o.slice(0,r.length+1)===r+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,d,p,h,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,y=a&&t.nodeName.toLowerCase(),v=!u&&!a,x=!1;if(m){if(o){for(;g;){for(d=t;d=d[g];)if(a?d.nodeName.toLowerCase()===y:1===d.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[s?m.firstChild:m.lastChild],s&&v){for(d=m,f=d[_]||(d[_]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),l=c[e]||[],p=l[0]===M&&l[1],x=p&&l[2],d=p&&m.childNodes[p];d=++p&&d&&d[g]||(x=p=0)||h.pop();)if(1===d.nodeType&&++x&&d===t){c[e]=[M,p,x];break}}else if(v&&(d=t,f=d[_]||(d[_]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),l=c[e]||[],p=l[0]===M&&l[1],x=p),!1===x)for(;(d=++p&&d&&d[g]||(x=p=0)||h.pop())&&((a?d.nodeName.toLowerCase()!==y:1!==d.nodeType)||!++x||(v&&(f=d[_]||(d[_]={}),c=f[d.uniqueID]||(f[d.uniqueID]={}),c[e]=[M,x]),d!==t)););return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,n){var i,o=E.pseudos[e]||E.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[_]?o(n):o.length>1?(i=[e,e,"",n],E.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),s=i.length;s--;)r=te(e,i[s]),e[r]=!(t[r]=i[s])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=L(e.replace(ue,"$1"));return i[_]?r(function(e,t,n,r){for(var o,s=i(e,null,r,[]),a=e.length;a--;)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),t[0]=null,!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return e=e.replace(we,Te),function(t){return(t.textContent||C(t)).indexOf(e)>-1}}),lang:r(function(e){return pe.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(we,Te).toLowerCase(),function(t){var n;do{if(n=Q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===P},focus:function(e){return e===O.activeElement&&(!O.hasFocus||O.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:l(!1),disabled:l(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!E.pseudos.empty(e)},header:function(e){return ye.test(e.nodeName)},input:function(e){return me.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:c(function(){return[0]}),last:c(function(e,t){return[t-1]}),eq:c(function(e,t,n){return[n<0?n+t:n]}),even:c(function(e,t){for(var n=0;nt?t:n;--r>=0;)e.push(r);return e}),gt:c(function(e,t,n){for(var r=n<0?n+t:n;++r2&&"ID"===(s=o[0]).type&&9===t.nodeType&&Q&&E.relative[o[1].type]){if(!(t=(E.find.ID(s.matches[0].replace(we,Te),t)||[])[0]))return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=he.needsContext.test(e)?0:o.length;i--&&(s=o[i],!E.relative[a=s.type]);)if((u=E.find[a])&&(r=u(s.matches[0].replace(we,Te),be.test(o[0].type)&&f(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&p(o)))return Z.apply(n,r),n;break}}return(l||L(e,c))(r,t,!Q,n,!t||be.test(e)&&f(t.parentNode)||t),n},T.sortStable=_.split("").sort(X).join("")===_,T.detectDuplicates=!!D,I(),T.sortDetached=i(function(e){return 1&e.compareDocumentPosition(O.createElement("fieldset"))}),i(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),T.attributes&&i(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(ne,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);Ce.find=ke,Ce.expr=ke.selectors,Ce.expr[":"]=Ce.expr.pseudos,Ce.uniqueSort=Ce.unique=ke.uniqueSort,Ce.text=ke.getText,Ce.isXMLDoc=ke.isXML,Ce.contains=ke.contains,Ce.escapeSelector=ke.escape;var Se=function(e,t,n){for(var r=[],i=n!==undefined;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&Ce(e).is(n))break;r.push(e)}return r},Le=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},je=Ce.expr.match.needsContext,Ne=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;Ce.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?Ce.find.matchesSelector(r,e)?[r]:[]:Ce.find.matches(e,Ce.grep(t,function(e){return 1===e.nodeType}))},Ce.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(Ce(e).filter(function(){for(t=0;t1?Ce.uniqueSort(n):n},filter:function(e){return this.pushStack(s(this,e||[],!1))},not:function(e){return this.pushStack(s(this,e||[],!0))},is:function(e){return!!s(this,"string"==typeof e&&je.test(e)?Ce(e):e||[],!1).length}});var Ae,De=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(Ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||Ae,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:De.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof Ce?t[0]:t,Ce.merge(this,Ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:we,!0)),Ne.test(r[1])&&Ce.isPlainObject(t))for(r in t)xe(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=we.getElementById(r[2]),i&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):xe(e)?n.ready!==undefined?n.ready(e):e(Ce):Ce.makeArray(e,this)}).prototype=Ce.fn,Ae=Ce(we);var Ie=/^(?:parents|prev(?:Until|All))/,Oe={children:!0,contents:!0,next:!0,prev:!0};Ce.fn.extend({has:function(e){var t=Ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&Ce.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?Ce.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?de.call(Ce(e),this[0]):de.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(Ce.uniqueSort(Ce.merge(this.get(),Ce(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),Ce.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return Se(e,"parentNode")},parentsUntil:function(e,t,n){return Se(e,"parentNode",n)},next:function(e){return a(e,"nextSibling")},prev:function(e){return a(e,"previousSibling")},nextAll:function(e){return Se(e,"nextSibling")},prevAll:function(e){return Se(e,"previousSibling")},nextUntil:function(e,t,n){return Se(e,"nextSibling",n)},prevUntil:function(e,t,n){return Se(e,"previousSibling",n)},siblings:function(e){return Le((e.parentNode||{}).firstChild,e)},children:function(e){return Le(e.firstChild)},contents:function(e){return null!=e.contentDocument&&ue(e.contentDocument)?e.contentDocument:(o(e,"template")&&(e=e.content||e),Ce.merge([],e.childNodes))}},function(e,t){Ce.fn[e]=function(n,r){var i=Ce.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=Ce.filter(r,i)),this.length>1&&(Oe[e]||Ce.uniqueSort(i),Ie.test(e)&&i.reverse()),this.pushStack(i)}});var Pe=/[^\x20\t\r\n\f]+/g;Ce.Callbacks=function(e){e="string"==typeof e?u(e):Ce.extend({},e);var t,n,i,o,s=[],a=[],l=-1,c=function(){for(o=o||e.once,i=t=!0;a.length;l=-1)for(n=a.shift();++l-1;)s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?Ce.inArray(e,s)>-1:s.length>0},empty:function(){return s&&(s=[]),this},disable:function(){return o=a=[],s=n="",this},disabled:function(){return!s},lock:function(){return o=a=[],n||t||(s=n=""),this},locked:function(){return!!o},fireWith:function(e,n){return o||(n=n||[],n=[e,n.slice?n.slice():n],a.push(n),t||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!i}};return f},Ce.extend({Deferred:function(t){var n=[["notify","progress",Ce.Callbacks("memory"),Ce.Callbacks("memory"),2],["resolve","done",Ce.Callbacks("once memory"),Ce.Callbacks("once memory"),0,"resolved"],["reject","fail",Ce.Callbacks("once memory"),Ce.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return Ce.Deferred(function(t){Ce.each(n,function(n,r){var i=xe(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&xe(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){function o(t,n,r,i){return function(){var a=this,u=arguments,f=function(){var e,f;if(!(t=s&&(r!==c&&(a=undefined,u=[e]),n.rejectWith(a,u))}};t?d():(Ce.Deferred.getStackHook&&(d.stackTrace=Ce.Deferred.getStackHook()),e.setTimeout(d))}}var s=0;return Ce.Deferred(function(e){n[0][3].add(o(0,e,xe(i)?i:l,e.notifyWith)),n[1][3].add(o(0,e,xe(t)?t:l)),n[2][3].add(o(0,e,xe(r)?r:c))}).promise()},promise:function(e){return null!=e?Ce.extend(e,i):i}},o={};return Ce.each(n,function(e,t){var s=t[2],a=t[5];i[t[1]]=s.add,a&&s.add(function(){r=a},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),s.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?undefined:this,arguments),this},o[t[0]+"With"]=s.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=le.call(arguments),o=Ce.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?le.call(arguments):n,--t||o.resolveWith(r,i)}};if(t<=1&&(f(e,o.done(s(n)).resolve,o.reject,!t),"pending"===o.state()||xe(i[n]&&i[n].then)))return o.then();for(;n--;)f(i[n],s(n),o.reject);return o.promise()}});var Qe=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;Ce.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&Qe.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},Ce.readyException=function(t){e.setTimeout(function(){throw t})};var Re=Ce.Deferred();Ce.fn.ready=function(e){return Re.then(e)["catch"](function(e){Ce.readyException(e)}),this},Ce.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--Ce.readyWait:Ce.isReady)||(Ce.isReady=!0,!0!==e&&--Ce.readyWait>0||Re.resolveWith(we,[Ce]))}}),Ce.ready.then=Re.then,"complete"===we.readyState||"loading"!==we.readyState&&!we.documentElement.doScroll?e.setTimeout(Ce.ready):(we.addEventListener("DOMContentLoaded",d),e.addEventListener("load",d));var qe=function(e,t,n,i,o,s,a){var u=0,l=e.length,c=null==n;if("object"===r(n)){o=!0;for(u in n)qe(e,t,u,n[u],!0,s,a)}else if(i!==undefined&&(o=!0,xe(i)||(a=!0),c&&(a?(t.call(e,i),t=null):(c=t,t=function(e,t,n){return c.call(Ce(e),n)})),t))for(;u1,null,!0)},removeData:function(e){return this.each(function(){Me.remove(this,e)})}}),Ce.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=He.get(e,t),n&&(!r||Array.isArray(n)?r=He.access(e,t,Ce.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=Ce.queue(e,t),r=n.length,i=n.shift(),o=Ce._queueHooks(e,t),s=function(){Ce.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return He.get(e,n)||He.access(e,n,{empty:Ce.Callbacks("once memory").add(function(){He.remove(e,[t+"queue",n])})})}}),Ce.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]*)/i,tt=/^$|^module$|\/(?:java|ecma)script/i;!function(){var e=we.createDocumentFragment(),t=e.appendChild(we.createElement("div")),n=we.createElement("input");n.setAttribute("type","radio"),n.setAttribute("checked","checked"),n.setAttribute("name","t"),t.appendChild(n),ve.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,t.innerHTML="",ve.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,t.innerHTML="",ve.option=!!t.lastChild}();var nt={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]} -;nt.tbody=nt.tfoot=nt.colgroup=nt.caption=nt.thead,nt.th=nt.td,ve.option||(nt.optgroup=nt.option=[1,""]);var rt=/<|&#?\w+;/,it=/^key/,ot=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,st=/^([^.]*)(?:\.(.+)|)/;Ce.event={global:{},add:function(e,t,n,r,i){var o,s,a,u,l,c,f,d,p,h,g,m=He.get(e);if(_e(e))for(n.handler&&(o=n,n=o.handler,i=o.selector),i&&Ce.find.matchesSelector(Xe,i),n.guid||(n.guid=Ce.guid++),(u=m.events)||(u=m.events=Object.create(null)),(s=m.handle)||(s=m.handle=function(t){return void 0!==Ce&&Ce.event.triggered!==t.type?Ce.event.dispatch.apply(e,arguments):undefined}),t=(t||"").match(Pe)||[""],l=t.length;l--;)a=st.exec(t[l])||[],p=g=a[1],h=(a[2]||"").split(".").sort(),p&&(f=Ce.event.special[p]||{},p=(i?f.delegateType:f.bindType)||p,f=Ce.event.special[p]||{},c=Ce.extend({type:p,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&Ce.expr.match.needsContext.test(i),namespace:h.join(".")},o),(d=u[p])||(d=u[p]=[],d.delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,s)||e.addEventListener&&e.addEventListener(p,s)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?d.splice(d.delegateCount++,0,c):d.push(c),Ce.event.global[p]=!0)},remove:function(e,t,n,r,i){var o,s,a,u,l,c,f,d,p,h,g,m=He.hasData(e)&&He.get(e);if(m&&(u=m.events)){for(t=(t||"").match(Pe)||[""],l=t.length;l--;)if(a=st.exec(t[l])||[],p=g=a[1],h=(a[2]||"").split(".").sort(),p){for(f=Ce.event.special[p]||{},p=(r?f.delegateType:f.bindType)||p,d=u[p]||[],a=a[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=d.length;o--;)c=d[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(d.splice(o,1),c.selector&&d.delegateCount--,f.remove&&f.remove.call(e,c));s&&!d.length&&(f.teardown&&!1!==f.teardown.call(e,h,m.handle)||Ce.removeEvent(e,p,m.handle),delete u[p])}else for(p in u)Ce.event.remove(e,p+t[l],n,r,!0);Ce.isEmptyObject(u)&&He.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,s,a=new Array(arguments.length),u=Ce.event.fix(e),l=(He.get(this,"events")||Object.create(null))[u.type]||[],c=Ce.event.special[u.type]||{};for(a[0]=u,t=1;t=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],s={},n=0;n-1:Ce.find(i,this,null,[l]).length),s[i]&&o.push(r);o.length&&a.push({elem:l,handlers:o})}return l=this,u\s*$/g;Ce.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=Ye(e);if(!(ve.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||Ce.isXMLDoc(e)))for(s=w(a),o=w(e),r=0,i=o.length;r0&&T(s,!u&&w(e,"script")),a},cleanData:function(e){for(var t,n,r,i=Ce.event.special,o=0;(n=e[o])!==undefined;o++)if(_e(n)){if(t=n[He.expando]){if(t.events)for(r in t.events)i[r]?Ce.event.remove(n,r):Ce.removeEvent(n,r,t.handle);n[He.expando]=undefined}n[Me.expando]&&(n[Me.expando]=undefined)}}}),Ce.fn.extend({detach:function(e){return R(this,e,!0)},remove:function(e){return R(this,e)},text:function(e){return qe(this,function(e){return e===undefined?Ce.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Q(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){A(this,e).appendChild(e)}})},prepend:function(){return Q(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=A(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Q(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Q(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(Ce.cleanData(w(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return Ce.clone(this,e,t)})},html:function(e){return qe(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!at.test(e)&&!nt[(et.exec(e)||["",""])[1].toLowerCase()]){e=Ce.htmlPrefilter(e);try{for(;n3,Xe.removeChild(t)),a}}))}();var ht=["Webkit","Moz","ms"],gt=we.createElement("div").style,mt={},yt=/^(none|table(?!-c[ea]).+)/,vt=/^--/,xt={position:"absolute",visibility:"hidden",display:"block"},bt={letterSpacing:"0",fontWeight:"400"};Ce.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=q(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=h(t),u=vt.test(t),l=e.style;if(u||(t=_(a)),s=Ce.cssHooks[t]||Ce.cssHooks[a],n===undefined)return s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:l[t];o=typeof n,"string"===o&&(i=Ve.exec(n))&&i[1]&&(n=v(e,t,i),o="number"),null!=n&&n===n&&("number"!==o||u||(n+=i&&i[3]||(Ce.cssNumber[a]?"":"px")),ve.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,s,a=h(t);return vt.test(t)||(t=_(a)),s=Ce.cssHooks[t]||Ce.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=q(e,t,r)),"normal"===i&&t in bt&&(i=bt[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),Ce.each(["height","width"],function(e,t){Ce.cssHooks[t]={get:function(e,n,r){if(n)return!yt.test(Ce.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?B(e,t,r):dt(e,xt,function(){return B(e,t,r)})},set:function(e,n,r){var i,o=ft(e),s=!ve.scrollboxSize()&&"absolute"===o.position,a=s||r,u=a&&"border-box"===Ce.css(e,"boxSizing",!1,o),l=r?M(e,t,r,u,o):0;return u&&s&&(l-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-M(e,t,"border",!1,o)-.5)),l&&(i=Ve.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=Ce.css(e,t)),H(e,n,l)}}}),Ce.cssHooks.marginLeft=F(ve.reliableMarginLeft,function(e,t){if(t)return(parseFloat(q(e,"marginLeft"))||e.getBoundingClientRect().left-dt(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),Ce.each({margin:"",padding:"",border:"Width"},function(e,t){Ce.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+Ue[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(Ce.cssHooks[e+t].set=H)}),Ce.fn.extend({css:function(e,t){return qe(this,function(e,t,n){var r,i,o={},s=0;if(Array.isArray(t)){for(r=ft(e),i=t.length;s1)}}),Ce.Tween=W,W.prototype={constructor:W,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||Ce.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(Ce.cssNumber[n]?"":"px")},cur:function(){var e=W.propHooks[this.prop];return e&&e.get?e.get(this):W.propHooks._default.get(this)},run:function(e){var t,n=W.propHooks[this.prop];return this.options.duration?this.pos=t=Ce.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):W.propHooks._default.set(this),this}},W.prototype.init.prototype=W.prototype,W.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=Ce.css(e.elem,e.prop,""),t&&"auto"!==t?t:0)},set:function(e){Ce.fx.step[e.prop]?Ce.fx.step[e.prop](e):1!==e.elem.nodeType||!Ce.cssHooks[e.prop]&&null==e.elem.style[_(e.prop)]?e.elem[e.prop]=e.now:Ce.style(e.elem,e.prop,e.now+e.unit)}}},W.propHooks.scrollTop=W.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},Ce.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},Ce.fx=W.prototype.init,Ce.fx.step={};var wt,Tt,Et=/^(?:toggle|show|hide)$/,Ct=/queueHooks$/;Ce.Animation=Ce.extend(J,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return v(n.elem,e,Ve.exec(t),n),n}]},tweener:function(e,t){xe(e)?(t=e,e=["*"]):e=e.match(Pe);for(var n,r=0,i=e.length;r1)},removeAttr:function(e){return this.each(function(){Ce.removeAttr(this,e)})}}),Ce.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?Ce.prop(e,t,n):(1===o&&Ce.isXMLDoc(e)||(i=Ce.attrHooks[t.toLowerCase()]||(Ce.expr.match.bool.test(t)?kt:undefined)),n!==undefined?null===n?void Ce.removeAttr(e,t):i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:(r=Ce.find.attr(e,t),null==r?undefined:r))},attrHooks:{type:{set:function(e,t){if(!ve.radioValue&&"radio"===t&&o(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(Pe);if(i&&1===e.nodeType)for(;n=i[r++];)e.removeAttribute(n)}}),kt={set:function(e,t,n){return!1===t?Ce.removeAttr(e,n):e.setAttribute(n,n),n}},Ce.each(Ce.expr.match.bool.source.match(/\w+/g),function(e,t){var n=St[t]||Ce.find.attr;St[t]=function(e,t,r){var i,o,s=t.toLowerCase();return r||(o=St[s],St[s]=i,i=null!=n(e,t,r)?s:null,St[s]=o),i}});var Lt=/^(?:input|select|textarea|button)$/i,jt=/^(?:a|area)$/i;Ce.fn.extend({prop:function(e,t){return qe(this,Ce.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[Ce.propFix[e]||e]})}}),Ce.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&Ce.isXMLDoc(e)||(t=Ce.propFix[t]||t,i=Ce.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=Ce.find.attr(e,"tabindex");return t?parseInt(t,10):Lt.test(e.nodeName)||jt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),ve.optSelected||(Ce.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),Ce.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){Ce.propFix[this.toLowerCase()]=this}),Ce.fn.extend({addClass:function(e){var t,n,r,i,o,s,a,u=0;if(xe(e))return this.each(function(t){Ce(this).addClass(e.call(this,t,Z(this)))});if(t=ee(e),t.length)for(;n=this[u++];)if(i=Z(n),r=1===n.nodeType&&" "+K(i)+" "){for(s=0;o=t[s++];)r.indexOf(" "+o+" ")<0&&(r+=o+" ");a=K(r),i!==a&&n.setAttribute("class",a)}return this},removeClass:function(e){var t,n,r,i,o,s,a,u=0;if(xe(e))return this.each(function(t){Ce(this).removeClass(e.call(this,t,Z(this)))});if(!arguments.length)return this.attr("class","");if(t=ee(e),t.length)for(;n=this[u++];)if(i=Z(n),r=1===n.nodeType&&" "+K(i)+" "){for(s=0;o=t[s++];)for(;r.indexOf(" "+o+" ")>-1;)r=r.replace(" "+o+" "," ");a=K(r),i!==a&&n.setAttribute("class",a)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):xe(e)?this.each(function(n){Ce(this).toggleClass(e.call(this,n,Z(this),t),t)}):this.each(function(){var t,i,o,s;if(r)for(i=0,o=Ce(this),s=ee(e);t=s[i++];)o.hasClass(t)?o.removeClass(t):o.addClass(t);else e!==undefined&&"boolean"!==n||(t=Z(this),t&&He.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":He.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+K(Z(n))+" ").indexOf(t)>-1)return!0;return!1}});var Nt=/\r/g;Ce.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=xe(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,Ce(this).val()):e,null==i?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=Ce.map(i,function(e){return null==e?"":e+""})),(t=Ce.valHooks[this.type]||Ce.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return(t=Ce.valHooks[i.type]||Ce.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace(Nt,""):null==n?"":n)}}}),Ce.extend({valHooks:{option:{get:function(e){var t=Ce.find.attr(e,"value");return null!=t?t:K(Ce.text(e))}},select:{get:function(e){var t,n,r,i=e.options,s=e.selectedIndex,a="select-one"===e.type,u=a?null:[],l=a?s+1:i.length;for(r=s<0?l:a?s:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),Ce.each(["radio","checkbox"],function(){Ce.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=Ce.inArray(Ce(e).val(),t)>-1}},ve.checkOn||(Ce.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),ve.focusin="onfocusin"in e;var At=/^(?:focusinfocus|focusoutblur)$/,Dt=function(e){e.stopPropagation()};Ce.extend(Ce.event,{trigger:function(t,n,r,i){var o,s,a,u,l,c,f,d,p=[r||we],h=ge.call(t,"type")?t.type:t,g=ge.call(t,"namespace")?t.namespace.split("."):[];if(s=d=a=r=r||we,3!==r.nodeType&&8!==r.nodeType&&!At.test(h+Ce.event.triggered)&&(h.indexOf(".")>-1&&(g=h.split("."),h=g.shift(),g.sort()),l=h.indexOf(":")<0&&"on"+h,t=t[Ce.expando]?t:new Ce.Event(h,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:Ce.makeArray(n,[t]),f=Ce.event.special[h]||{},i||!f.trigger||!1!==f.trigger.apply(r,n))){if(!i&&!f.noBubble&&!be(r)){for(u=f.delegateType||h,At.test(u+h)||(s=s.parentNode);s;s=s.parentNode)p.push(s),a=s;a===(r.ownerDocument||we)&&p.push(a.defaultView||a.parentWindow||e)}for(o=0;(s=p[o++])&&!t.isPropagationStopped();)d=s,t.type=o>1?u:f.bindType||h,c=(He.get(s,"events")||Object.create(null))[t.type]&&He.get(s,"handle"),c&&c.apply(s,n),(c=l&&s[l])&&c.apply&&_e(s)&&(t.result=c.apply(s,n),!1===t.result&&t.preventDefault());return t.type=h,i||t.isDefaultPrevented()||f._default&&!1!==f._default.apply(p.pop(),n)||!_e(r)||l&&xe(r[h])&&!be(r)&&(a=r[l],a&&(r[l]=null),Ce.event.triggered=h,t.isPropagationStopped()&&d.addEventListener(h,Dt),r[h](),t.isPropagationStopped()&&d.removeEventListener(h,Dt),Ce.event.triggered=undefined,a&&(r[l]=a)),t.result}},simulate:function(e,t,n){var r=Ce.extend(new Ce.Event,n,{type:e,isSimulated:!0});Ce.event.trigger(r,null,t)}}),Ce.fn.extend({trigger:function(e,t){return this.each(function(){Ce.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return Ce.event.trigger(e,t,n,!0)}}),ve.focusin||Ce.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){Ce.event.simulate(t,e.target,Ce.event.fix(e))};Ce.event.special[t]={setup:function(){var r=this.ownerDocument||this.document||this,i=He.access(r,t);i||r.addEventListener(e,n,!0),He.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this.document||this,i=He.access(r,t)-1;i?He.access(r,t,i):(r.removeEventListener(e,n,!0),He.remove(r,t))}}});var It=e.location,Ot={guid:Date.now()},Pt=/\?/;Ce.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=undefined}return n&&!n.getElementsByTagName("parsererror").length||Ce.error("Invalid XML: "+t),n};var Qt=/\[\]$/,Rt=/\r?\n/g,qt=/^(?:submit|button|image|reset|file)$/i,Ft=/^(?:input|select|textarea|keygen)/i;Ce.param=function(e,t){var n,r=[],i=function(e,t){var n=xe(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!Ce.isPlainObject(e))Ce.each(e,function(){i(this.name,this.value)});else for(n in e)te(n,e[n],t,i);return r.join("&")},Ce.fn.extend({serialize:function(){return Ce.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=Ce.prop(this,"elements");return e?Ce.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!Ce(this).is(":disabled")&&Ft.test(this.nodeName)&&!qt.test(e)&&(this.checked||!Ze.test(e))}).map(function(e,t){var n=Ce(this).val();return null==n?null:Array.isArray(n)?Ce.map(n,function(e){return{name:t.name,value:e.replace(Rt,"\r\n")}}):{name:t.name,value:n.replace(Rt,"\r\n")}}).get()}});var $t=/%20/g,_t=/#.*$/,Ht=/([?&])_=[^&]*/,Mt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Bt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Wt=/^(?:GET|HEAD)$/,zt=/^\/\//,Vt={},Ut={},Xt="*/".concat("*"),Yt=we.createElement("a");Yt.href=It.href,Ce.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:It.href,type:"GET",isLocal:Bt.test(It.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Xt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":Ce.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?ie(ie(e,Ce.ajaxSettings),t):ie(Ce.ajaxSettings,e)},ajaxPrefilter:ne(Vt),ajaxTransport:ne(Ut),ajax:function(t,n){function r(t,n,r,a){var l,d,p,b,w,T=n;c||(c=!0,u&&e.clearTimeout(u),i=undefined,s=a||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=oe(h,E,r)),!l&&Ce.inArray("script",h.dataTypes)>-1&&(h.converters["text script"]=function(){}),b=se(h,b,E,l),l?(h.ifModified&&(w=E.getResponseHeader("Last-Modified"),w&&(Ce.lastModified[o]=w),(w=E.getResponseHeader("etag"))&&(Ce.etag[o]=w)),204===t||"HEAD"===h.type?T="nocontent":304===t?T="notmodified":(T=b.state,d=b.data,p=b.error,l=!p)):(p=T,!t&&T||(T="error",t<0&&(t=0))),E.status=t,E.statusText=(n||T)+"",l?y.resolveWith(g,[d,T,E]):y.rejectWith(g,[E,T,p]),E.statusCode(x),x=undefined,f&&m.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?d:p]),v.fireWith(g,[E,T]),f&&(m.trigger("ajaxComplete",[E,h]),--Ce.active||Ce.event.trigger("ajaxStop")))}"object"==typeof t&&(n=t,t=undefined),n=n||{};var i,o,s,a,u,l,c,f,d,p,h=Ce.ajaxSetup({},n),g=h.context||h,m=h.context&&(g.nodeType||g.jquery)?Ce(g):Ce.event,y=Ce.Deferred(),v=Ce.Callbacks("once memory"),x=h.statusCode||{},b={},w={},T="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!a)for(a={};t=Mt.exec(s);)a[t[1].toLowerCase()+" "]=(a[t[1].toLowerCase()+" "]||[]).concat(t[2]);t=a[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return c?s:null},setRequestHeader:function(e,t){return null==c&&(e=w[e.toLowerCase()]=w[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||T;return i&&i.abort(t),r(0,t),this}};if(y.promise(E),h.url=((t||h.url||It.href)+"").replace(zt,It.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(Pe)||[""],null==h.crossDomain){l=we.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Yt.protocol+"//"+Yt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=Ce.param(h.data,h.traditional)),re(Vt,h,n,E),c)return E;f=Ce.event&&h.global,f&&0==Ce.active++&&Ce.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Wt.test(h.type),o=h.url.replace(_t,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace($t,"+")):(p=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(Pt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),p=(Pt.test(o)?"&":"?")+"_="+Ot.guid+++p),h.url=o+p),h.ifModified&&(Ce.lastModified[o]&&E.setRequestHeader("If-Modified-Since",Ce.lastModified[o]),Ce.etag[o]&&E.setRequestHeader("If-None-Match",Ce.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+Xt+"; q=0.01":""):h.accepts["*"]);for(d in h.headers)E.setRequestHeader(d,h.headers[d]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(T="abort",v.add(h.complete),E.done(h.success),E.fail(h.error),i=re(Ut,h,n,E)){if(E.readyState=1,f&&m.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,r)}catch(e){if(c)throw e;r(-1,e)}}else r(-1,"No Transport");return E},getJSON:function(e,t,n){return Ce.get(e,t,n,"json")},getScript:function(e,t){return Ce.get(e,undefined,t,"script")}}),Ce.each(["get","post"],function(e,t){Ce[t]=function(e,n,r,i){return xe(n)&&(i=i||r,r=n,n=undefined),Ce.ajax(Ce.extend({url:e,type:t,dataType:i,data:n,success:r},Ce.isPlainObject(e)&&e))}}),Ce.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),Ce._evalUrl=function(e,t,n){return Ce.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){Ce.globalEval(e,t,n)}})},Ce.fn.extend({wrapAll:function(e){var t;return this[0]&&(xe(e)&&(e=e.call(this[0])),t=Ce(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return xe(e)?this.each(function(t){Ce(this).wrapInner(e.call(this,t))}):this.each(function(){var t=Ce(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=xe(e);return this.each(function(n){Ce(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){Ce(this).replaceWith(this.childNodes)}),this}}),Ce.expr.pseudos.hidden=function(e){return!Ce.expr.pseudos.visible(e)},Ce.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)}, -Ce.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Gt={0:200,1223:204},Jt=Ce.ajaxSettings.xhr();ve.cors=!!Jt&&"withCredentials"in Jt,ve.ajax=Jt=!!Jt,Ce.ajaxTransport(function(t){var n,r;if(ve.cors||Jt&&!t.crossDomain)return{send:function(i,o){var s,a=t.xhr();if(a.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(s in t.xhrFields)a[s]=t.xhrFields[s];t.mimeType&&a.overrideMimeType&&a.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(s in i)a.setRequestHeader(s,i[s]);n=function(e){return function(){n&&(n=r=a.onload=a.onerror=a.onabort=a.ontimeout=a.onreadystatechange=null,"abort"===e?a.abort():"error"===e?"number"!=typeof a.status?o(0,"error"):o(a.status,a.statusText):o(Gt[a.status]||a.status,a.statusText,"text"!==(a.responseType||"text")||"string"!=typeof a.responseText?{binary:a.response}:{text:a.responseText},a.getAllResponseHeaders()))}},a.onload=n(),r=a.onerror=a.ontimeout=n("error"),a.onabort!==undefined?a.onabort=r:a.onreadystatechange=function(){4===a.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{a.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),Ce.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),Ce.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return Ce.globalEval(e),e}}}),Ce.ajaxPrefilter("script",function(e){e.cache===undefined&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),Ce.ajaxTransport("script",function(e){if(e.crossDomain||e.scriptAttrs){var t,n;return{send:function(r,i){t=Ce(" + + diff --git a/lib/server/README.md b/lib/server/README.md new file mode 100644 index 0000000..d5f18e5 --- /dev/null +++ b/lib/server/README.md @@ -0,0 +1,34 @@ +# server + +Tiny static-file HTTP server + +- Text routes default to `ok` +- File-like `.get(...)` paths serve that file +- Static dirs default from the prefix +- `index.html` auto-serves at `/` + +## usage + +```js +import { Server } from '#lib/server' + +Server.create({ root: import.meta.dirname }) + .get('/health') + .get('/robots.txt', 'User-agent: *\nAllow: /\n') + .static('/assets') + .listen(process.env.PORT || 0, function() { + console.log(`listening: ${this.address().port}`) + }) +``` + +## test + +```sh +npm test +``` + +## gotchas + +- Root bounds page files and static dirs +- Query strings are ignored for routing +- Only `GET` and `HEAD` are allowed diff --git a/lib/server/index.js b/lib/server/index.js new file mode 100644 index 0000000..e86093a --- /dev/null +++ b/lib/server/index.js @@ -0,0 +1,253 @@ +import { Server as HttpServer } from 'node:http' +import { createHash } from 'node:crypto' +import { readFile, access } from 'node:fs/promises' +import { join, resolve, extname, + relative, isAbsolute } from 'node:path' + +class Server extends HttpServer { + static DEFAULTS = { + root: import.meta.dirname, + maxAge: 300, + mime: { + '.html': 'text/html', + '.txt': 'text/plain', + '.xml': 'application/xml', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.webp': 'image/webp' + }, + security: { + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'Referrer-Policy': + 'strict-origin-when-cross-origin', + 'Permissions-Policy': + 'clipboard-write=(self)' + } + } + + #config + #pages = {} + #routes = {} + #staticUrl + #staticDir + + static create(config = {}) { + return new Server({ + ...Server.DEFAULTS, ...config, + mime: { ...Server.DEFAULTS.mime, ...config.mime }, + security: { + ...Server.DEFAULTS.security, + ...config.security + } + }) + } + + constructor(config) { + super((req, res) => this.#handle(req, res)) + this.#config = { + ...config, + root: resolve(config.root) + } + } + + get(path, body) { + const ext = extname(body || path) + const mime = this.#config.mime[ext] + + return mime + ? this.#addPage(path, body || path.slice(1)) + : this.#addRoute(path, body || 'ok') + } + + static(prefix, dir) { + this.#staticUrl = prefix.startsWith('/') + ? prefix : `/${prefix}` + const dirPath = resolve( + this.#config.root, + dir || prefix.replace(/^\//, '') + ) + + if (!this.#inRoot( + this.#config.root, dirPath + )) + throw new Error('static dir outside root') + + this.#staticDir = dirPath + + return this + } + + async listen(port = 0, cb) { + await this.#detectIndex() + await this.#loadPages() + + return super.listen(port, cb) + } + + #addPage(path, file) { + const filePath = resolve( + this.#config.root, file + ) + + if (!this.#inRoot( + this.#config.root, filePath + )) + throw new Error('page file outside root') + + this.#pages[path] = { file, filePath } + return this + } + + #addRoute(path, body) { + this.#routes[path] = body + return this + } + + async #detectIndex() { + const index = join(this.#config.root, + 'index.html') + + if (this.#pages['/']) return + + await access(index) + .then(() => this.#addPage('/', 'index.html')) + .catch(() => {}) + } + + async #loadPages() { + for (const [, page] of + Object.entries(this.#pages)) { + page.html = await readFile( + page.filePath + ) + page.etag = `"${ + createHash('sha256') + .update(page.html) + .digest('hex') + .slice(0, 16) + }"` + } + } + + #inRoot(base, file) { + const rel = relative(base, file) + + return rel === '' || ( + !rel.startsWith('..') + && !isAbsolute(rel) + ) + } + + #path(url = '/') { + return url.replace(/[?#].*$/, '') + || '/' + } + + #send(res, status, body, headers = {}) { + return res.writeHead(status, { + 'Content-Type': + 'text/plain; charset=utf-8', + ...this.#config.security, + ...headers + }).end(body) + } + + #sendPage(req, res, page) { + const { maxAge } = this.#config + + return req.headers['if-none-match'] + === page.etag + ? this.#send(res, 304, null, { + ETag: page.etag, + 'Cache-Control': + `public, max-age=${maxAge}` + }) + : this.#send(res, 200, page.html, { + 'Content-Type': + `${this.#config.mime[ + extname(page.file) + ]}; charset=utf-8`, + 'Content-Length': page.html.length, + 'Cache-Control': + `public, max-age=${maxAge}`, + ETag: page.etag + }) + } + + #sendStatic(path, res) { + const { mime, maxAge } = this.#config + const rel = path.slice( + this.#staticUrl.length + ) + const file = resolve( + this.#staticDir, `.${rel}` + ) + + return !file.startsWith( + `${this.#staticDir}/`) + ? this.#send(res, 403, 'Forbidden\n') + : !mime[extname(file)] + ? this.#send(res, 404, 'Not Found\n', { + 'Cache-Control': 'no-store' + }) + : readFile(file) + .then(data => this.#send( + res, 200, data, { + 'Content-Type': + mime[extname(file)], + 'Content-Length': data.length, + 'Cache-Control': + `public, max-age=${maxAge}` + } + )) + .catch(() => this.#send( + res, 404, 'Not Found\n', + { 'Cache-Control': 'no-store' } + )) + } + + #handle(req, res) { + try { + if (req.method !== 'GET' + && req.method !== 'HEAD') + return this.#send( + res, 405, 'Method Not Allowed\n', + { Allow: 'GET, HEAD' } + ) + + const path = this.#path(req.url) + const page = this.#pages[path] + + if (page) + return this.#sendPage(req, res, page) + + const route = this.#routes[path] + + if (route !== undefined) + return this.#send(res, 200, route, { + 'Cache-Control': 'no-store' + }) + + return this.#staticUrl + && path.startsWith( + `${this.#staticUrl}/`) + ? this.#sendStatic(path, res) + : this.#send( + res, 404, 'Not Found\n', + { 'Cache-Control': 'no-store' } + ) + } catch { + if (!res.headersSent) + this.#send( + res, 500, 'Internal Server Error\n', + { 'Cache-Control': 'no-store' } + ) + } + } +} + +export { Server } diff --git a/lib/server/package.json b/lib/server/package.json new file mode 100644 index 0000000..1dd5f5a --- /dev/null +++ b/lib/server/package.json @@ -0,0 +1,6 @@ +{ + "name": "server", + "version": "1.0.0", + "type": "module", + "exports": "./index.js" +} diff --git a/lib/server/test/fixtures/assets/pixel.png b/lib/server/test/fixtures/assets/pixel.png new file mode 100644 index 0000000..0dd1608 --- /dev/null +++ b/lib/server/test/fixtures/assets/pixel.png @@ -0,0 +1,2 @@ +‰PNG + diff --git a/lib/server/test/fixtures/index.html b/lib/server/test/fixtures/index.html new file mode 100644 index 0000000..58d93db --- /dev/null +++ b/lib/server/test/fixtures/index.html @@ -0,0 +1 @@ +

test

diff --git a/lib/server/test/fixtures/robots.txt b/lib/server/test/fixtures/robots.txt new file mode 100644 index 0000000..7d329b1 --- /dev/null +++ b/lib/server/test/fixtures/robots.txt @@ -0,0 +1 @@ +User-agent: * diff --git a/lib/server/test/main.test.js b/lib/server/test/main.test.js new file mode 100644 index 0000000..af786e9 --- /dev/null +++ b/lib/server/test/main.test.js @@ -0,0 +1,264 @@ +import { test } from 'node:test' +import { join } from 'node:path' +import { Server } from '../index.js' + +const ROOT = join(import.meta.dirname, 'fixtures') + +test('#Server', async t => { + await t.test('#create', async t => { + await t.test('returns a Server', t => { + const server = Server.create() + + t.assert.ok(server instanceof Server) + }) + + }) + + await t.test('#get', async t => { + t.beforeEach(t => { + t.server = Server.create({ root: ROOT }) + }) + + await t.test('body with known extension', async t => { + await t.test('chains', t => { + t.assert.strictEqual( + t.server.get('/', 'index.html'), t.server + ) + }) + }) + + await t.test('body with no extension', async t => { + await t.test('chains', t => { + t.assert.strictEqual( + t.server.get('/ping', 'pong'), t.server + ) + }) + }) + + await t.test('no body, path has known extension', + async t => { + await t.test('chains', t => { + t.assert.strictEqual( + t.server.get('/robots.txt'), t.server + ) + }) + }) + + await t.test('no body, path has no extension', + async t => { + await t.test('chains', t => { + t.assert.strictEqual( + t.server.get('/health'), t.server + ) + }) + }) + + await t.test('body with unknown extension', async t => { + await t.test('chains as text route', t => { + t.assert.strictEqual( + t.server.get('/foo', 'bar.xyz'), t.server + ) + }) + }) + + await t.test('body with dotted text', async t => { + await t.test('chains as text route', t => { + t.assert.strictEqual( + t.server.get('/version', 'v1.0.0'), t.server + ) + }) + }) + + }) + + await t.test('#static', async t => { + t.beforeEach(t => { + t.server = Server.create({ root: ROOT }) + }) + + await t.test('with prefix and dir', async t => { + await t.test('chains', t => { + t.assert.strictEqual( + t.server.static('/img', 'assets'), t.server + ) + }) + }) + + await t.test('prefix only', async t => { + await t.test('derives dir from prefix', t => { + t.assert.strictEqual( + t.server.static('/assets'), t.server + ) + }) + }) + + }) + + await t.test('#listen', async t => { + t.beforeEach(async t => { + t.server = Server.create({ root: ROOT }) + .get('/health') + .get('/ping', 'pong') + .get('/robots.txt') + .get('/', 'index.html') + .static('/assets') + + await t.server.listen(0, () => {}) + + t.fetch = (path, opts) => fetch( + `http://127.0.0.1:${ + t.server.address().port + }${path}`, opts + ) + }) + + t.afterEach(async t => { + await new Promise(ok => t.server.close(ok)) + }) + + await t.test('auto-detects index.html', async t => { + const server = Server.create({ root: ROOT }) + .get('/health') + + await server.listen(0, () => {}) + + const res = await fetch( + `http://127.0.0.1:${server.address().port}/` + ) + + t.assert.strictEqual(res.status, 200) + t.assert.ok( + res.headers.get('content-type') + .includes('text/html') + ) + + await new Promise(ok => server.close(ok)) + }) + + await t.test('page route', async t => { + await t.test('responds 200 with html', async t => { + const res = await t.fetch('/') + + t.assert.strictEqual(res.status, 200) + t.assert.ok( + res.headers.get('content-type') + .includes('text/html') + ) + }) + + await t.test('includes etag', async t => { + const res = await t.fetch('/') + + t.assert.ok(res.headers.get('etag')) + }) + + await t.test('responds 304 on matching etag', + async t => { + const first = await t.fetch('/') + const etag = first.headers.get('etag') + + const second = await t.fetch('/', { + headers: { 'If-None-Match': etag } + }) + + t.assert.strictEqual(second.status, 304) + }) + + await t.test('includes cache-control', async t => { + const res = await t.fetch('/') + const cc = res.headers.get('cache-control') + + t.assert.ok(cc.includes('max-age=')) + }) + + await t.test('responds 200 with query string', + async t => { + const res = await t.fetch('/?v=1') + + t.assert.strictEqual(res.status, 200) + }) + }) + + await t.test('text route', async t => { + await t.test('responds 200', async t => { + const res = await t.fetch('/health') + + t.assert.strictEqual(res.status, 200) + t.assert.strictEqual(await res.text(), 'ok') + }) + + await t.test('responds with custom body', + async t => { + const res = await t.fetch('/ping') + + t.assert.strictEqual( + await res.text(), 'pong' + ) + }) + + await t.test('responds 200 with query string', + async t => { + const res = await t.fetch('/health?view=full') + + t.assert.strictEqual(res.status, 200) + t.assert.strictEqual(await res.text(), 'ok') + }) + }) + + await t.test('file route from path', async t => { + await t.test('serves file content', async t => { + const res = await t.fetch('/robots.txt') + + t.assert.strictEqual(res.status, 200) + t.assert.ok( + (await res.text()).includes('User-agent') + ) + }) + }) + + await t.test('unknown path', async t => { + await t.test('responds 404', async t => { + const res = await t.fetch('/nonexistent') + + t.assert.strictEqual(res.status, 404) + }) + }) + + await t.test('static files', async t => { + await t.test('serves known mime type', async t => { + const res = await t.fetch('/assets/pixel.png') + + t.assert.strictEqual(res.status, 200) + t.assert.ok( + res.headers.get('content-type') + .includes('image/png') + ) + }) + + await t.test('responds 404 on missing file', + async t => { + const res = await t.fetch('/assets/nope.png') + + t.assert.strictEqual(res.status, 404) + }) + + await t.test('responds 404 on unknown extension', + async t => { + const res = await t.fetch('/assets/file.xyz') + + t.assert.strictEqual(res.status, 404) + }) + + await t.test('serves file with query string', + async t => { + const res = await t.fetch('/assets/pixel.png?v=1') + + t.assert.strictEqual(res.status, 200) + t.assert.ok( + res.headers.get('content-type') + .includes('image/png') + ) + }) + }) + }) +}) diff --git a/lib/server/test/security.test.js b/lib/server/test/security.test.js new file mode 100644 index 0000000..f4fc565 --- /dev/null +++ b/lib/server/test/security.test.js @@ -0,0 +1,116 @@ +import { test } from 'node:test' +import { request } from 'node:http' +import { join } from 'node:path' +import { Server } from '../index.js' + +const ROOT = join(import.meta.dirname, 'fixtures') + +test('#Server', async t => { + await t.test('#get', async t => { + t.beforeEach(t => { + t.server = Server.create({ root: ROOT }) + }) + + await t.test('body outside root', async t => { + await t.test('throws', t => { + t.assert.throws( + () => t.server.get('/foo', '../secret.html'), + { message: /outside root/i } + ) + }) + }) + }) + + await t.test('#static', async t => { + t.beforeEach(t => { + t.server = Server.create({ root: ROOT }) + }) + + await t.test('dir outside root', async t => { + await t.test('throws', t => { + t.assert.throws( + () => t.server.static('/escape', '../../..'), + { message: /outside root/i } + ) + }) + }) + + await t.test('sibling dir sharing root prefix', + async t => { + await t.test('throws', t => { + t.assert.throws( + () => t.server.static( + '/escape', '../fixtures2' + ), + { message: /outside root/i } + ) + }) + }) + }) + + await t.test('#listen', async t => { + t.beforeEach(async t => { + t.server = Server.create({ root: ROOT }) + .get('/health') + .get('/ping', 'pong') + .get('/robots.txt') + .get('/', 'index.html') + .static('/assets') + + await t.server.listen(0, () => {}) + + t.fetch = (path, opts) => fetch( + `http://127.0.0.1:${ + t.server.address().port + }${path}`, opts + ) + }) + + t.afterEach(async t => { + await new Promise(ok => t.server.close(ok)) + }) + + await t.test('security headers', async t => { + await t.test('includes all headers', async t => { + const res = await t.fetch('/') + + t.assert.partialDeepStrictEqual( + Object.fromEntries(res.headers), { + 'x-content-type-options': 'nosniff', + 'x-frame-options': 'DENY', + 'referrer-policy': + 'strict-origin-when-cross-origin', + 'permissions-policy': + 'clipboard-write=(self)' + } + ) + }) + }) + + await t.test('method not allowed', async t => { + await t.test('responds 405 on POST', async t => { + const res = await t.fetch('/', { method: 'POST' }) + + t.assert.strictEqual(res.status, 405) + t.assert.ok( + res.headers.get('allow').includes('GET') + ) + }) + }) + + await t.test('static files', async t => { + await t.test('responds 403 on traversal', + async t => { + const { port } = t.server.address() + const res = await new Promise(ok => + request({ + hostname: '127.0.0.1', port, + path: '/assets/../../../etc/passwd' + }, ok).end() + ) + + t.assert.strictEqual(res.statusCode, 403) + }) + }) + }) +}) diff --git a/package.json b/package.json index 78a705a..4abee64 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,14 @@ "version": "1.0.0", "description": "Bitpaper API documentation", "type": "module", - "exports": "./server.js", "engines": { "node": ">=24" }, "scripts": { - "start": "node server.js" + "start": "PORT=${PORT:-3007} node --title=bp-api-docs app.js", + "dev": "PORT=${PORT:-3007} node --title=bp-api-docs --watch --watch-path=app.js --watch-path=lib --watch-path=index.html --watch-path=assets app.js", + "test": "node --test '**/*.test.js'" + }, + "imports": { + "#lib/*": "./lib/*/index.js" }, "license": "MIT" } diff --git a/server.js b/server.js deleted file mode 100644 index 7025ed2..0000000 --- a/server.js +++ /dev/null @@ -1,44 +0,0 @@ -import { createServer } from 'node:http' -import { readFile } from 'node:fs/promises' -import { join, extname } from 'node:path' - -const root = join(import.meta.dirname, 'build') -const port = parseInt(process.env.PORT || '4567', 10) - -const types = { - '.html': 'text/html', - '.css': 'text/css', - '.js': 'application/javascript', - '.json': 'application/json', - '.svg': 'image/svg+xml', - '.png': 'image/png', - '.ico': 'image/x-icon', - '.woff': 'font/woff', - '.woff2': 'font/woff2', - '.ttf': 'font/ttf', - '.eot': 'application/vnd.ms-fontobject', -} - -const serve = async (req, res) => { - const url = req.url.split('?')[0] - const path = url === '/' - ? join(root, 'index.html') - : join(root, url) - - try { - const body = await readFile(path) - const ext = extname(path) - - res.writeHead(200, { 'Content-Type': types[ext] || 'application/octet-stream' }) - res.end(body) - } catch { - const fallback = await readFile(join(root, 'index.html')) - - res.writeHead(404, { 'Content-Type': 'text/html' }) - res.end(fallback) - } -} - -createServer(serve).listen(port, () => - console.log(`docs: http://localhost:${port}`) -) diff --git a/test/smoke.test.js b/test/smoke.test.js new file mode 100644 index 0000000..6e969f7 --- /dev/null +++ b/test/smoke.test.js @@ -0,0 +1,48 @@ +import { test } from 'node:test' +import { join } from 'node:path' +import { Server } from '#lib/server' + +const ROOT = join(import.meta.dirname, '..') + +test('smoke', async t => { + t.beforeEach(async t => { + t.server = Server.create({ root: ROOT }) + .get('/health') + .get('/robots.txt', 'User-agent: *\nAllow: /\n') + .static('/assets') + + await t.server.listen(0, () => {}) + + t.fetch = path => fetch( + `http://127.0.0.1:${t.server.address().port}${path}` + ) + }) + + t.afterEach(async t => { + await new Promise(ok => t.server.close(ok)) + }) + + await t.test('GET / responds 200', async t => { + const res = await t.fetch('/') + + t.assert.strictEqual(res.status, 200) + }) + + await t.test('GET /health responds 200', async t => { + const res = await t.fetch('/health') + + t.assert.strictEqual(res.status, 200) + }) + + await t.test('GET /robots.txt responds 200', async t => { + const res = await t.fetch('/robots.txt') + + t.assert.strictEqual(res.status, 200) + }) + + await t.test('GET /unknown responds 404', async t => { + const res = await t.fetch('/nope') + + t.assert.strictEqual(res.status, 404) + }) +})