Compare commits

..

517 Commits

Author SHA1 Message Date
github-actions[bot] ba2a523722 Version Packages (#2387)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-19 08:28:14 +02:00
Samy Pessé 57adb3e54a Fix publish with changeset (#2386) 2024-07-19 08:23:03 +02:00
github-actions[bot] 7377928c8a Version Packages (#2385)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-18 21:05:22 +02:00
Samy Pessé 5f8a8fe9b1 Setup Changeset to publish packages to NPM (#2384) 2024-07-18 21:00:50 +02:00
Scott Cazan 29a0bf25c4 Update Site search endpoint (#2382)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-07-18 10:36:12 +02:00
spastorelli 92bb016576 Show additional context when linking to a page group & a page in another space (#2381) 2024-07-18 10:22:15 +02:00
Samy Pessé c8810ea002 Hide pages from table of contents when set as hidden (#2383)
Co-authored-by: Scott Cazan <scottcazan@gmail.com>
2024-07-18 09:50:46 +02:00
spastorelli aa8bbaddc4 Fallback redirect to root when switching variants (#2376) 2024-07-16 09:24:31 +02:00
Samy Pessé b8c136e9c0 Fix render of one of when alternatives are primitives (#2378) 2024-07-15 10:15:42 +02:00
Samy Pessé 46d54172b0 Fix CI step to verify formatting (#2379) 2024-07-14 23:53:55 +02:00
Brett Jephson 68dc2de190 Remove Page/Space label from BlockContentRef (#2374) 2024-07-12 15:02:05 +01:00
Samy Pessé 81b372f372 Fix response example when response example is a string (#2377) 2024-07-12 10:35:22 +02:00
Scott Cazan 966e520640 Fix Source links in Ask AI not navigating to the page (#2375) 2024-07-11 15:39:28 +02:00
spastorelli 12702930ec Optimize SpacesDropdownMenuItem client side props (#2373) 2024-07-11 10:42:53 +02:00
spastorelli 6ce57bcda1 Fix issue where nav route/path is not retained when switching variants (#2372) 2024-07-10 17:10:19 +02:00
spastorelli d99b453bc4 Fix issue where published content crashes when localstorage is disabled (#2371) 2024-07-10 14:56:21 +02:00
Samy Pessé aaf5793cb7 Use schema.example if defined, even if value is falsy (#2370) 2024-07-05 16:34:48 +02:00
Samy Pessé 9b1762d2ab Fix invalid api response from carbon (#2369) 2024-07-05 15:55:35 +02:00
Samy Pessé 8d38d000fd Fix RND-4344 (#2368) 2024-07-04 17:04:35 +02:00
Scott Cazan 4cf9cdd56d Fix ToC not re-rendering when linking to a variant from SearchResults (#2366) 2024-07-03 12:22:08 +02:00
Scott Cazan 39406bbf54 Use published URL for site when displaying results for a Site space (#2364) 2024-07-02 16:14:39 +02:00
Scott Cazan 9c5c1e5b68 Show sources with no answer (#2363)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-06-28 15:25:01 +02:00
Johan Preynat 6929b0b98b Prevent SVG images to be resized and rendered on the /~gitbook/image endpoint (#2362) 2024-06-27 12:24:46 +02:00
Johan Preynat 7c26d491b1 Fix passing the correct nonce for iframely script and preload it (#2360) 2024-06-25 17:11:19 +02:00
Johan Preynat 97035a054a Tree shake unused code for Sentry (#2359) 2024-06-25 15:55:48 +02:00
Johan Preynat db45c6d773 Rework context to actually share memory cache between middleware and handlers for all requests (#2358) 2024-06-25 14:30:38 +02:00
Johan Preynat e3f5f81e62 Use fnv1a instead of SHA256 to create images signatures (#2353) 2024-06-24 13:31:18 +02:00
Johan Preynat f5df40e17d Use a singleton to store the memory cache in an internal WeakMap for the current request (#2356) 2024-06-24 11:13:03 +02:00
Johan Preynat e13c76efed Set preconnect links to main domains on render (#2354) 2024-06-21 17:02:02 +02:00
Johan Preynat 4a11d8d9eb Share memory cache between middleware and request handler (#2347) 2024-06-21 11:13:06 +02:00
spastorelli 009ed8cc6e Call trackViewInSiteById API endpoint when tracking view for sites (#2350) 2024-06-21 10:01:35 +02:00
vibhanshub 106cbbd987 Add support for examples in OpenAPI block (#2314) 2024-06-20 16:02:58 +05:30
Johan Preynat 56e5c3c3f8 Parse cache tags returned from the API set them in response (#2349) 2024-06-20 10:09:02 +02:00
Taran Vohra 1dc3234003 Fix page metadata title (#2348) 2024-06-18 20:52:12 +05:30
Johan Preynat 22dc0d4ff5 Fix passing revalidateBefore option when calling cacheResponse to ensure these go through background revalidation (#2342) 2024-06-17 17:16:12 +02:00
Taran Vohra 3a4caf062c Add getContentTitle to properly render the title for sites & legacy published content (#2343) 2024-06-17 20:28:21 +05:30
Taran Vohra 8ad0657159 Use site's share key as context while fetching site spaces (#2341) 2024-06-14 20:59:05 +05:30
Taran Vohra d8a4ecd0c6 Pass site url as context to listSiteSpaces API (#2339) 2024-06-14 10:40:18 +05:30
David Burghoff 65c7968438 Add German Locale support (#2317)
Co-authored-by: David Burghoff <david.burghoff@swot.de>
Co-authored-by: taranvohra <taranvohra@outlook.com>
2024-06-13 15:04:49 +02:00
Rodrigo Castro 3914f11150 Add Brazilian Portuguese translation (#2285)
Co-authored-by: taranvohra <taranvohra@outlook.com>
2024-06-13 14:54:47 +02:00
Addison d363908297 Add contributors to readme, bump API (#2333) 2024-06-12 15:24:18 +02:00
Greg Bergé 414556ac7a Remove cache client-side (#2337) 2024-06-10 11:17:48 +02:00
Steven H c768e9a90a Add another space to ignore list whilst we debug issues. (#2334) 2024-05-28 19:02:07 +01:00
Taran Vohra 553f1f228f Bump @gitbook/api to 0.48.0 (#2328) 2024-05-24 20:10:25 +05:30
Taran Vohra 30986528a7 bump @gitbook/api to 0.47 (#2327) 2024-05-24 19:07:54 +05:30
Steven H c70c2ddad8 Bump next-on-pages version to fix SSRF attack. (#2319) 2024-05-16 17:05:58 +01:00
Marc Laventure cf74d00758 chore: update scalar client (#2316) 2024-05-14 11:52:09 -07:00
Steven H c2256bfd14 Fix an issue where attackers could use the Nextjs image endpoint to redirect to any URL. (#2312) 2024-05-14 11:34:33 +01:00
fuyangpengqi 5d73a0f062 chore: fix some typos in README.md (#2290)
Signed-off-by: fuyangpengqi <995764973@qq.com>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-05-13 16:22:07 +02:00
Samy Pessé 8f5f6c61f8 Setup the pull_request workflow on actions (#2311) 2024-05-13 16:10:15 +02:00
Taran Vohra 9a86965ba1 Fix VA cookie to be only set if basePaths match (#2300) 2024-05-06 21:02:48 +05:30
Scott Cazan c8d5f825fc Update GitBook trademark (#2305) 2024-05-02 12:18:02 +02:00
Steven H 5db3cbc2ce Render Embed blocks that can be handled by an integration in the Space. (#2302) 2024-04-25 15:25:53 +01:00
Steven H f8d148bec4 Use plain code highlighting on some spaces that are currently crashing. (#2301) 2024-04-25 09:50:08 +01:00
Steven H 198dd9f158 Investigate an issue where images with 127.0.0.1 in the URL are not correctly resolved. (#2298) 2024-04-19 17:21:22 +01:00
Steven H 2b63bc158a Improve rendering of File column in tables. (#2297) 2024-04-19 16:58:46 +01:00
spastorelli 4a8097d968 Use site space context from token in multi-id mode when provided (#2295) 2024-04-19 16:51:12 +02:00
spastorelli f1440ca7ef Fix preview crashing when token do not include site data (#2292) 2024-04-18 17:36:16 +02:00
Taran Vohra 7ef2eeed13 fix site cache tag to exclude org id (#2291) 2024-04-18 20:28:39 +05:30
Taran Vohra 4005895337 Fix previews for sites in multi-id mode (#2289) 2024-04-18 18:19:52 +05:30
Steven H bf450e5cc7 Support GitHub-flavoured Markdown when parsing OpenAPI descriptions. (#2288) 2024-04-17 14:21:07 +01:00
Taran Vohra 4c12722c0a Use appropriate customization settings for sites (#2279) 2024-04-17 17:28:14 +05:30
Taran Vohra ac124a7ea5 Add e2e tests for site variants (#2286) 2024-04-16 20:59:58 +05:30
spastorelli 1de25056fa Fix space dropdown showing for single variant sites and update E2E tests (#2284) 2024-04-16 15:29:15 +02:00
Steven H bf3d2032ab Avoid crashing Cloudflare Worker when syntax highlighting too many lines of code. (#2283) 2024-04-15 11:57:22 +01:00
spastorelli bfb2342ab3 Add support for integration inject script for sites (#2277) 2024-04-11 10:28:24 +02:00
Samy Pessé 76cbc3c10f Print signature and url when having error with image resizing (#2275) 2024-04-09 11:22:36 +02:00
Taran Vohra c80c76d50d Override site's spaces published URLs (#2274) 2024-04-09 09:59:25 +05:30
Johan Preynat 263c89391e Add HSTS and other security headers (#2271) 2024-04-08 19:44:57 +02:00
Samy Pessé ebf742e81a Use a sampleRate of 0.1 for edge/node runtimes (#2273) 2024-04-08 15:05:01 +02:00
Samy Pessé 685317f508 Fix full-width for code-blocks and embeds (#2272) 2024-04-08 14:45:42 +02:00
Taran Vohra 0c224861e3 Sites support (#2262) 2024-04-08 17:06:22 +05:30
Steven H a007604ab1 Bump caches for documents. (#2265) 2024-04-04 14:09:43 +02:00
Steven H 3076e96061 Fix an issue where OpenAPI blocks could not be fetched. (#2267) 2024-04-04 12:28:30 +02:00
Steven H a3741dd65f Bump caches for revisions. (#2263) 2024-04-04 11:15:29 +02:00
Taran Vohra 07085a5b89 Set tracking prompt cookie age to 365days instead of a session cookie (#2266) 2024-04-04 14:39:27 +05:30
Samy Pessé 36600bc0c7 Fix styling for cards when no cover is defined (#2261) 2024-04-03 00:10:59 +02:00
Samy Pessé 5b7488b066 Handle ?theme query string to force theme in preview mode (#2260) 2024-04-02 13:55:27 +02:00
Samy Pessé c1ef7f2782 Fix display of parameter's description in OpenAPI block (#2258) 2024-04-02 00:35:59 +02:00
Samy Pessé f26ab0cf80 Remove mask on page cover (#2257) 2024-04-02 00:21:21 +02:00
Samy Pessé 7c570a008e Use a sample rate of 10% for Sentry errors (#2256) 2024-04-01 23:07:43 +02:00
Samy Pessé 1d71d85ed7 Don't test redirect on path that are longer than 512 (#2255) 2024-04-01 02:49:48 +02:00
Samy Pessé 4f728c0137 Fix parsing of Swagger 2.0 when it can be patched (#2254) 2024-03-31 20:13:34 +02:00
Samy Pessé 844a3e1845 Fix search in collection spaces leading to invalid urls (#2253) 2024-03-31 18:03:24 +02:00
Steven H 3144b2fec1 Update SyncedBlocks to use new API (#2251) 2024-03-29 15:57:08 +00:00
Samy Pessé 5805c24255 Fix copy for code blocks in OpenAPI or AI answers (#2250) 2024-03-28 22:46:29 +01:00
Samy Pessé d0ef225ea3 Make click on OpenAPI property name selects the entire text (#2249) 2024-03-28 22:44:03 +01:00
Samy Pessé 5f1d82cb5a Fix rendering of select in cards (#2248) 2024-03-28 20:53:01 +01:00
Samy Pessé 0894a740ff Fix crash when "required" is not an array in OpenAPI schemas (#2247) 2024-03-28 17:03:30 +01:00
Samy Pessé 14012d92f9 Fix cover image overflowing the card (#2243) 2024-03-27 18:20:16 +01:00
Samy Pessé 4be6eada62 Fix redirect to right page for content redirects (#2241) 2024-03-26 23:30:47 +01:00
Samy Pessé 0ddbe5c0ee Revert crossOrigin="anonymous" for images (#2240) 2024-03-26 23:19:20 +01:00
Samy Pessé 3a7864d731 Fix design of embed cards being centered (#2239) 2024-03-26 18:29:52 +01:00
Samy Pessé 73d806a796 Set z-index on annotation popover (#2238) 2024-03-26 16:46:19 +01:00
Samy Pessé d40568c8f4 Fix OpenAPI tag in aside when text is long (#2237) 2024-03-26 14:02:17 +01:00
Samy Pessé a6789513dc Fix alignment and border around small images in a grid with captions (#2229) 2024-03-26 13:45:23 +01:00
Samy Pessé ee7a74fe43 Fix VA redirects for nested paths (#2236) 2024-03-26 11:53:32 +01:00
Samy Pessé 0d99906774 Fix crash with concurrent loading of shiki syntaxes (#2235) 2024-03-26 11:30:38 +01:00
Greg Bergé d3f9131af4 Support images & files in synced blocks (#2234) 2024-03-25 21:53:24 +01:00
Addison 30bae7c877 Add legacy references to readme (#2233) 2024-03-25 13:49:39 +01:00
Samy Pessé 0c99a57b3c Also fix crossOrigin for images when zooming it (#2232) 2024-03-25 13:43:19 +01:00
Samy Pessé ad9e8f2232 Fix authentication displayed for images (#2231) 2024-03-25 13:15:19 +01:00
Greg Bergé 4c286b503c Fix fullPage screenshots (#2230) 2024-03-25 12:43:50 +01:00
Samy Pessé c5a28f884a Fix zoom on small images with custom zoom component (#2228) 2024-03-25 11:31:40 +01:00
Samy Pessé 2ed5445af8 Fix display of cover when TOC is hidden (#328)
* Add visual tests for page options

* Fix the case of a landing page

* Fix height of the aside when page has a cover

* Remove some visual tests
2024-03-22 23:46:35 +01:00
Samy Pessé 755291d1b7 Fix redirect for uppercased pathnames and fix og:image for collections (#327)
* Fix redirect for uppercased pathnames

* Try fixing it

* Try without metadataBase

* New try

* Fix it

* Try to fix og:image

* Generate canonical URL meta tag
2024-03-22 22:23:11 +01:00
Samy Pessé 7421d1c6c3 Cache getPublishedContentByUrl by tags (#326)
* Cache getPublishedContentByUrl by tags

* Reduce amount of cases returned by getURLLookupAlternatives

* Optimize lookup even more for revisions / change-requests

* Fix basePath for revisions / crs
2024-03-22 21:10:58 +01:00
Samy Pessé ff773233dd Prevent click on images in links to zoom it (#324)
* Prevent click on images in links to zoom it

* Fix TS
2024-03-22 16:56:08 +01:00
Samy Pessé 44983f8495 Minor perf improvements to code block highlighting (#323)
* Use a singleton map when loading languages

* typing

* Lint

* Remove one trace
2024-03-22 16:43:22 +01:00
Viktor Renkema 435ccb10ce Fix tab truncation behaviour (#322) 2024-03-22 12:18:49 +01:00
Amrit Kahlon ef8eb72364 fix: clear scalar api client response on close (#318)
* fix: clear scalar api client response on close

* bun install

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-03-21 14:53:29 +01:00
Samy Pessé 2a28f28421 Fix some links opening in new tab and links on blankslate (#321)
* Open links from TOC in same tab

* Fix page blankslate not resolving page links correctly
2024-03-21 14:11:42 +01:00
Samy Pessé be45f23cc8 Use whitespace-pre to fix whitespace in highlighted text in search (#320)
* Use whitespace-pre to fix whitespace in highlighted text in search

* Correctly fix it
2024-03-21 13:10:58 +01:00
Brett Jephson fff6dce8fc RND-3192: images expanding card content beyond their width (#281)
* Fix for RND-3192: images expanding card content beyond their width

* Wrap Image in a size-restricting container

---------

Co-authored-by: Viktor Renkema <vrenkema@gmail.com>
2024-03-21 12:46:46 +01:00
Samy Pessé cfcfbdf16c Fix text/image alignment in cards (#319)
* Add a visual test

* Fix test
2024-03-21 12:15:58 +01:00
Samy Pessé b029b1e23d Fix display of advanced blocks in OL/Tasks lists (#317)
* Fix OL list style

* Format

* Fix it for tasks lists as well

* Default to fullPage

* Run some test without fullPage

* Run fullPage only on list
2024-03-21 09:50:18 +01:00
Samy Pessé 1dc81fc56a Use absolute URLs in sitemap.xml (#316) 2024-03-20 23:58:57 +01:00
Samy Pessé 31799038dd Improve rendering of complex emojis in favicon (#315)
* Improve rendering of complex emojis in favicon

* Lint
2024-03-20 19:25:42 +01:00
Samy Pessé db4ecbc747 Fix long words in headings breaking the layout (#314) 2024-03-20 18:59:52 +01:00
Samy Pessé 0ad7bf561e Let users extend the limit of PDF generation (#313)
* Add link to extend PDF generation limit

* Translate

* Use variable
2024-03-20 18:49:28 +01:00
Samy Pessé dc2d50e455 Iterate over all keys in KV when purging cache instead of just first page (#312)
* Iterate over all keys in KV when purging cache instead of just first key

* Call getTagPrefix once
2024-03-20 18:48:17 +01:00
Addison 10fc27ba4e Add README, License & Contributing Guide (#153)
* Initial commit, format

* Use codeblocks instead of inline code

* Add header

* Add badge, update placeholder for hero image

* Update badges and structure

* Add license

* Update badges

* Add contributing guide

* Add deployment guide

* Add license section

* Update header image

* Add image of deployed site

* Update readme

* remove deployment buttons

* formatting

* Add types of contributions

* format

* Add review suggestions

* Update badges
2024-03-20 17:26:44 +01:00
Samy Pessé 2e6e96728c Fix logic to parse changeRequest URL in multi-id mode (#311) 2024-03-20 16:50:55 +01:00
Samy Pessé e1de340fc5 Support URL routes to export PDF for a change-request / revision (#310)
* Support more URL patterns for "multi-id" mode

* Format
2024-03-20 15:43:29 +01:00
Samy Pessé fc9238498d Only use GITBOOK_TOKEN when request is using default API (#309)
* Only use GITBOOK_TOKEN when request is using default API

* Format
2024-03-20 10:40:38 +01:00
Samy Pessé 2420f9b730 Return private cache-control when receiving prefetch request (#308) 2024-03-20 00:17:59 +01:00
Samy Pessé d12aad6867 Fix different flickering when navigating in docs (#307)
* Prevent scroll flickering when switching between page with scroll and no scroll

* Oups

* Remove id

* Apply only on large device and use variable

* Fix default
2024-03-19 20:51:16 +01:00
Samy Pessé 37be6f3164 Support options in the OpenAPI spec to control the look and feel (#306)
* Don't show code-samples if x-codeSamples is defined

* Support x-hideTryItPanel to hide "Try it"

* Fix display of code samples
2024-03-19 16:32:15 +01:00
Marc Laventure 4f039ecb2b feat: add scalar api client (#180)
* wip

* feat: add scalar api client

* fix: pr feedback

* feat: add scalar react client package, remove vueary

* feat: add scalar proxy

* Move dependencies to package

* Format once

* Rename prop

* Use react context and simplify lazy loading

* Add loading state

* Format

* Move styles

* bun install

* bun

* fix: remove extraneous api-reference package

* feat: make operation functional with spec in client

* fix: column bug and http request type wrapping

* fix: add scalar api reference package

* Format

* bun install

* fix: scrolling in scalar modal

* Lazy load operations data in component

* fix: change dependency from api-reference to oas-utils to reduce size

* bun install

* Log error in the worker proxy

* Cleanup

* Comment "mode"

* Without credentials and referrerPolicy

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
Co-authored-by: Amrit <amrit@hockey-community.com>
2024-03-19 15:32:54 +01:00
Samy Pessé 71e993d0ca Fix support for page redirects with uppercases (#305) 2024-03-19 14:37:45 +01:00
Samy Pessé 6695de7b79 Fix inline math not displayed with KaTeX (#304)
* Fix mathjax loading

* Fix inline math not being displayed

* Format
2024-03-19 12:39:53 +01:00
Samy Pessé cf958bacd1 Fallback to MathJax when KaTeX fails to compile math (#303)
* Start

* Use MathJax v3

* Format

* Improve font-size

* Lint

* Better handle loading

* Copy MathJax assets to public folder

* Attempt at using local assets for MathJax

* Use bun 1.0.31

* Try with bun 1.0.33

* Try fixing headers

* Simplify

* Simplify and not use suspense module

* Cleanup more

* Add test for Math

* Make math block scroll if needed

* Update packages/react-math/src/KaTeX.tsx

Co-authored-by: Greg Bergé <berge.greg@gmail.com>

* Use React.use

* Format

* Use exports

* Lint

* Disable cache in CI for lint

* Format

---------

Co-authored-by: Greg Bergé <berge.greg@gmail.com>
2024-03-19 11:34:53 +01:00
Samy Pessé 85a32fda4f Wrap RocketLoaderDetector in DOMContentLoaded (#302) 2024-03-18 19:35:31 +01:00
Samy Pessé 706aa42a6e Fix RocketLoaderDetector being encoded (#301)
* Fix RocketLoaderDetector being encoded

* suppressHydrationWarning

* Show alert at the top of the page
2024-03-18 19:14:54 +01:00
Samy Pessé 4dcca8b3ec Pass the nonce to the RocketLoaderDetector (#300) 2024-03-18 18:42:56 +01:00
Samy Pessé d0b92dd408 Improve text displayed in mention for anchor content refs (#287)
* Resolve anchor content reference to the block text for mentions

* Only apply it for mentions/etc, where text is needed

* Handle tabs/expandable

* Rename to resolveAnchorText
2024-03-18 18:16:53 +01:00
Greg Bergé 718452728f Detect Rocket Loader and advert about it on docs (#295) 2024-03-18 17:33:26 +01:00
Greg Bergé 9b6d34bdbc Remove screenshot from AI result (#298)
Not possible to have it stable
2024-03-18 15:53:41 +01:00
Greg Bergé 3c96bf84b5 Upgrade Argos (#296)
* Upgrade Argos

* Wait for cookie dialog, avoid fullPage if not needed

* Fix E2E

* Cookies dialog
2024-03-18 15:30:45 +01:00
Greg Bergé cc4a86cc23 Fix ordered list display (#294)
* Fix ordered list display

* Cover lists in E2E
2024-03-18 15:26:43 +01:00
Greg Bergé c87197bb98 Replace shikiji by shiki (#297) 2024-03-18 15:11:22 +01:00
Valentino Hudhra 69dcf75083 Align loading skeleton with content (#288) 2024-03-18 14:48:40 +01:00
Samy Pessé 66dcaf850f Ignore all errors for rendering integration blocks (#293) 2024-03-18 12:08:49 +01:00
Greg Bergé 5f70ed73b9 Fix page cover display (#291)
* Fix page cover display

Make it always looks good with the recommended size of 1990x480.

* Take maximum size on mobile

* Stabilize visual testing
2024-03-18 11:04:28 +01:00
Samy Pessé bda3080b94 Don't show "Last updated" when no createdAt/updatedAt (#292) 2024-03-18 10:09:05 +01:00
Samy Pessé 9f5c68acfe Fix passing ref to Link elements to make enter in search works (#290) 2024-03-18 09:41:15 +01:00
Samy Pessé df92c72164 Fix resolution of first page document when content starts with empty group (#289)
* Fix resolution of first page document when content starts with empty group

* Lint
2024-03-18 09:23:23 +01:00
Samy Pessé a27fa68ae6 Fix scroll to hash and update next to 14.1.3 (#286)
* Update next to 14.1.3

* Scroll to hash when url changes

* Simplify tests

* Lint and format

* Fix TS

* Dummy comment

* Try async function
2024-03-18 08:55:30 +01:00
Samy Pessé 9e8cde48d4 Fix URL.canParse failing on some browsers (#285)
* Fix call to URL.canParse failing on some browsers

* format
2024-03-15 21:04:05 +01:00
Steven H 9f75929269 Fix an issue where following a link to a different space would not update the ToC or header. (#284)
* Fix an issue where clicking links to other spaces would not update ToC/header

* Fix

* Remove dev text

* Use URL.CanParse for testing the external url

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-03-15 20:09:29 +01:00
Samy Pessé 5c7c17fe02 Fix OpenAPI parsing for Swagger 2.0 YAML anchors (#282) 2024-03-15 18:09:44 +01:00
Johan Preynat 66506143ef Redirect uppercase pages path to lowercase (#273)
* Redirect to lowercase pathname when page path has uppercase

* Also redirect in generateMetadata

* Rollback changes in middleware normalizeURL

* Improve argument type

* Only redirect in page component
2024-03-15 17:01:45 +01:00
Greg Bergé 50346135f9 Refactor image sizing logic (#278)
Fix RND-3140
2024-03-15 16:21:03 +01:00
Steven H f98146f5b6 Show a single page when user clicks Export as PDF (#279) 2024-03-15 15:08:41 +00:00
Steven H 88a7ab21a2 Revert a change in the way we await for local caches to update. (#277) 2024-03-15 14:03:00 +00:00
Steven H a4099dff9e Remove awaiting for local caches to set their values whilst we investigate hanging pages. (#275) 2024-03-15 13:47:31 +00:00
Steven H 98f91d9e82 Reduce local caches to 30s (#274) 2024-03-15 13:34:52 +00:00
Greg Bergé 4c2625c277 Fix memory cache usage, use the same value of 1 min for non immutable in local cache (#272) 2024-03-15 12:37:27 +01:00
Greg Bergé 77b6228677 Fix an issue where we were caching mutable cache entries indefinitely. (#270) 2024-03-15 11:22:15 +00:00
Johan Preynat ff93689d4e Rollbacking lowercasing paths since it breaks share links... (#271) 2024-03-15 12:01:58 +01:00
Steven H d8ffa5d69c Fix an issue where parsing an OpenAPI schema without securitySchemas would crash the page. (#263) 2024-03-15 10:27:23 +00:00
Samy Pessé f77b2b6cd7 Fix resolution of content-refs to pages in another space (#267)
* Fix resolution of content-refs to pages in another space

* Fix title of the content ref
2024-03-15 11:25:23 +01:00
Sebastian Graz 5dec1a1f0e Top links visibility container queries (#257)
* improve top link vis queries & footer style fixes

* add ellipsis to menu items

* Add spacing to menu items

* Remove weird shadow

* Format

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-03-15 11:24:47 +01:00
Johan Preynat e462ac837e Redirect URLs containing uppercase characters to lowercase (#266)
* Add tests for normalizeURL

* Redirect requests containing uppercase in pathname to lowercase via normalizeURL

* Fix typo
2024-03-15 11:24:41 +01:00
Taran Vohra a21673e4c1 use sameSite: none for va cookie (#265)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-03-15 15:52:10 +05:30
Johan Preynat c6537422a4 Use bun 1.0.30 for now in CI (#269) 2024-03-15 11:03:37 +01:00
Johan Preynat 654a9e6227 Increase getDocument timeout to 20s to prevent failing while the API is normalizing documents (#264) 2024-03-14 20:35:00 +01:00
Steven H 80d45c3ff7 Force light mode when exporting PDF. (#242) 2024-03-14 16:40:49 +00:00
Steven H 8c75bd364b Revert "Add w-full to consider Cards block (#261)" (#262)
This reverts commit 06b94d4525.
2024-03-14 15:42:06 +00:00
Viktor Renkema 06b94d4525 Add w-full to consider Cards block (#261) 2024-03-14 15:55:25 +01:00
Steven H e5a9d16d7e Mitigate long loading times on spaces with many files. (#260) 2024-03-14 11:41:11 +00:00
Samy Pessé d0eebceacf Batch request for getRevisionFile to avoid too many sub-requests (#259)
* Try approach with batching

* Improve it to support getRevision

* Fix

* Real fix

* Implement skip

* Lint
2024-03-13 18:10:07 +01:00
Steven H ee3221885c Remove Sentry trace/transaction (#258) 2024-03-13 16:35:18 +00:00
Greg Bergé 76dbf0f0d3 Tag synced block in API (#256) 2024-03-13 15:32:52 +01:00
Scott Cazan fc11ffdc8b Don't try to render responses if none are defined by the user (#255)
* don't try to parse responses if none are defined

* don't try to render responses if none exists

* move logic back into component
2024-03-13 14:37:21 +01:00
Steven H 9f83b0e783 Fix an issue where an incorrect cache key was used for revisions and pages, causing 404s and 500s. (#254) 2024-03-13 13:20:57 +00:00
Samy Pessé b0a9361cbd Disable redis cache (#253) 2024-03-13 13:36:38 +01:00
Samy Pessé e34b923e4a Correctly handle errors in blockFallback when inputs are running (#249)
* Correctly handle errors in blockFallback when inputs are running

* Format

* Fix case of steven
2024-03-13 12:54:49 +01:00
Samy Pessé cf4b1249a9 Optimize performances for reading revisions (#251)
* Optimize read of revisions with metadata=false when possible

* Format

* Fix

* Update src/components/AdminToolbar/AdminToolbar.tsx

Co-authored-by: Steven H <shne24@gmail.com>

---------

Co-authored-by: Steven H <shne24@gmail.com>
2024-03-13 12:54:32 +01:00
Samy Pessé 221d85cc4d Optimize caching of revisions/documents (immutable entries) in memory and cloudflare cache (#252)
* Better handle immutable cache entries in memory/cf-cache

* Format

* Update src/lib/cache/utils.ts

Co-authored-by: Steven H <shne24@gmail.com>

---------

Co-authored-by: Steven H <shne24@gmail.com>
2024-03-13 12:54:22 +01:00
Scott Cazan 658eb4d13f extra check for undefined in schema.items (#250) 2024-03-13 12:24:10 +01:00
Samy Pessé 3725239e92 Skip data: images for image resizing (#248) 2024-03-13 11:36:06 +01:00
Samy Pessé bdbd14c560 Fix next edge runtime not liking error in logs (#247)
* Fix next edge runtime not liking error in logs

* Fix other errors
2024-03-13 11:04:43 +01:00
Taran Vohra 0da4e9a926 Fix visitor auth logic by testing multiple basePaths (#239)
* WIP

* add search params to target in multi-path

* Simplify

* Format

* Ensure trailing slash

* Add unit test

* fix lint

* use search from target in redirect

* remove unused import

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-03-13 13:54:47 +05:30
Taran Vohra 1eb4bff09b Strip url search params when comparing in lookupSpaceByAPI (#246)
* strip url search while comparison in lookupSpaceByAPI

* strip URL search for the lookupURL
2024-03-13 13:44:44 +05:30
Samy Pessé 147719c19b Fix testing of VA in local development (#245) 2024-03-13 09:02:35 +01:00
Samy Pessé c13f676ea8 Fix content references to other spaces (#243) 2024-03-12 20:43:07 +01:00
Samy Pessé d326822b9d Pass token to getPublishedContentByUrl to ensure rate-limit settings are propagated (#244) 2024-03-12 20:33:28 +01:00
Samy Pessé 111ec45357 Fix env variable used for API token (#241) 2024-03-12 18:12:49 +01:00
Steven H e39e1db484 Fix an issue where track_view was failing. (#240) 2024-03-12 15:53:58 +00:00
Samy Pessé 6849df49ec Fix error with primary revision being fetched instead of specific one (#236) 2024-03-12 12:03:13 +01:00
Steven H a697dc4fcd Fix an error where a Theme nonce value could cause inconsistencies between server- and client-side rendering. (#237) 2024-03-12 10:09:14 +00:00
Samy Pessé a8d15555c8 Remove custom text selection color (#234) 2024-03-11 18:58:36 +01:00
Samy Pessé 7d53a2c849 Remove colors from HTML root and define them in the tailwind config (#233)
* Remove colors from HTML root and define them in the tailwind config

* Add visual test
2024-03-11 18:50:45 +01:00
Sebastian Graz 270f8b55a5 Hints (#232)
* V1 hint refactor

* checkbox fix

* Update src/components/DocumentView/Hint.tsx

remove comment

Co-authored-by: Samy Pessé <samypesse@gmail.com>

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-03-11 18:28:20 +01:00
Samy Pessé 56a9d5601b Fix cache revalidation in KV and output stats on revalidation (#231)
* Store setAt on cache meta and output stats on revalidation

* Delete from KV cache

* test more

* Improve

* Format

* Store the key in metadata

* Avoid storing too much in KV

* Don't put tag on resolve of url
2024-03-11 13:30:26 +01:00
Samy Pessé ea7ff33f3a Expose publicly cache tags with x-gitbook-cache-tag (#230) 2024-03-11 10:17:44 +01:00
Samy Pessé 642e33444a More immutable cache and pre-revalidation (#229)
* Try different approach

* Revalidate before cache expires

* TS

* Only replicate to local backends

* Ensure revalidate is done only once for the same key

* Improve perf of getCacheKey

* Simplify type

* Use better method and comment

* Use it 7 days only
2024-03-11 09:35:20 +01:00
Samy Pessé 0e858b1a43 Improve span name in sentry for cache (#228)
* Improve span name in sentry for cache

* Better use `trace`

* One more span

* Lint

* Log memory usage

* Reuse span between fetchValue and get
2024-03-09 14:31:23 +01:00
Samy Pessé 510a6f39d7 Fix revalidation and redis get (#226)
* Log

* Try to fix

* Use automaticDeserialization

* Log errors

* Ignore AbortError
2024-03-08 16:38:43 +01:00
Samy Pessé f9e3835c2f Simplify redis cache and remove hits count (#225)
* Simplify redis cache and remove hits count

* Fix naming

* Update src/lib/tracing.ts

Co-authored-by: Steven H <shne24@gmail.com>

---------

Co-authored-by: Steven H <shne24@gmail.com>
2024-03-08 13:05:32 +01:00
Greg Bergé 5a5b4bb93c Replace Puppeteer by Playwright (#224)
* Replace Puppeteer by Playwright

* Remove the limit of workers on ci

* Various fixes after review

* Document things
2024-03-08 10:41:30 +01:00
Samy Pessé 55d1e538d9 More resilient caching and use of KV (#222)
* Add unit tests for "race"

* Reduce to 80s

* Implement a Cloudflare KV cache

* Format

* Lint

* Fix tagging of entries in KV

* No longer use HTTP cache tags

* Simplify tags to only keep 2

* Implement a blockFallback logic

* Improve the block fallback logic

* More tests

* Start introducing a always optional signal on all cache ops

* Correctly pass signal end to end

* Fix one more case and add tests

* Fix it

* Add test for error

* Fix error handling

* Simplify even more

* Improve logs

* Improve logs / measurements

* Fix timing

* Fix read cache duration and add minor tests

* Log redis time

* Change replication logic

* Fix redis errors

* Log more

* More logs and test memory first

* Improve tracing for cache backends

* Ignore all dependencies

* Log the key in the cache low level traces
2024-03-08 09:43:30 +01:00
Greg Bergé 7d04336b5f Support synced blocks (#223) 2024-03-08 09:16:01 +01:00
Samy Pessé 79f420eb8e Add timeout to cache get (#221)
* Add timeout to cache get

* Reduce it to 200ms
2024-03-06 20:12:47 +01:00
Samy Pessé 2742d961ee Remove feedback toast (#217) 2024-03-06 18:27:25 +01:00
Samy Pessé f41b0c7ba1 Fix crash when resolving ref fails in an OpenAPI file (#220)
* Fix crash when resolving ref fails in an OpenAPI file

* Use static.gitbook.com for assets prefix
2024-03-06 18:09:34 +01:00
Samy Pessé 77527f0a4e Fallback for Intl.RelativeTimeFormat for old browsers (#219) 2024-03-06 17:08:09 +01:00
Steven H 9436549510 Fix an issue where ordered list items were not respecting the start property from the API. (#218) 2024-03-06 14:51:19 +00:00
Taran Vohra 76d6a2009e Fix visitor auth token lookup from request cookies (#215)
* Fix visitor auth token lookup from request cookies

* review
2024-03-06 15:34:25 +05:30
Samy Pessé 46c3ec6699 Add fallback to the different fonts (#216) 2024-03-06 10:59:43 +01:00
Samy Pessé 203e9103de Fix crash when displaying invalid Swagger 2.0 block (#214)
* Ignore errors when converting Swagger 2 to OpenAPI 3

* Apply page-api-block only when a block is actually displayed

* Format

* Try to be more lax
2024-03-06 10:43:11 +01:00
Samy Pessé d22944419f Prevent crash when integration associated to a block is no longer installed (#213)
* Prevent crash when integration is not installed for a block

* Format
2024-03-05 20:24:51 +01:00
Samy Pessé a9f9f18e66 Fix crash when resolving references without permissions (#212) 2024-03-05 19:45:38 +01:00
Samy Pessé fdaf722ba1 Align table cells to the middle (#211) 2024-03-05 18:17:32 +01:00
Samy Pessé 6c64841325 Use hardlinks instead of Next link for collection dropdown (#210) 2024-03-05 18:01:59 +01:00
Samy Pessé 44f4afdf94 Improve content reference resolution to support absolute ones to spaces (#209) 2024-03-05 17:39:24 +01:00
Samy Pessé a22bb55715 Do not crash on invalid LaTeX formulas (#208) 2024-03-05 16:16:29 +01:00
Samy Pessé 44945f887a Fix crash when no example can be generated for response of API (#207) 2024-03-05 15:51:40 +01:00
Samy Pessé cf20c5ff3b Output cache-control only when env is set (#206) 2024-03-05 15:46:08 +01:00
Samy Pessé c8935c62c8 Fix code block highlighting crashing when containing \r (#205) 2024-03-05 11:15:58 +01:00
Samy Pessé 1e1d202b5a Improve PDF export for tabs/expandables/api blocks (#204)
* Fix integrations blocks and text being too long

* Fix size of code blocks

* Open expandable by default on print mode

* Render tabs one after the other

* Adjust colors

* Open all in openapi when printing

* Fix title

* Format

* Fix TS
2024-02-29 19:49:03 +01:00
Samy Pessé d304c853fb Fix header logo (#203) 2024-02-29 14:46:56 +01:00
Samy Pessé 7278d7868d Translates and typing as safe-guard (#198)
* Translates and typing as safe-guard

* Add tests for intl
2024-02-29 12:09:53 +01:00
Samy Pessé cb91fb9df8 Fix access to cookies when used in iframe (#202)
* Fix access to cookies when used in iframe

* Allow https: embed
2024-02-29 11:24:55 +01:00
Samy Pessé 826ddbc533 Improve preview URL by better storing the api endpoint and the token in cookies (#201)
* Use one cookie per content and store api host in it

* Fix fallback to api endpoint

* Ignore parsing error
2024-02-29 10:46:41 +01:00
Samy Pessé 10375bae51 Add tracing to cache operations to debug I/O issue (#200)
* Change global context used to "cf"

* Add traces to cache operations
2024-02-29 10:19:57 +01:00
Samy Pessé f26e856170 Accept customization settings as a query paremeter (#199)
* Accept a query parameter "customization" to extend space customization

* Test it

* Use rison instead of JSON

* Use object version
2024-02-28 19:24:48 +01:00
Samy Pessé 2df5d68eae Pass next.internal on fetch requests to avoid I/O sharing (#197) 2024-02-28 15:26:36 +01:00
Samy Pessé 70c6c9df42 First pass at implementing ContentKit (#69)
* Start

* Try webframe

* Lint and format

* Move to package

* Continue

* Implement card and cleanup contexts

* Start code blocks

* Start rendering image

* Continue with card

* Start stack

* Start text

* Start modal

* Format

* Continue

* Fix TS errors

* Cleanup

* Make markdown work with server components

* Format

* Add visual test

* Lint and format

* Fix lifecycle

* Implement textinput

* Divider

* Remove console logs

* Remove listSpaceIntegrationsBlocks
2024-02-28 14:46:47 +01:00
Samy Pessé 8eb2850bdd Fallback crypto.randomUUID for old browsers (#196)
* Fallback crypto.randomUUID for old browsers

* Oups
2024-02-28 14:09:31 +01:00
Samy Pessé 887e99c8ed Archive build output on GitHub Actions 2024-02-28 10:52:41 +01:00
Samy Pessé 46b3e461dd Fix code block highlighting resulting in shared promises (#195)
* Fix code block highlighting resulting in shared promises

* Lint

* Fix tests
2024-02-28 10:44:47 +01:00
Samy Pessé 76ba5d043e Use customization.title instead of space.title (#194)
* Use customization.title instead of space.title

* Format

* Fix TS
2024-02-28 10:26:04 +01:00
Samy Pessé 5e12c0a9be Ignore errors when tracking page views (#193) 2024-02-27 16:27:09 +01:00
Samy Pessé 7ed89fe0fe Ignore errors with animate calls (#192) 2024-02-27 11:48:40 +01:00
Samy Pessé a3af78031f Use .ctx to avoid issue with proxy object (#191) 2024-02-27 11:15:58 +01:00
Samy Pessé 05de8109bb Fix crash for some code blocks with annotations (#190)
* Start

* Add failing test

* Better handle nested tokens

* Run tests in CI

* Comment

* Fix another case
2024-02-27 11:03:51 +01:00
Samy Pessé 41278bad68 Fix responsiveness of API blocks (#189)
* Add unit test

* Fix responsiveness of API blocks with 2 columns
2024-02-24 17:14:06 +01:00
Samy Pessé 884f5aabb0 Fix text overflowing on long words (#188)
* Break words when overflowing

* Format and add tests
2024-02-24 17:00:11 +01:00
Samy Pessé eea1323c17 Don't show aside when there is only one aside (#187) 2024-02-24 15:17:53 +01:00
Samy Pessé 341264e879 Fix page not found and main layout (#186)
* Switch to a global layout

* Format and improve main layout

* Fix TS

* Real fix

* Cleanup
2024-02-23 22:53:02 +01:00
Taran Vohra 345a2ea964 log request cookies in the middleware (#184) 2024-02-23 19:44:50 +05:30
Samy Pessé 56143277b7 Expose window.GitBook to integrations (#185)
* Expose window.GitBook to integrations

* Simplify

* Oups

* Use basic script to load integrations

* Preload the scripts
2024-02-23 13:32:09 +01:00
Samy Pessé aeeb9b0305 Hide table header when column titles are empty (#182)
* Hide table header when column titles are empty

* Add test

* Skip downloading puppeteer for some CI jobs
2024-02-22 13:23:56 +01:00
Taran Vohra 64bb5ebf5a Redirect after normalizing URL for VA (#183)
* Redirect after normalizing URL for VA

* Redirect after normalizing URL for VA
2024-02-22 17:31:41 +05:30
Taran Vohra 98d44f01c7 Do a redirect if visitor auth token found in param (#181)
* redirect if visitor auth token in param

* add resolved cookies to redirect
2024-02-22 16:41:43 +05:30
Samy Pessé 1fc9f17f76 Use correct colors for contrast header theme (#179) 2024-02-21 00:22:07 +01:00
Samy Pessé 9a25c8c19c Fix size of cards in table (#178) 2024-02-20 19:01:41 +01:00
Samy Pessé 09de40002a Fix share links resulting in errors because of (#177) 2024-02-20 14:29:14 +01:00
Samy Pessé a2d967e98e Use Joypixels instead of native emojis (#176)
* fist test

* Use a font

* Cleanup

* Use right fallbacks for fonts

* Lint

* Cleanup and comment

* Add visual test
2024-02-20 13:54:57 +01:00
Taran Vohra 258ef413b1 Fix logic to fetch visitor auth token from cookies and cleanup URL params (#175)
* Remove va token from query params before using it for response

* sanitize
2024-02-20 17:11:23 +05:30
Samy Pessé cf1f281119 Fix color of the title in the header when theme is custom (#174) 2024-02-20 09:14:04 +01:00
Samy Pessé 773fa50d41 Fix H# in aside and link to expandables (#173)
* Set ID on expandables

* Do not include h3

* Add a hash link

* Format

* Add tests
2024-02-19 20:00:18 +01:00
Samy Pessé b0725eb852 Fix CI deployment config 2024-02-19 16:36:06 +01:00
Samy Pessé 6bd6ddd031 Use an assetPrefix to serve all assets from same URLs (#172)
* Test an assetPrefix

* Fix condition

* Use the env instead

* Use it for CSP
2024-02-19 16:32:33 +01:00
Samy Pessé 9b285625d9 Hide page footer navigation when disabled in customization (#171) 2024-02-19 15:24:43 +01:00
Taran Vohra 8c0970d3d9 Ensure unique and isolated visitor auth cookie state (#169)
* Ensure unique and isolated visitor auth cookie state

* Fix typecheck

* Fix typecheck
2024-02-19 18:18:02 +05:30
Samy Pessé 1ad9bfd6f3 Fix image size in some cases (#170) 2024-02-18 21:28:11 +01:00
Samy Pessé 1a37b17a68 Implement color mark (#168)
* Define text and background colors

* Handle dark mode

* Add content tests
2024-02-18 00:21:50 +01:00
Samy Pessé c5455d4498 Display page emoji in content-ref blocks (#167) 2024-02-17 21:23:22 +01:00
Samy Pessé a82c548f42 Fix image size and alignment (#166)
* Fix image alignment

* Fix size of inline image

* Correctly size images
2024-02-16 18:58:59 +01:00
Sebastian Graz eead1dba66 add heading hash order conditional (#165)
* add heading hash order conditional

* v1 api color pass

* Delete .env

* Format

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-02-16 10:29:18 +01:00
Samy Pessé a7d4896484 Process all pages to fetch the list of spaces in a collection (#164) 2024-02-15 17:32:20 +01:00
Samy Pessé fa734fb217 Fix border radius for tabs and code-blocks when using straight corners (#163)
* Fix border radius for tabs and code-blocks when using straight corners

* Format
2024-02-15 15:37:18 +01:00
Samy Pessé 46a1b58d01 Fix responsiveness of footer (#162) 2024-02-15 12:54:29 +01:00
Sebastian Graz 12520ccede Style sprint 4 (#160)
* Commit

* make light base lighter, improve table legiblity

* improve hover leg on toc

* improve tabs leg

* improve tab design

* improve expandable leg

* slight global color tweak

* hide debugger
2024-02-15 11:50:04 +01:00
Samy Pessé ebdec4966b Use absolute URLs for logos (#161) 2024-02-15 10:06:05 +01:00
Samy Pessé 0b620640fa Fix hiding the table of content when disabled in page options (#159)
* Hide TOC when page is configured to hide it

* Improve layout in different cases
2024-02-14 17:19:07 +01:00
Samy Pessé 06fb56edc7 Fix emoji not displayed in cards with target (#157)
* Fix emoji not displayed in cards with target

* Only have hover effect on card when clickable

* Use 4 instead of 2
2024-02-14 15:42:20 +01:00
Samy Pessé 49a1561801 Update @cloudflare/next-on-pages to 1.9.0 (#156) 2024-02-14 12:35:39 +01:00
Samy Pessé b2acfab7c0 Enable zoom on drawings (#155) 2024-02-14 12:00:46 +01:00
Samy Pessé cf193f1738 Fix size of logo in header (#154)
* Fix sizing of logo in header

* Format

* Fix cases where size is not passed
2024-02-14 11:17:29 +01:00
Samy Pessé 6edd2648a4 Fiz zoom causing inline image to be displayed as block, also fix dark mode (#152)
* Fix inline image being displayed as blocks

* Make it work on dark mode

* Format
2024-02-13 22:12:28 +01:00
Samy Pessé ec1769fc32 Let users zoom images (inline and block) (#151) 2024-02-13 21:54:48 +01:00
Samy Pessé e9432c9486 Fix default size for inline images (#150)
* Fix default size for inline images

* Lint
2024-02-13 21:08:11 +01:00
Sebastian Graz c3d9e85823 Style sprint 3 (#136)
* first pass at nested ordered list symbols

* remove margin inside lists

* V1 of nested bullets & fix header hash pos

* clean up

* convert listItem render to switch

* Improve lists

* improve heading hash

* fix list todos to match heading size too

* no need for inline-grid

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-02-13 21:03:57 +01:00
Samy Pessé 3e2327c707 Change refiner feedback form URL 2024-02-13 19:01:41 +01:00
Samy Pessé f2e5ad97ab Show button to share feedback about GitBook Open when in preview mode (#149)
* Show button to share feedback

* Lint
2024-02-13 18:27:48 +01:00
Samy Pessé 45dd16b146 Improve caching: reduce browser caching, optimize perfs of worker cache (#148)
* Disable browser caching

* Improve cache read

* Fix

* Try using it for lookup

* Read before increment in redis

* Don't use multi

* Limit cache to 2min in cloudflare

* Cache in cloudflare but not in the browser
2024-02-13 16:09:38 +01:00
Samy Pessé 9e5f717fbe Fix Bearer schema in code sample (#147) 2024-02-13 11:01:33 +01:00
Samy Pessé be46c5db4b Fix error "Cannot perform I/O on behalf of a different request" (#146)
* Use reliable cloudflare context

* Format

* Comment
2024-02-13 10:06:58 +01:00
Samy Pessé 97dafa5580 Make match background a customization option (#134)
* Add classname

* Start applying it

* reset existing style changes in favor of global setup

* setup v1 plain mode

* fix name clash

* move light/dark bases as vars

* side image render fix

* customize individual light tiers for light mode

* Add comment about primary and base

* remove style prop from image

* w-unset not needed on image

---------

Co-authored-by: sebastiangraz <graz@live.se>
2024-02-12 21:03:49 +01:00
Samy Pessé f62656ed9f Persist token in multi-id mode in a cookie (#144)
* Persist token in multi-id mode in a cookie

* Format
2024-02-12 16:53:04 +01:00
Samy Pessé 46a3543a5e Use new version of cloudflare/next-on-pages and remove patch (#145)
* Use new version of cloudflare/next-on-pages and remove patch

* TS

* Fix ts again

* Remove patch-package

* Remove test

* Make it work locally
2024-02-12 16:38:33 +01:00
Samy Pessé 2470a40531 Only catch GitBookAPIError in getPublishedContentByUrl (#143) 2024-02-09 21:40:10 +01:00
Samy Pessé 34e339b5e4 Correctly handle errors from getPublishedContentByUrl (#142)
* Correctly handle errors from getPublishedContentByUrl

* Format

* Improve error message
2024-02-09 19:24:36 +01:00
Samy Pessé dfb8d4e8d4 Normalize the URL by redirecting (#141) 2024-02-09 12:59:33 +01:00
Samy Pessé 4826c9e012 Fix search button in compact mode (#140) 2024-02-09 11:56:57 +01:00
Samy Pessé 6441f8a05e Improve placeholder in search button and input (#139)
* Improve search placeholders

* Improve perfs of AI search

* Increase back font-size

* Format
2024-02-09 11:25:03 +01:00
Samy Pessé 6cab31faf6 Stream AI answers to questions (#138)
* Stream AI answers to questions

* Use a different polyfill

* Revert "Use a different polyfill"

This reverts commit b8474c56301faf3180eebf81f697703c31447500.

* Update api

* Patch event-source-polyfill to support Cloudflare

* Patch the min version

* Update api client and remove eventsource polyfill

* Cleanup
2024-02-08 15:53:05 +01:00
Samy Pessé 1c0ea074b3 Update API client and fetch API blocks in new format only (#137) 2024-02-07 18:19:30 +01:00
Sebastian Graz caa74cf48f Style sprint 2 (#135)
* fix tab multiline

* OpenAPI improvements (#127)

* Fix display of authentication infos

* Fix property names

* Fix body / request starting with oneOf

* CSS fixes

* Show description

* Format

* Fix code samples

* Convert swagger 2.0 to OpenAPI v3 (#128)

* Convert swagger 2.0 to OpenAPI 3

* Format

* Fix page with one api block shown as empty (#129)

* Fix page with one api block shown as empty

* Format

* Style sprint (#126)

* V1 code blocks fix

* equalize modal text label + fix safari bug with fixed position stroke

* code block adjustments

* fix search result spacing

* remove metal + adjust light and dark mode

* remove metal from tw config

* Add colordebugger + remove unused global colors

* correct opacities on all colors

* first part of cleaning up colors

* fix tab colors

* search modal color changes

* adjust global colors

* expandable + code color changes

* add delay to loading panes

* adjust globals

* improve select table type

* improve light mode checkbox

* fix delay

* content

* uncomment debug

* expandable fixes

* OpenAPI improvements (#127)

* Fix display of authentication infos

* Fix property names

* Fix body / request starting with oneOf

* CSS fixes

* Show description

* Format

* Fix code samples

* Fix empty API responses not being displayed (#130)

* Fix response not being displayed when contains no body

* Fix padding

* Resolve any JSON/YAML OpenAPI file and resolve common parameters (#132)

* Fix parsing of OpenAPI when content-type is unknown

* Resolve common parameters

* Format

* Add support for straight corners (#131)

* Add class to body and tailwind variant

* Use it on the search button

* Use it for buttons and cookies banner

* Use it for cards

* Use it in more places

* Lint

* More

* page feedback

* theme toggler

* Rename Authentication to Authorization (#133)

* remove unused color light-5

* fix dupe css

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-02-06 16:25:02 +01:00
Samy Pessé c829555f1b Rename Authentication to Authorization (#133) 2024-02-02 10:20:30 +01:00
Samy Pessé 53299ce829 Add support for straight corners (#131)
* Add class to body and tailwind variant

* Use it on the search button

* Use it for buttons and cookies banner

* Use it for cards

* Use it in more places

* Lint

* More

* page feedback

* theme toggler
2024-02-02 10:16:15 +01:00
Samy Pessé 260305759d Resolve any JSON/YAML OpenAPI file and resolve common parameters (#132)
* Fix parsing of OpenAPI when content-type is unknown

* Resolve common parameters

* Format
2024-02-01 22:29:28 +01:00
Samy Pessé b5e6673976 Fix empty API responses not being displayed (#130)
* Fix response not being displayed when contains no body

* Fix padding
2024-02-01 10:30:03 +01:00
Sebastian Graz e5fc5c2d2a Style sprint (#126)
* V1 code blocks fix

* equalize modal text label + fix safari bug with fixed position stroke

* code block adjustments

* fix search result spacing
2024-01-31 23:20:09 +01:00
Samy Pessé a8379be4c1 Fix page with one api block shown as empty (#129)
* Fix page with one api block shown as empty

* Format
2024-01-31 16:11:08 +01:00
Samy Pessé 75876474a2 Convert swagger 2.0 to OpenAPI v3 (#128)
* Convert swagger 2.0 to OpenAPI 3

* Format
2024-01-31 15:46:42 +01:00
Samy Pessé f8fde8cec8 OpenAPI improvements (#127)
* Fix display of authentication infos

* Fix property names

* Fix body / request starting with oneOf

* CSS fixes

* Show description

* Format

* Fix code samples
2024-01-31 14:36:20 +01:00
Samy Pessé 4bbd86aeca Render drawing blocks (#125)
* Improve rendering of block with captions

* Remove redirect test
2024-01-30 18:30:27 +01:00
Samy Pessé aeab122d54 Use flex and simplify code block styling to fix safari issues (#124) 2024-01-30 12:10:34 +01:00
Samy Pessé 85ec1884e6 OpenAPI blocks (#74)
* Start methods to resolve refs

* bun

* Start package

* Add code to resolve ref

* Continue

* Deref

* Start monorepo

* Continue

* Fix ref resolver

* Style a bit more

* Start toggling section

* Continue

* Start displaying query/path/headers params

* Hide respo se if empty

* Fix recursive refs

* Simplify interactive and styling

* Display enums

* Format

* Improve type name being displayed

* Improve naming

* Render oneOf/anyOf/allOf

* Handle circular references

* Start variable in server url

* Use client component for the spec part

* Improve CSS sizing

* Display open api blocks in aside

* Make aside an overlay

* Improve stickiness of openapi

* Align document on the left when api page

* Improve general layout

* Better align

* Fix padding in aside

* Use syntax highlighting

* Show sample of response

* Improve code generation

* Improve code generated

* Format

* Format

* Rename to OpenAPI

* Skip deprecated properties and handle additionalProperties

* Use discriminator for naming

* Better name enum

* Make entire header toggeable

* Format and lint

* Add curl example

* Improve curl

* style pass

* Start securities

* Bun

* Fix TS

* Improve label

* Start markdown

* Improve code samples

* Test

* Use custom skeleton for api block

* Format

* Add support for redocly code samples

* Fix api blocks in aside

* Use typography for markdown

* Format

* Fix spacing in markdown

* Render headers

* Format

* Format

---------

Co-authored-by: Sebastian Graz <graz@live.se>
2024-01-30 10:15:58 +01:00
Samy Pessé 86a436645a Increase font-weight of code mark in headings (#123) 2024-01-27 22:11:27 +01:00
Samy Pessé 0096947c72 Fix font-size and line-height of code text mark (#122) 2024-01-26 12:42:31 +01:00
Samy Pessé ee35796576 Lower traces sample rate on Sentry to 1% (#121) 2024-01-26 11:32:24 +01:00
Samy Pessé 7045675eda Fix soft-break in the document (#120) 2024-01-26 11:08:39 +01:00
Samy Pessé 730b444403 Improve cache invalidation (#119)
* Limit revalidation and limit memory cache to 5 min

* Format

* Return keys for memory cache invalidation
2024-01-25 15:44:28 +01:00
Sebastian Graz d4e3c6d597 v1 (#117) 2024-01-24 17:52:12 +01:00
Samy Pessé f1f91a6816 Fix opening search results (#118)
* Switch to nuqs

* Navigate post url update

* Fix

* Fix AI search
2024-01-24 17:25:01 +01:00
Samy Pessé b5986e833a Fix optimized image size when using inline image (#116) 2024-01-24 14:02:16 +01:00
Samy Pessé eabc46b86e Start using next-themes (#115)
* Start using next-themes

* Fix with nonce

* Lint
2024-01-24 11:39:59 +01:00
Samy Pessé aec74d68fd Stop using shared promises (#114)
* Try cf.cacheTags

* Remove shared promises

* Use a pendings set per context
2024-01-23 23:19:23 +01:00
Sebastian Graz 22a68a71c4 Style PR (#101)
* fix card clickthrough

* better checkbox empty states

* add card bg

* fix keynav styles search

* add v1 style for search scope

* scope mobile defensive css

* Update src/components/Search/SearchResults.tsx

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-01-23 21:40:03 +01:00
Samy Pessé 9d9040f24e Fix error for redirects/files on change-requests (#113) 2024-01-23 21:38:28 +01:00
Samy Pessé 613d67c14e Limit Cloudflare data cache to 5 minutes (#112) 2024-01-23 18:28:20 +01:00
Samy Pessé 5f7b29bbc8 Improve purging of cache and return keys (#111)
* Use input url instead of request url

* Improve cache purging

* Lint

* Fix TS

* Add method to delete from cloudflare

* Clear on all backends

* Fix

* Add global cloudflare tag
2024-01-23 18:21:46 +01:00
Samy Pessé 756b5a766f Setup Sentry for errors (#110)
* First try

* Lint and format

* Use edge runtime

* Use envs for sentry

* Fix type

* Fix upload of sourcemaps

* Try fixing monitoring calls

* Fix gitbook/monitoring in middleware

* test error in middleware

* Define context on sentry data

* Set SENTRY_ENVIRONMENT to preview or production

* Format

* Pass url and userAgent

* Remove test cases

* Disable debug

* Remove ignoreBuildErrors
2024-01-23 14:36:56 +01:00
Samy Pessé 29f2177357 Close table of content when clicking on a link (#109)
* Close table of content when clicking on a link

* Lint
2024-01-23 10:48:25 +01:00
Samy Pessé 0e4ba13065 Show page emoji if any (#108)
* Show page emoji if any

* Format
2024-01-20 23:41:28 +01:00
Samy Pessé 05f206a7a5 Use an input for the search input to scroll value when long (#106) 2024-01-20 21:46:58 +01:00
Samy Pessé e660432227 Add support for table text column alignment (#107)
* Align text in table based on set alignment

* Format
2024-01-20 21:46:35 +01:00
Samy Pessé 56b8b86f58 Correctly align logo to the left (#105) 2024-01-20 18:37:21 +01:00
Samy Pessé da215347f4 Add aria-busy to loading components (#95) 2024-01-20 18:34:52 +01:00
Samy Pessé ec075af455 Store the build version in the generator meta tag (#104)
* Store the build version in the generator meta tag

* Format

* Send header with version

* Add header on 404 as well
2024-01-20 12:43:59 +01:00
Samy Pessé 1f7530aaf8 Set a user-agent for the API (#103)
* Pass user-agent and allow configuring it with envs

* Format
2024-01-19 14:55:04 +01:00
Samy Pessé 8c44653292 Add back last modified date in the footer of the page (#102) 2024-01-18 23:13:49 +01:00
Samy Pessé a6f7a71af9 Fix right click to save images (#100) 2024-01-17 12:19:33 +01:00
Samy Pessé b16636c180 Only show table progress on browsers that supports it (#99) 2024-01-17 12:18:31 +01:00
Samy Pessé 64a9e27ca7 Resolve revision and change request URL correctly (#98)
* Add tests for getURLLookupAlternatives

* Match correctly revisions and changes URLs

* Use revision and changerequets ID passed from lookup

* Prevent indexing these urls

* Start

* Start rendering toolbar

* Update api client

* Format

* Format

* Mock resolve of snippet

* Add test for revision
2024-01-15 09:42:38 +01:00
Samy Pessé 3c44eb4f80 Fix resolving redirect by encoding path (#97) 2024-01-12 21:37:14 +01:00
Samy Pessé 6e5208d3ae Fix handling of special chars in the URL (#96)
* Fix handling of special chars in the URL

* Add log

* More logs

* Test encoding

* Cleanup
2024-01-12 21:18:23 +01:00
Samy Pessé 3c2dbb7b10 Fix crash on invalid selectors (#94) 2024-01-12 17:45:27 +01:00
Samy Pessé 59a8e74e39 Remove useless console.log (#93) 2024-01-12 16:55:09 +01:00
Samy Pessé 2a1e491fee Show loading indicator when loading first results in search (#92)
* Show loading indicator when loading first results in search

* Fornat

* Format
2024-01-12 15:40:46 +01:00
Samy Pessé 7d60c75dd8 Allow seo indexation of x-gitbook-search-indexation (#91) 2024-01-11 18:33:46 +01:00
Samy Pessé f4ac246a09 Redirect to original image when cloudflare resize is returning an error (#90) 2024-01-09 14:05:32 +01:00
Samy Pessé e7c184cc99 Search across all spaces in a collection (#89)
* Start buttons

* Run the search globally

* Improve look and feel

* Improve text

* Format

* Fix TS
2024-01-08 19:23:12 +01:00
Samy Pessé 28be5aaeca Fix resolution of URL for collection URLs (#88) 2024-01-07 23:19:05 +01:00
Samy Pessé 5f6ca7af56 Hide trademark when disable in customization settings (#87)
* Hide trademark when disabled in customization

* Fix padding in toc

* Run upload of artifacts when failed
2024-01-07 13:55:01 +01:00
Samy Pessé 46a726f3a8 Fix visual tests being unreliable (#86)
* Test

* Upload failed tests as artifacts

* Format
2024-01-06 19:11:30 +01:00
Samy Pessé a1fca33947 Switch to <a> instead of next Link for aside (#85) 2024-01-05 17:51:10 +01:00
Sebastian Graz ccfcaa590b Style PR (#84)
* fixes for table values

* color tweaks rating

* inc legibility on aside active

* add tab separators

* fix hint icon

* fix header colors & remove custom balance

* Better mobile burger colors

* remove border from logofallback img

* style divider dark mode

* compact header + file + expandable hover + mt mobile nav

* testing file types
2024-01-04 18:43:37 +01:00
Samy Pessé 87e67866a4 Fix condition to render document being reversed (#83)
* Fix condition to render document

* Format
2024-01-02 18:31:15 +01:00
Samy Pessé 0c80f96579 Show grid of sub-pages when page is empty (#82)
* Add a blankslate when the page has no document

* Format
2024-01-02 17:55:43 +01:00
Samy Pessé 22eeaca92c Add support for all column types (#81)
* Support files and content ref

* Display all column types

* Pass width

* Format
2024-01-02 10:43:52 +01:00
Sebastian Graz 66ba1c244f Styling PR (#80)
* add BR to TOC links

* fix sticky TOC padding

* increase searchbar contrast

* align toc and aside

* copy on hover only

* nicer tabs

* more dynamic table sizes

* dynamic expandable treatment

* skeleton v1

* modal spacing + close modal on source click

* fix page link icon TOC

* add PDF separate spacing

* fix TW Intellisense + further styling of loadingpane

* properly style skeletons

* better loading panels, AI search enhancement while waiting

* Commit

* Use the same state for answer

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-01-02 09:09:01 +01:00
Samy Pessé 0668d8a287 Track page view (#42)
* Track page view using a server action

* Track the page view

* Format

* Track it once

* Support no page an pass language

* Update API client
2023-12-29 15:28:49 +01:00
Samy Pessé 2fdf3e3838 Progressive loading (#79)
* Suspense boundary and progressive loading on blocks

* Start refactoring to better leverage app router

* Use skeleton for page layout

* Improve dynamic toc

* Switch page full width to be client side only

* Format

* Remove old PageLoading

* Fix first page not marked as active

* Close search when clicking search link
2023-12-28 14:38:15 +01:00
Samy Pessé 1c2d68c518 Fix handle of multi-id and improve PDF UI in that case (#78)
* Fix handling urls with direct space ID

* Preserve all search params when navigating in the PDF UI

* Show a trademark on the pdf page

* Generate proper trademark url

* Format

* Cleanup
2023-12-27 16:32:59 +01:00
Samy Pessé c8358feeeb Support an "api" query parameter in mode multi-id (#77) 2023-12-27 14:15:47 +01:00
Samy Pessé 6cf73e4962 Support URL path to directly access a space by its ID (even private) (#76)
* Handle URLs /~space/:id

* Validate permissions in multi-id mode
2023-12-27 13:46:35 +01:00
Samy Pessé 67b20138dd Show loading, fix z-index and prevent print from opening (#75) 2023-12-27 11:37:44 +01:00
Samy Pessé 83f4cb5968 Add link to PDF export in the page and improve PDF UI (#73)
* Add buttons to pdf page

* Present as page

* Fix break

* Show active page

* Add description

* Make buttons work

* Handle back link

* Show alert when reaching maximum

* Format
2023-12-26 23:14:46 +01:00
Samy Pessé 24f5e73bbe PDF export (#72)
* Start

* Improve generation

* Improve style

* Lint

* Use tilde instead of dot in the url

* Improve style

* Fix page group

* Format

* Add visual tests for PDF
2023-12-26 18:36:44 +01:00
Sebastian Graz 1fde857aee Stylepr (#60)
* hint leading

* first attempt at themed ui

* v2 theme ui

* fix feedback

* fix hint css override

* cookie

* more sensible scroll logic

* Add better heading margin

* inline code styling

* fix copy button, list vis, next/prev mobile

* add shortcut to search button

* search button, toc

* add toc style

* fix toc hover

* safari fix

* simplify search, swagger, next/prev

* add mask to page cover

* codeblock fix

* global defaults

* improve mobile cards

* modal changes

* change mask intensity dark mode, fix table progress zindex

* a target

* selection, footer, search icon

* fix card resp

* fix multi col images

* fix aside

* card item pos

* search vis

* reuse same header for mobile

* fix header and search legibility

* format

* Sidebar changes

* remove linear gradient light mode page cover
2023-12-26 14:38:54 +01:00
Samy Pessé 4d38479a0f Render inline images (#71)
* Implement inline images

* Format
2023-12-26 12:22:19 +01:00
Samy Pessé db4d4a3408 Format (#70) 2023-12-26 11:20:08 +01:00
Samy Pessé 7267398d5b Use caching for search/ask and rich content for answers (#68)
* Use caching for search/ask

* Refactor DocumentView to be ready to be used for other stuff

* Start logic to use document

* Fix comment

* Remove log

* Format and typecheck
2023-12-25 22:59:14 +01:00
Samy Pessé 2d4c9880fd Fix last accessibility issue in cookies modal and try CI to monitor regressions (#67)
* Add aria-label to cookies modal

* Test lighthouse

* Remove lighthouse as it doesn't work in bun

https://github.com/oven-sh/bun/issues/4958

* Translate everything in search
2023-12-24 17:44:36 +01:00
Samy Pessé 231654e43b Improve accessibility of the cookie banner (#66) 2023-12-24 16:30:34 +01:00
Samy Pessé bfdf6640c8 Add "Edit on GitHub" in aside (#58)
* Start showing link in the aside

* Show link in aside

* Add icon next to link

* Fix padding
2023-12-24 16:04:25 +01:00
Samy Pessé 528a06c65a Improve scroll and aside (#64)
* Scroll to the right location and show active section in aside

* Format
2023-12-24 16:02:34 +01:00
Samy Pessé f7cb622863 Render inline emojis as native emojis (#65) 2023-12-24 14:53:22 +01:00
Samy Pessé 6f6dde1aa4 Update tailwind to 3.4 (#63)
* Update tailwind to 3.4.0

* Update tailwind-merge

* Fix responsive for cards in the footer

* Make card responsive

* Lint
2023-12-24 01:26:51 +01:00
Samy Pessé 3188795a52 Improve not found error (#62)
* Implement error page

* Improve color

* Lint

* Add padding for mobile
2023-12-23 23:25:01 +01:00
Samy Pessé e681aa6ba1 Use card for embed and display caption (#61)
* Use card for embed and display caption

* Fix warnings in icon
2023-12-23 22:49:28 +01:00
Samy Pessé 215c724a9b Use Cloudflare Cache API as one of the cache backend (#59)
* Implement cache for cloudflare

* Start using it

* Fix cache use

* Generate proper URLs

* Comment

* Fix cache always resulting in cache miss

* Log

* Correctly cancel redis

* Write to fallback caches

* Disable memory cache

* Re-enable memory cache

* Try patching

* Use it

* Also patch package.json

* Fix TS errors

* Remove yarn.lock

* Fix local error

* Lint

* Try bunx to fix vercel build

* Update lockfile

* Add back revalidating
2023-12-22 11:05:14 +01:00
Sebastian Graz c1d3f990f1 Styling PR (#56)
* v1 loading state

* refactor primitives

* fix order

* fix toast styling

* v1 rating

* transition on page spinner

* visual fixes
2023-12-21 16:51:36 +01:00
Samy Pessé 6a62f42521 Lazy load KaTeX CSS (#57)
* First attempt at lazy loading it

* New approach

* Format

* Remove logs
2023-12-21 15:11:06 +01:00
Samy Pessé e81d65dc2e Fix typescript error 2023-12-21 12:38:09 +01:00
Sebastian Graz c5f75116d3 Commit (#45) 2023-12-21 12:27:12 +01:00
Samy Pessé c18ec51a4a Experiment with setting Cloudflare-CDN-Cache-Control to force caching 2023-12-21 10:24:11 +01:00
Samy Pessé 5f690af5fb Fix flash of the header when switching between light and dark mode (fix RND-1917) 2023-12-20 18:22:07 +01:00
Samy Pessé fbc2ae10a0 Show cookies approval toast (#55)
* Show cookies toast when privacy policy is enabled

* Flex buttons

* Improve design

* Fix mobile design
2023-12-20 16:51:14 +01:00
Samy Pessé ca3ab044c3 Improve CI with caching and lint work (#53)
* Cache next.js build

* Add format and lint job

* Run typecheck

* FIx all TS issues

* Declare modules for images
2023-12-20 16:07:18 +01:00
Samy Pessé 2888b95b36 Accept a GITBOOK_TOKEN to fetch published content (#54) 2023-12-20 16:04:44 +01:00
Samy Pessé b11566dfb1 Fix favicon for collections and improve middleware matching (#52)
* Use collection title instead of space/customization title when possible

* Skip useless match
2023-12-20 11:41:04 +01:00
Samy Pessé be198045b1 Fallback to using hash of arguments for cache key (#51)
* Fallback to using hash of arguments for cache key

* Dummy commit to trigger build
2023-12-20 09:36:20 +01:00
Samy Pessé 720c2ca909 Setup visual testing (#50)
* Add cloudflare job to publish pages

* Try like this

* Fix build path

* Try end to end

* Fix installation

* Use var instead of secret for CLOUDFLARE_PROJECT_NAME

* Use new puppeteer mode

* Add more tests

* Run in parallel

* Go back to no-concurrency

* Test pagespeed

* Print output

* Fix thresholds
2023-12-19 23:51:06 +01:00
Samy Pessé c935700672 Improve cache revalidation to avoid processing keys multiple times 2023-12-19 19:39:04 +01:00
Samy Pessé e04d03c696 Remove cache tags for documents 2023-12-19 19:31:14 +01:00
Samy Pessé 4f84011f6b Purge cache when option is passed and revalidate starting by entries with max hits (#49)
* Accept an option to purge without refreshing

* Store hits on the redis cache entry

* Revalidate first the entries with the most hits

* Format

* bun

* Ignore error from JSON.NUMINCRBY
2023-12-19 19:16:36 +01:00
Samy Pessé 93aa1cfabf Fix typecheck in .gitbook folders 2023-12-19 18:13:11 +01:00
Samy Pessé 522eaa4319 Fallback to API to resolve page by path to handle redirects (#48) 2023-12-19 15:55:00 +01:00
Samy Pessé 772d120e3a Show loading spinner when navigating between pages (#46) 2023-12-19 15:24:54 +01:00
Samy Pessé bfe91f71f9 Generate robots.txt and add meta link to privacy policy (#47) 2023-12-19 13:34:55 +01:00
Samy Pessé e903bb7752 Update upstash module to fix typing 2023-12-19 10:11:58 +01:00
Sebastian Graz 196e8bcfc0 quickfix: add overflow hidden if search modal open 2023-12-18 23:11:54 +01:00
Samy Pessé eb8baffcf8 Add class search-open to body when search modal is displayed 2023-12-18 22:03:07 +01:00
Sebastian Graz f3ecb69404 modal Y position 2023-12-18 21:53:35 +01:00
Sebastian Graz 86fe37f968 Search PR (#44)
* add search spinner

* better dark/light settings

* text color use textarea for input to get multiline

* fix fonts

* fix margins on result

* fix local ascent override, primary spinner color

* fix overflow

* fix overflow not scrolling all the way up

* testing hiding sidescroller on hover

* add better hover

* no-answer fix
2023-12-18 21:32:54 +01:00
Samy Pessé 953c6bd194 Revalidate cache instead of just invalidating it (#43)
* Store arguments and tags in redis cache

* Refactor cache to support revalidation

* Format

* Fix redis cache

* Try with both GT and NX

* Fix get from redis

* Fix json syntax

* New attempt

* Fix log

* Skip case where there is no key for tags

* New try

* Ignore when no tags is passed
2023-12-18 21:03:07 +01:00
Samy Pessé fc68dbdd4e Fix accessibility issues reported by PageSpeed tests (#41)
* Set aria-label on all buttons

* Use a <p> instead of <h4> in footer
2023-12-18 12:31:56 +01:00
Samy Pessé 705ea6c997 If image resizing fail, redirect to origin image 2023-12-18 12:07:49 +01:00
Samy Pessé 13b9f6b07f Optimize images using Cloudflare Images (#40)
* Optimize images using Cloudflare Images

* Try skipping middleware

* Make it work with base path

* Fetch image size

* Use a target instead of the cf fetch options

* Fix resizing

* Start optimizing images

* Preload images

* Use it for page cover

* Format and adapt cover

* Fix image resizing serving jpeg

* Lazy load dark mode images

* Timeout at 2s

* Fix iframe CSP

* Implement logic to lazy load images that should be offscreen

* Also lazy load cover images

* Low priority in preloading

* Fix csp
2023-12-17 22:26:07 +01:00
Samy Pessé afcb0ecba6 Load integration scripts and secure the app with CSP (#39)
* Set colorSchema metadata

* Try CSP

* Move colorScheme to generateViewport

* Bun install

* Use wildcard for img-src and connect-src

* Allow all unsafe-inline for now for style

* Remove nonce for style

* Remove nonce for style

* Optimize data fetching with parallel requests

* Try to reuse memory cache between middleware and rendering in CF

* Comment

* Fetch document in parallel of collections

* Cleanup

* Log if redis is enabled for caching

* Fix memory cache before redis one

* Optimize alternatives being tested
2023-12-17 00:25:07 +01:00
Samy Pessé 046e74b4a8 Page ratings form (#38)
* Start showing ratings in the aside

* Conditionally render it

* Format

* Lint

* Accept NEXT_PUBLIC_GITBOOK_APP_URL
2023-12-15 22:43:44 +01:00
Samy Pessé 4dfb7c1e61 Load all fonts (#37)
* Load all fonts

* Use font-content as CSS variable

* Fix weird performance.now issue

* Fix font definition
2023-12-15 12:16:52 +01:00
Samy Pessé 594a1d18a7 Log cache performances (#36)
* Log cache performances

* Handle concurrent requests

* Debug log

* Remove debug log

* Comment
2023-12-14 18:33:09 +01:00
Samy Pessé 909a6e76f5 AI Search: recommended questions and hide if disabled (#35)
* Don't show AI search if ask is disabled in the space

* Fetch recommended questions

* Comment and format

* Fix when withAsk is false

* Format

* Lint
2023-12-14 17:38:02 +01:00
Sebastian Graz 5103686d7f remove content-ref for now 2023-12-14 16:51:46 +01:00
Sebastian Graz dae7f704fa Style PR (#34)
* codeblock styling

* fix hover rounding

* checkpoint layout

* improve highlight algo

* v1 syntax theme

* fix table cell add title style

* color tweaks

* opaque sticky numbers

* fix collection spaces

* card aspect fix

* adding back full-width tables

* num tabs

* Add v1 more table types

* format
2023-12-14 16:17:13 +01:00
Samy Pessé 87c38f9091 Sources and followup questions in AI search (#33)
* Show followup questions

* Show sources

* Lint
2023-12-13 08:55:14 +01:00
Samy Pessé ece7f421b4 First basic integration of AI search (#32)
* Refctor Intl and show "Ask" in search

* Handle ask state in the url

* Start fetching answer
2023-12-13 00:57:42 +01:00
Samy Pessé 7a87cc1bb1 Implement an endpoint to clear cache for a space (#31)
* Implement a tagging logic for cache value

* Extract cache-tags from HTTP requests

* Auto-tag API cache entry

* Implement route

* Output cache-control and cache-tag

* Make the revalidate route a edge one
2023-12-12 23:34:18 +01:00
Samy Pessé 2001e4dba5 Use absolute URLs for metadata (icon / social graph) (#30)
* Use absolute URLs for metadata (icon / social graph)

* Disable cache using "next" instead of "cache"

* Use cloudflare module from upstash
2023-12-12 21:38:22 +01:00
Samy Pessé f13e6cb55b Fix use of abort controller and parallel lookup of url 2023-12-12 20:58:37 +01:00
Samy Pessé fff0ea8c34 Add logs 2023-12-12 19:28:34 +01:00
Samy Pessé ee0b3c2032 Wait for cache operation before responding in middleware 2023-12-12 19:24:22 +01:00
Samy Pessé a4ae7140f2 Remove GitHub actions staging workflow 2023-12-12 19:23:23 +01:00
Samy Pessé 7cdf04d2da Remove debug log and try with a single alternative url 2023-12-12 19:03:13 +01:00
Samy Pessé 9138b18a68 Add debug logs 2023-12-12 12:17:31 +01:00
Samy Pessé 632ac7bbab Test by passing a x-forwarded-host header 2023-12-12 11:03:08 +01:00
Samy Pessé ff8a343a54 Fix new cache on Cloudflare (#29)
* Try with next.revalidate = 0

* Remove log

* Format
2023-12-12 10:56:07 +01:00
Samy Pessé b7a4721d66 Fix expired token with fetch data cache (#28)
* Start using cache module for getPublishedContentByUrl

* USe cache-header in cacheResponse

* Pass no-store to getPublishedContentByUrl

* bun install
2023-12-12 10:10:54 +01:00
Sebastian Graz 9e0aabc8bd Styling PR 4 (#24)
* add space to expandable, reduce shadow

* temp animation

* little better anims

* v1 stagger

* add opacity-0 back

* fix opacity on header

* overlay fix opacity

* fix chevron shrink

* style summary arrow

* search button add transition

* style hint custom anchor colors

* Commit

* image border

* fix text color hints

* fix code & code within hints

* annotation pass

* clean up

* setup before iconwork

* simplify hint animations

* allow heading sizes in hints, fix global outline

* fix arrows pagefooter

* increase hitarea arrow

* style ol ul

* v1 checkbox

* toc motion speed

* link legibility, move header links to end, anchor underline

* table pass for busy content

* better table defaults

* v1 table side scroller

* only show on threshold

* format
2023-12-11 20:21:18 +01:00
Samy Pessé 860bc02aa8 Fix code highlighting in Cloudflare (#27)
* Try calling WebAssembly.instantiate directly

* Load from shikiji/onig.wasm instead
2023-12-11 13:43:30 +01:00
Samy Pessé e880bce80b Fallback to plain code block if syntax is not found (#26)
* Fallback to plain code block if syntax is not found

* Try with shikiji instead

* Fix css variables

* Try calling loadWasm

* Support highlighting for all languages of shikiji

* Remove shiki

* Install vscode-oniguruma

* Run bun
2023-12-11 13:23:21 +01:00
Samy Pessé eea8aa306e Prevent search indexation for now 2023-12-10 23:44:20 +01:00
Samy Pessé cf925fd21c Update next to 14.0.4 2023-12-10 23:07:41 +01:00
Samy Pessé 0d15b052ab Fix metadata for pages: title and og images 2023-12-10 22:19:36 +01:00
Sebastian Graz 5761b1914a Styling PR 3 (#23)
- Fix headings always showing hashtag
- replace slate-color with Gitbook brand everywhere
- add gitbook logo to TOC
- Style inline code blocks like `these`
- made TOC headings sticky to top on scroll
- Redesign the blockref component
- Nicer use of `ring` instead of `border` for layout separation
2023-12-05 20:44:56 +01:00
Sebastian Graz d38752433f Styling pr 2 (#20)
- hints remove blend mode on depth > 1
- hints icons animated
- Fix color theming on dropdown menu sub items
- figure out custom shadows for 1px stroke outside paragdim
- tab blocks add spacing between items
- set default accent/primary color if not set
2023-12-04 20:43:47 +01:00
Samy Pessé 8e5b28d2e0 Page cover (#22)
* Start rendering for the cover

* Style image

* Negative margins

* Support hero cover
2023-12-04 16:51:37 +01:00
Samy Pessé ba0d38c220 Page layout options (#21)
* Hide title/description based on layout

* Hide aside based on page.layout.outline

* Hide pagination based on layout

* Hide table of contents based on layout
2023-12-04 00:29:20 +01:00
Samy Pessé 5d236c138a Update gitbook/api@0.18.0 2023-12-03 19:17:25 +01:00
Sebastian Graz bc6244bb58 Styling PR 1 (#16)
* add maxwidth limit for very large screens

* testing animation on hints

* page aside style on darkmode

* fix icon TOC

* Make scrollbars stable when overflowing

* fix scrollbar on search

* search compact header

* search style

* test without bg toc

* Commit

* setup before mobile TOC

* test if style{} clashes

* fix TOC hover

* add prevent default on search shortcut

* add deps

* Add header mobile menu setup

* work on fallback logo

* style search modal position

* style hamburger button

* padding

* add rudimental toggle

* comment

* gap value responsive

* remove slate

* fix z index

* undo test

* Install and format

---------

Co-authored-by: Samy Pessé <samypesse@gmail.com>
2023-11-29 21:18:47 +01:00
Samy Pessé 0cdb5fdf61 Resole root page to / instead of its entire path (#19) 2023-11-29 19:20:31 +01:00
Samy Pessé 78f2469a53 Try the edge runtime (#18)
* Use edge runtime

* Install
2023-11-29 13:38:31 +01:00
Samy Pessé c0f7ccfe3f Dummy commit to trigger Vercel 2023-11-28 21:19:37 +01:00
Samy Pessé 28915e906f Fix invalid colors generated when deployed (#17)
* Try logging

* Try

* Try

* Fix?

* Format

* Remove log
2023-11-28 18:04:26 +01:00
Samy Pessé 92451dba91 Improve middleware with support for x-gitbook-basepath (#15)
* Accept a x-gitbook-api to use as the API endpoint

* Format

* Add support for x-gitbook-basepath

* Add skipTrailingSlashRedirect
2023-11-28 12:43:54 +01:00
Samy Pessé 02715436e7 Accept an X-GitBook-Api header to configure the API endpoint (#14)
* Accept a x-gitbook-api to use as the API endpoint

* Format
2023-11-27 19:00:31 +01:00
Samy Pessé b8af0cb4af Second try with staging git workflow 2023-11-27 16:06:29 +01:00
Samy Pessé b2b2a8c23c Automatically update the staging branch from main using a GH action 2023-11-27 16:03:30 +01:00
Samy Pessé 808f10d428 Namespace cache and prepare handling staging requests (#13)
* Implement UPSTASH_REDIS_NAMESPACE

* Accept X-GitBook-Host and switch to multi mode when present

* Format
2023-11-27 15:50:41 +01:00
Samy Pessé ee0d45fa8b Try fixing PageAside 2023-11-27 14:49:59 +01:00
Samy Pessé 315954dffc Use the new document API to fetch page document (#12)
* Use new API

* Remove getPageDocument

* Format
2023-11-27 13:57:10 +01:00
Samy Pessé bb8b039c1a Code blocks (#10)
* Remove added line

* Improve gutter and prepare wrap

* Style layout of code blocks

* Start rendering annotation

* Test serverComponentsExternalPackages

* Directly import theme

* Use Tailwind with shiki

* Prepare idea on how to lazy load languages

* Start better matching with inlines

* Make test work

* Start loading more languages

* Start annotations

* Format

* Fix multiple lines

* bun install
2023-11-26 23:49:04 +01:00
Samy Pessé fbd17713fe Use newer APIs to optimize data loading and fix resolution of links in the TOC (#11)
* Update api@0.16.0

* Fetch pages and files independantly

* Correctly resolve content ref in TOC

* Format

* Format

* Cleanup
2023-11-26 23:46:51 +01:00
Samy Pessé 2a03461a21 Improve readme 2023-11-23 11:31:04 +01:00
Samy Pessé 84fd934fe1 Try custom cache (#9)
* Try custom cache

* Fix redis cache

* Comment
2023-11-21 21:19:08 +01:00
Sebastian Graz aab7d0bd7a Design pass (#8)
* first stab at bullet points
* setup theming, custom opacities, use of brand UI colors
* color key change
* delete brand
* setup initial defaults
* Add opacity parameter to primary color
* style TOC
* styling pass header, footer, hints, toc, trademark
* table pass
* style pass, typo, quote, tabs, search, theme, toc
2023-11-21 18:29:36 +01:00
Samy Pessé a4ec3c8c53 Refactor routing logic for production to support multi-tenancy (#7)
* Move back to use the middleware as the main logic

* Fix link

* Improve lookup to support docs.gitbook.com.localhost:3000

* Make it work with dev url in prod for now

* Start lookup with API

* Continue

* Prepare for cloudflare build

* Dummy commit

* Try to abort requests

* Support x-forwarded-host

* Test multiple modes

* Add logs

* Add logs

* Update next

* Default to multi-path

* Switch back to nodejs runtime

* Adapt to staging API

* Make sure to return an absolute url for a redirect

* Update api client and try single request

* Try again with concurrent lookup
2023-11-21 16:53:02 +01:00
Samy Pessé 88795e35d9 Update @gitbook/api 0.14.0 (#6)
* Simplify types

* More fixes
2023-11-15 13:46:30 +01:00
Samy Pessé 2fe11a35d4 Functional foundation for search modal (#5)
* Start fetching search using a server actions

* Format

* Keyboard navigation in search

* Highlight query in results

* Add translation to input placeholder

* Add placeholder when no results
2023-11-14 21:36:53 +01:00
Samy Pessé 257f7cd62a Add demo (#4) 2023-11-14 18:11:32 +01:00
Samy Pessé 462b2b6e65 Add support for no header in customization settings (#3)
* Start hiding header

* Adapt page aside as well

* Revert condition
2023-11-13 22:08:29 +01:00
Samy Pessé 77f769c9c2 Fix some TS issues and group import during lint (#2)
* Start fixing TS errors

* Continue

* Also add dom.iterable

* Group header and lint
2023-11-12 21:17:18 +01:00
Samy Pessé df2e267fb2 Start search UI (#1)
* Start

* Improve style

* Add shortcut to open search modal
2023-11-12 00:46:30 +01:00
Samy Pessé 55b29ca608 Fix sitemap 2023-11-10 12:59:46 +01:00
Samy Pessé 61718847b0 Update typing for customization and use latest version of next.js 2023-11-09 19:10:20 +01:00
Samy Pessé 1d44267958 Fix icons 2023-11-04 23:40:17 +01:00
Samy Pessé f190139d5d Test fr 2023-11-04 23:37:55 +01:00
Samy Pessé cf54430651 Format 2023-11-04 23:35:45 +01:00
Samy Pessé d786e1332f Test emoji 2023-11-04 23:35:39 +01:00
Samy Pessé 9ed5dc490e Improve rendering of footer links 2023-11-04 23:30:17 +01:00
Samy Pessé 926fa0f52c Improve rendering of images 2023-11-04 23:24:18 +01:00
Samy Pessé 0eab4389a4 Make footer conditional 2023-11-04 19:02:01 +01:00
Samy Pessé a7c323a023 Start footer 2023-11-04 18:51:53 +01:00
Samy Pessé e5d94dbd23 Add support for emoji in icon 2023-11-04 18:32:05 +01:00
Samy Pessé 288e5db8dc Start favicon generation 2023-11-04 18:26:48 +01:00
Samy Pessé f84a5a2214 Improve accessibility of dropdowns 2023-11-04 16:06:59 +01:00
Samy Pessé 208dca6bab Start supporting collections 2023-11-04 15:18:30 +01:00
Samy Pessé f45735798a Start header links 2023-11-04 00:18:58 -04:00
Samy Pessé abb3ed132a Start theming header 2023-11-03 22:58:04 -04:00
Samy Pessé 13df0d69f4 Generate entire palette of shades for primary color 2023-11-03 22:29:20 -04:00
Samy Pessé cdddebb931 Start using dynamic color for primary 2023-11-03 22:14:29 -04:00
Samy Pessé 47cc664b4f Fetch customization settings 2023-11-03 21:36:40 -04:00
Samy Pessé df84dda36a Implement block content ref 2023-11-03 21:23:41 -04:00
Samy Pessé 84dd482430 Better type inline 2023-11-03 21:13:04 -04:00
Samy Pessé 1e389fca86 Use cache and fix crash in TOC 2023-11-03 21:03:38 -04:00
Samy Pessé 2d4faaf396 Add link to generate links in PDF mode 2023-11-03 16:59:37 -04:00
Samy Pessé 85af5fd4e2 Start PDF html generation 2023-11-03 16:16:00 -04:00
Samy Pessé 8a08f5ae94 Start generating sitemap.xml 2023-11-03 15:45:42 -04:00
Samy Pessé af646b05af Update @gitbook/api 2023-11-01 14:04:23 -04:00
Samy Pessé 07ad0d3909 Try more blocks 2023-11-01 13:41:14 -04:00
Samy Pessé 12da9e27bb Implement block file 2023-10-31 19:22:37 -04:00
Samy Pessé 2b56afb848 Fix some blocks 2023-10-31 18:46:47 -04:00
Samy Pessé 117721f5f6 Fix crash 2023-10-30 20:11:48 -04:00
Samy Pessé 9ab6bf0cec Render inline and block math 2023-10-30 19:50:09 -04:00
Samy Pessé 2e28affa93 Fix type 2023-10-30 19:03:15 -04:00
Samy Pessé 55464029d4 Add support for quote 2023-10-30 18:59:17 -04:00
Samy Pessé d9b8ae95bd Add icons and color to hint 2023-10-30 18:48:17 -04:00
Samy Pessé 80e4d7186c Better type and implement start of cards 2023-10-30 15:45:31 -04:00
Samy Pessé 62ddeaa6bf Start swagger 2023-10-30 13:05:57 -04:00
Samy Pessé 2d278e612c Disable typechecking in next build 2023-10-29 23:32:55 -04:00
Samy Pessé 4b46eac5ba Improve flex in header 2023-10-29 16:50:22 -04:00
Samy Pessé 73fcc0f9ae Use it for the page footer as well 2023-10-29 16:33:27 -04:00
Samy Pessé b5b7090bb9 Start translations 2023-10-29 16:29:59 -04:00
Samy Pessé af09b108d1 Add button for headings 2023-10-29 15:16:35 -04:00
Samy Pessé 66157393ea Start table 2023-10-29 13:49:31 -04:00
Samy Pessé e62f91dc0d Start dark mode 2023-10-29 10:11:33 -04:00
Samy Pessé cca37bc9e8 Start rendering images 2023-10-29 09:05:48 -04:00
Samy Pessé 873570543f Start expandable 2023-10-24 22:14:20 +02:00
Samy Pessé dd0e6db171 Start tabs 2023-10-24 22:07:19 +02:00
Samy Pessé 7295d25e4e Start aside with sections 2023-10-24 21:42:45 +02:00
Samy Pessé e1372b0ede Make layout great! 2023-10-24 20:57:00 +02:00
Samy Pessé 4ab6e6272e Improve layout and prepare for full width blocks 2023-10-24 20:07:27 +02:00
Samy Pessé cdf17b2d80 Improve search button and add line on toc 2023-10-24 19:32:18 +02:00
Samy Pessé e675454117 Add icons and improve toggeable buttons in the toc 2023-10-24 19:12:32 +02:00
Samy Pessé cde014a355 Start prev/next navigation 2023-10-24 18:48:37 +02:00
Samy Pessé cfb197067a Start search box 2023-10-24 18:22:40 +02:00
Samy Pessé 6786f64c9f Fix page link resolution 2023-10-24 17:45:13 +02:00
Samy Pessé 3443ed3e82 Start link and switch to tailwind-merge 2023-10-24 17:42:54 +02:00
Samy Pessé cef72dca61 Start marks and fix h3 2023-10-24 14:00:57 +02:00
Samy Pessé 3cb826de96 Start highlighting of code blocks 2023-10-24 13:36:32 +02:00
Samy Pessé 5f01b90f55 Open ancestors of current page in TOC 2023-10-24 08:25:06 +02:00
Samy Pessé b41ab9115d Start toggling toc 2023-10-24 08:12:43 +02:00
Samy Pessé 970731b38a Improve cover image 2023-10-24 07:52:29 +02:00
Samy Pessé 02bbb39765 Simplify and generate og:image for each page 2023-10-23 21:32:29 +02:00
Samy Pessé e29fb2091e Start generating metadata for the page 2023-10-23 00:05:13 +02:00
Samy Pessé 68d198f255 Start search button 2023-10-22 23:57:36 +02:00
Samy Pessé 0b6910951d Start 2023-10-22 23:48:31 +02:00
Samy Pessé 0e90584911 Initial commit from Create Next App 2023-10-22 15:24:44 +02:00
559 changed files with 29883 additions and 32922 deletions
+8
View File
@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+27
View File
@@ -0,0 +1,27 @@
# Configurations
### API ###
## API token to use when looking up the published content
# GITBOOK_API_URL=https://api.gitbook.com
# GITBOOK_TOKEN=xxx
## User agent to use when making requests to the API
# GITBOOK_USER_AGENT=GitBook/1.0.0
# GITBOOK_USER_AGENT_COMMENT=something
### URL of the application ###
# NEXT_PUBLIC_GITBOOK_APP_URL=https://app.gitbook.com
### Image resizing ###
# GITBOOK_IMAGE_RESIZE_SIGNING_KEY=1234567890
# GITBOOK_IMAGE_RESIZE_URL=https://mycompany.com/cdn-cgi/image/
### SEO ###
# GITBOOK_BLOCK_SEARCH_INDEXATION=true
## Caching
# GITBOOK_OUTPUT_CACHE=true
### Sentry ###
# SENTRY_DSN=xxx
+31
View File
@@ -0,0 +1,31 @@
{
"extends": "next/core-web-vitals",
"plugins": ["import"],
"rules": {
"import/order": [
"error",
{
"groups": ["builtin", "external", ["internal", "parent", "sibling", "index"]],
"newlines-between": "always",
"distinctGroup": true,
"pathGroups": [
{
"pattern": "@/**",
"group": "external",
"position": "after"
},
{
"pattern": "@gitbook/**",
"group": "external",
"position": "after"
}
],
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
],
"@next/next/no-img-element": ["off"]
}
}
+74
View File
@@ -0,0 +1,74 @@
# Welcome to GitBook's contributing guide!
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team.In this guide, you'll learn the different ways you can contribute.
## Types of Contributions
This repository contains code related to the rendering engine of GitBook's published content. Depending on what you'd like to contribute to, head to the section below to find the necessary steps.
### Add a feature
Because this portion of GitBook is open source and available for you to use - if you think you can provide extra value through a new feature - you're welcome to add it! If you plan to distribute the code, keep the source code public to comply with GNU GPLv3. To clone in a private repository, you must first acquire a [commercial license](https://www.gitbook.com/pricing).
### Create a new issue
If you spot a problem within a repository, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new issue here!Please make sure any added issues are
- Descriptive
- Thoughtful
- Organized
We recommend adding as many relevant links, minimal reproductions of the issue, and other materials that will help our team solve the issue fast.
### Solve an issue
If you're interested in solving an issue in our repository, start by scanning through it's exisiting issues to find one that you're interested in working on. If you find an issue to work on, you are welcome to open a PR with a fix. See the following sections below for more information on contributing for specific sections.
### Documentation
The official documentation on GitBook open can be found directly in this Readme. Any updates or changes you would like to make, you can make directly to the README of this repository.
## Contributing
### Make changes locally
Any contribution you make can be made to the code located in this repository. In order to contribute, you'll need to start off of a local version of this repository.
#### 1. Fork the repository
##### Using GitHub Desktop:
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
##### Using the command line:
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
##### GitHub Codespaces:
- [Fork, edit, and preview](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace) using [GitHub Codespaces](https://github.com/features/codespaces) without having to install and run the project locally.
#### 2. Create a working branch and start with your changes
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of. After creating the branch, you can start making changes!
### Commit your update
[Commit your changes](https://github.com/git-guides/git-commit) once you are happy with them. See [Atom's contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages) to know how to use emoji for commit messages!
Once your changes are ready, don't forget to self-review your code to double check that your chagnes are ready to be added.
### Pull Request
When you're finished with the changes, [create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request), also known as a PR.
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. Once you submit your PR, a GitBook team member will review your proposal. We may ask questions or request for additional information.
- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
- If you run into any merge issues, checkout this [git tutorial](https://lab.github.com/githubtraining/managing-merge-conflicts) to help you resolve merge conflicts and other issues.
### Your PR is merged
Congratulations 🎉Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
+18
View File
@@ -0,0 +1,18 @@
---
name: Bug report
about: Something not working as expected? Let us look into it
labels: bug
---
## Bug description
_Please describe._
_If this affects the front-end, screenshots would be of great help._
## How to reproduce
1.
2.
3.
## Additional context
@@ -0,0 +1,35 @@
name: 'Setup Playwright'
description: 'Install Playwright and dependencies'
runs:
using: 'composite'
steps:
# Run npm ci and get Playwright version
- name: 🏗 Prepare Playwright env
shell: bash
run: |
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["@playwright/test"].version')
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
# Cache browser binaries, cache key is based on Playwright version and OS
- name: 🧰 Cache Playwright browser binaries
uses: actions/cache@v3
id: playwright-cache
with:
path: '~/.cache/ms-playwright'
key: '${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}'
restore-keys: |
${{ runner.os }}-playwright-
# Install browser binaries & OS dependencies if cache missed
- name: 🏗 Install Playwright browser binaries & OS dependencies
if: steps.playwright-cache.outputs.cache-hit != 'true'
shell: bash
run: |
bun x playwright install --with-deps chromium
# Install only the OS dependencies if cache hit
- name: 🏗 Install Playwright OS dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
shell: bash
run: |
bun x playwright install-deps
+174
View File
@@ -0,0 +1,174 @@
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
deploy:
name: Deploy to Cloudflare Pages
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
outputs:
deployment_url: ${{ steps.cloudflare.outputs.url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Cache Next.js build
uses: actions/cache@v3
with:
path: |
${{ github.workspace }}/.next/cache
# Generate a new cache whenever packages or source files change.
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
# If source files changed but packages didn't, rebuild from a prior cache.
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}-
- name: Sets env vars for production
run: |
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/heads/main')
- name: Sets env vars for preview
run: |
echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV
if: 1 && !startsWith(github.ref, 'refs/heads/main')
- name: Build Next.js with next-on-pages
run: bun run build:cloudflare
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- id: cloudflare
name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
directory: ./.vercel/output/static
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
workingDirectory: ./
- name: Outputs
run: |
echo "ID: ${{ steps.cloudflare.outputs.id }}"
echo "URL: ${{ steps.cloudflare.outputs.url }}"
echo "Environment: ${{ steps.cloudflare.outputs.environment }}"
echo "Alias: ${{ steps.cloudflare.outputs.alias }}"
- name: Archive build output
uses: actions/upload-artifact@v4
with:
name: build-output
path: .vercel/
visual-testing:
runs-on: ubuntu-latest
name: Visual Testing
needs: deploy
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{needs.deploy.outputs.deployment_url}}
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
pagespeed-testing:
runs-on: ubuntu-latest
name: PageSpeed Testing
needs: deploy
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run pagespeed tests
run: bun ./tests/pagespeed-testing.ts $DEPLOYMENT_URL
env:
DEPLOYMENT_URL: ${{needs.deploy.outputs.deployment_url}}
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
format:
runs-on: ubuntu-latest
name: Format
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun format:check
lint:
runs-on: ubuntu-latest
name: Lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun lint --no-cache
test:
runs-on: ubuntu-latest
name: Test
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun unit
typecheck:
runs-on: ubuntu-latest
name: Typecheck
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun typecheck
+35
View File
@@ -0,0 +1,35 @@
name: Publish
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+46 -23
View File
@@ -1,25 +1,48 @@
# Logs
logs
*.log
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
# dependencies
node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# visual tests
screenshots/
# Sentry Config File
.sentryclirc
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# Generated public files
/public/~gitbook/static/
+4
View File
@@ -0,0 +1,4 @@
.next
# Generated
packages/emoji-codepoints/index.ts
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 100,
"singleQuote": true,
"tabWidth": 4
}
-5
View File
@@ -1,5 +0,0 @@
language: node_js
node_js:
- "0.11"
- "0.10"
- "0.8"
+11
View File
@@ -0,0 +1,11 @@
{
"tailwindCSS.experimental.classRegex": [
["Style \\=([^;]*);", "'([^']*)'"],
["Style \\=([^;]*);", "\"([^\"]*)\""],
["Style \\=([^;]*);", "\\`([^\\`]*)\\`"],
["style \\=([^;]*);", "'([^']*)'"],
["style \\=([^;]*);", "\"([^\"]*)\""],
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
],
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"]
}
-17
View File
@@ -1,17 +0,0 @@
Authors
=======
Also see https://github.com/GitbookIO/gitbook/graphs/contributors.
Names below are ordered by first contribution.
Author
------
- Samy Pessé <samypesse@gmail.com>
- Aaron O'Mullan <aaron.omullan@gmail.com>
Contributors
------------
- Nijiko Yonskai
-68
View File
@@ -1,68 +0,0 @@
module.exports = function (grunt) {
var path = require("path");
// Load NPM tasks
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-requirejs');
// Init GRUNT configuraton
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
less: {
development: {
options: {
compress: true,
yuicompress: true,
optimization: 2
},
files: {
"theme/assets/style.css": "theme/stylesheets/main.less",
"theme/assets/print.css": "theme/stylesheets/print.less"
}
}
},
requirejs: {
compile: {
options: {
name: "app",
baseUrl: "theme/javascript/",
out: "theme/assets/app.js",
preserveLicenseComments: false,
optimize: "uglify", //"uglify",
include: ["requireLib"],
paths: {
"jQuery": 'vendors/jquery',
"lodash": 'vendors/lodash',
"requireLib": 'vendors/require',
"Mousetrap": 'vendors/mousetrap',
"lunr": path.join(__dirname, "node_modules/lunr/lunr")
},
shim: {
'jQuery': {
exports: '$'
},
'lodash': {
exports: '_'
},
'Mousetrap': {
exports: 'Mousetrap'
},
'lunr': {
exports: 'lunr'
}
}
}
}
}
});
// Build
grunt.registerTask('build', [
'less',
'requirejs'
]);
grunt.registerTask('default', [
'build'
]);
};
+642 -169
View File
@@ -1,201 +1,674 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
1. Definitions.
Preamble
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
"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.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
"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.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
"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).
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
"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.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
"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."
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
"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.
The precise terms and conditions for copying, distribution and
modification follow.
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.
TERMS AND CONDITIONS
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.
0. Definitions.
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:
"This License" refers to version 3 of the GNU General Public License.
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
(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
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
(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.
A "covered work" means either the unmodified Program or a work based
on the Program.
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.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
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.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
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.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
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.
1. Source Code.
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.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
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.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
END OF TERMS AND CONDITIONS
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
APPENDIX: How to apply the Apache License to your work.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the 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.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
Copyright {yyyy} {name of copyright owner}
The Corresponding Source for a work in source code form is that
same work.
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
2. Basic Permissions.
http://www.apache.org/licenses/LICENSE-2.0
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
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.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+85 -90
View File
@@ -1,135 +1,130 @@
GitBook
=======
<h1 align="center">GitBook</h1>
[![Build Status](https://travis-ci.org/GitbookIO/gitbook.png?branch=master)](https://travis-ci.org/GitbookIO/gitbook)
<p align="center">
<a href="https://docs.gitbook.com/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a>
</p>
GitBook is a command line tool (and Node.js library) for building beautiful programming books and exercises using GitHub/Git and Markdown. You can see an example: [Learn Javascript](http://gitbookio.github.io/javascript/). An [editor](https://github.com/GitbookIO/editor) is available for Windows, Mac and Linux. You can follow [@GitBookIO](https://twitter.com/GitBookIO) on Twitter.
<p align="center">
<a href="https://gitbook.com"><img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iNjUiIGhlaWdodD0iNjUiIHZpZXdCb3g9IjAgMCA2NSA2NSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI3LjM5NjQgMzMuNjc2NEMzMC41MjU1IDM1LjQ4MjQgMzIuMDkgMzYuMzg1NCAzMy44MDgzIDM2LjM4NjlDMzUuNTI2NSAzNi4zODg0IDM3LjA5MjYgMzUuNDg4MiA0MC4yMjQ5IDMzLjY4NzdMNjAuMTkxNCAyMi4yMTA0QzYxLjA5MjcgMjEuNjkyMiA2MS42NDg0IDIwLjczMTggNjEuNjQ4NCAxOS42OTIxQzYxLjY0ODQgMTguNjUyNCA2MS4wOTI3IDE3LjY5MiA2MC4xOTE0IDE3LjE3MzlMNDAuMjE3NyA1LjY5MjQ1QzM3LjA4ODggMy44OTM4NiAzNS41MjQzIDIuOTk0NTcgMzMuODA3OCAyLjk5NTI0QzMyLjA5MTIgMi45OTU5MSAzMC41Mjc1IDMuODk2NDIgMjcuNCA1LjY5NzQ0TDEwLjIyOTMgMTUuNTg1NUMxMC4xMDIgMTUuNjU4OCAxMC4wMzg0IDE1LjY5NTQgOS45NzkwOCAxNS43MzAxQzQuMTEzNzEgMTkuMTYzNyAwLjQ4OTg5MiAyNS40MzIzIDAuNDQxNDM4IDMyLjIyODZDMC40NDA5NDggMzIuMjk3MyAwLjQ0MDk0OCAzMi4zNzA4IDAuNDQwOTQ4IDMyLjUxNzZDMC40NDA5NDggMzIuNjY0MyAwLjQ0MDk0OCAzMi43Mzc2IDAuNDQxNDM3IDMyLjgwNjNDMC40ODk3ODUgMzkuNTk0OSA0LjEwNTUyIDQ1Ljg1NzcgOS45NjA0NCA0OS4yOTRDMTAuMDE5NiA0OS4zMjg3IDEwLjA4MzIgNDkuMzY1NCAxMC4yMTAyIDQ5LjQzODdMMjAuOTY1OSA1NS42NDg3QzI3LjIzMzIgNTkuMjY3MyAzMC4zNjY4IDYxLjA3NjYgMzMuODA4MSA2MS4wNzc3QzM3LjI0OTMgNjEuMDc4OSA0MC4zODQyIDU5LjI3MTcgNDYuNjUzOSA1NS42NTc0TDU4LjAwOCA0OS4xMTIxQzYxLjE0NzQgNDcuMzAyMyA2Mi43MTcxIDQ2LjM5NzQgNjMuNTc5IDQ0LjkwNTZDNjQuNDQwOSA0My40MTM5IDY0LjQ0MDkgNDEuNjAyIDY0LjQ0MDkgMzcuOTc4NFYzMC45NzgxQzY0LjQ0MDkgMjkuOTcyOCA2My44OTY1IDI5LjA0NjQgNjMuMDE4MiAyOC41NTczQzYyLjE2ODQgMjguMDgzOSA2MS4xMzI1IDI4LjA5MSA2MC4yODkxIDI4LjU3NThMMzcuMDA3NCA0MS45NTg4QzM1LjQ0NTQgNDIuODU2NyAzNC42NjQzIDQzLjMwNTYgMzMuODA3MyA0My4zMDU5QzMyLjk1MDIgNDMuMzA2MiAzMi4xNjg5IDQyLjg1NzcgMzAuNjA2MyA0MS45NjA3TDE0Ljg0ODcgMzIuOTE1NUMxNC4wNTk0IDMyLjQ2MjQgMTMuNjY0NyAzMi4yMzU5IDEzLjM0NzcgMzIuMTk1QzEyLjYyNSAzMi4xMDE3IDExLjkzMDEgMzIuNTA2NiAxMS42NTQ4IDMzLjE4MTNDMTEuNTM0MSAzMy40NzcyIDExLjUzNjUgMzMuOTMyMiAxMS41NDE0IDM0Ljg0MjRDMTEuNTQ1IDM1LjUxMjQgMTEuNTQ2OCAzNS44NDc0IDExLjYwOTQgMzYuMTU1NkMxMS43NDk3IDM2Ljg0NTYgMTIuMTEyNyAzNy40NzA1IDEyLjY0MjggMzcuOTM0MUMxMi44Nzk1IDM4LjE0MTEgMTMuMTY5NiAzOC4zMDg1IDEzLjc1IDM4LjY0MzVMMzAuNTk3NCA0OC4zNjcyQzMyLjE2NDEgNDkuMjcxNCAzMi45NDc0IDQ5LjcyMzUgMzMuODA3NSA0OS43MjM3QzM0LjY2NzcgNDkuNzIzOSAzNS40NTEzIDQ5LjI3MjMgMzcuMDE4NCA0OC4zNjg5TDU3LjY2ODQgMzYuNDY1NEM1OC4yMDM3IDM2LjE1NjkgNTguNDcxNCAzNi4wMDI2IDU4LjY3MjEgMzYuMTE4NUM1OC44NzI3IDM2LjIzNDUgNTguODcyNyAzNi41NDM0IDU4Ljg3MjcgMzcuMTYxM1Y0MC4zMzY1QzU4Ljg3MjcgNDEuMjQyNCA1OC44NzI3IDQxLjY5NTQgNTguNjU3MiA0Mi4wNjgzQzU4LjQ0MTggNDIuNDQxMyA1OC4wNDkzIDQyLjY2NzUgNTcuMjY0NCA0My4xMTk5TDQwLjIzMjIgNTIuOTM4QzM3LjA5NjYgNTQuNzQ1NCAzNS41Mjg4IDU1LjY0OTIgMzMuODA3OSA1NS42NDg0QzMyLjA4NjkgNTUuNjQ3NiAzMC41MTk5IDU0Ljc0MjQgMjcuMzg2IDUyLjkzMjFMMTEuNDUwOSA0My43MjdDMTEuNDAwMyA0My42OTc4IDExLjM3NSA0My42ODMyIDExLjM1MTQgNDMuNjY5NEM4LjAxMDIzIDQxLjcxNyA1Ljk0ODU5IDM4LjE0NTEgNS45MjkyNSAzNC4yNzU0QzUuOTI5MTIgMzQuMjQ4IDUuOTI5MTIgMzQuMjE4OCA1LjkyOTEyIDM0LjE2MDRWMzEuMjQ1OEM1LjkyOTEyIDI5LjEwOTUgNy4wNjY4OSAyNy4xMzQ5IDguOTE1MTMgMjYuMDYzNkMxMC41NDgzIDI1LjExNjkgMTIuNTYyOCAyNS4xMTUxIDE0LjE5NzcgMjYuMDU4N0wyNy4zOTY0IDMzLjY3NjRaIiBmaWxsPSIjRjJGN0Y3Ii8+Cjwvc3ZnPgo=&labelColor=5c5c5c&color=3F89A1&label=%20" alt="Documented on GitBook"></a>
<a href="#"><img src="https://img.shields.io/badge/Open_Source-❤️-FDA599?"/></a>
<a href="/LICENSE"><img src="https://img.shields.io/badge/License-GNU_GPLv3-F4E28D"/></a>
<a href="/.github/CONTRIBUTING.md"><img src="https://img.shields.io/github/contributors/gitbookIO/gitbook"/></a>
<a href="https://github.com/gitbookIO/gitbook/issues"><img src="https://img.shields.io/github/issues/gitbookIO/gitbook"/></a>
</p>
![Image](https://raw.github.com/GitbookIO/gitbook/master/preview.png)
<p align="center">Welcome to GitBook, the platform for managing technical knowledge for teams.</p>
## How to use it:
<p align="center">This repository contains the open-source code used to render GitBook's published content.</p>
GitBook can be installed from **NPM** using:
<p align="center">
<img alt="GitBook Open Published Site" src="./assets/published-site.png">
</p>
## Table of Contents
- [Getting Started](#getting-started)
- [Contributing](#contributing)
- [Types of contributions](#types-of-contributions)
- [Licensing](#license)
- [Acknowledgements](#acknowledgements)
- [Legacy GitBook](#legacy-gitbook-deprecated)
## Getting Started
To run a local version of this project, please follow these simple steps.
### Prerequisites
- Node.js (Version: >=18.x)
- Bun
### Set up
1. Clone the repo into a **public** GitHub repository. If you plan to distribute the code, keep the source code public to comply with GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
```
$ npm install gitbook -g
git clone https://github.com/gitbookIO/gitbook.git
```
You can serve a repository as a book using:
2. Install the project's dependencies through Bun.
```
$ gitbook serve ./repository
bun install
```
Or simply build the static website using:
3. Start your local development server.
```
$ gitbook build ./repository --output=./outputFolder
bun dev
```
Options for commands `build` and `serve` are:
Then open the space in your web browser, using http://localhost:3000/<host>/<path> (example: http://localhost:3000/docs.gitbook.com).
```
-t, --title <name> Name of the book to generate, defaults to repo name
-i, --intro <intro> Description of the book to generate
-g, --github <repo_path> ID of github repo like : username/repo
-o, --output <directory> Path to output directory, defaults to ./_book
-f, --format <name> Change generation format, defaults to site, availables are: site, page, pdf, json
--githubHost <url> The url of the github host (defaults to https://github.com/)
--theme <path> Path to theme directory
```
### Other development commands
You can publish your books to our index by visiting [GitBook.io](http://www.gitbook.io)
- `bun format`: format the code
- `bun lint`: lint the code
## Output Formats
### CI and testing
GitBook can generate your book in the following formats:
All pull-requests will be tested against both visual and performances testing to prevent regressions.
* **Static Website**: This is the default format, it generates a complete interactive static website that can be for example hosted on GitHub Pages.
* **PDF**: A complete PDF book with exercise solutions at the end of the book. Generate to this format using: ```gitbook pdf ./myrepo```, you need to have [gitbook-pdf](https://github.com/GitbookIO/gitbook-pdf) installed.
* **eBook**: A complete eBook with exercise solutions at the end of the book. Generate to this format using: ```gitbook ebook ./myrepo```, you need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed.
* **Single Page**: The book will be stored in a single printable HTML page, this format is used for conversion to PDF or eBook. Generate to this format using: ```gitbook build ./myrepo -f page```.
* **JSON**: This format is used for debugging or extracting metadata from a book. Generate to this format using: ```gitbook build ./myrepo -f json```.
## Contributing
## Book Format
GitBook's rendering engine is fully open-source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
A book is a GitHub repository containing at least 2 files: `README.md` and `SUMMARY.md`.
### Types of contributions
#### README.md
We encourage you to contribute to GitBook to help us build the best tool for documenting technical knowledge. If you're looking for some quick ways to contribute, continue reading to learn more about popular contributions.
As usual, it should contains an introduction for your book. It will be automatically added to the final summary.
#### Translations
#### SUMMARY.md
The GitBook UI is rendered using a set of translation files found in [`src/intl/translations`](/src/intl/translations/). We welcome all additional translations for the UI.
The `SUMMARY.md` defines your book's structure. It should contain a list of chapters, linking to their respective pages.
#### Bugs
Example:
Encounter a bug or find an issue you'd like to fix? Helping us fix issues related to GitBook greatly improves the experience for everyone. Head to the issues section of this repository to learn more about the types of bugs you can already help out with.
```
# Summary
## Deployment
This is the summary of my book.
> [!WARNING]
> While it is possible to self-host this project, we do not recommend this unless you are certain this option fits your need.
>
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](/.github/CONTRIBUTING.md) to get started._
>
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
>
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to contribute to our product directly instead of creating your own version.
* [section 1](section1/README.md)
* [example 1](section1/example1.md)
* [example 2](section1/example2.md)
* [section 2](section2/README.md)
* [example 1](section2/example1.md)
```
This project allows you to self-host the rendering portion of your GitBook published content. Self-hosting has pros and cons.
Files that are not included in the `SUMMARY.md` will not be processed by `gitbook`.
On the pro side, you can customize the look and feel of your content, and better embed your documentation in your application.
#### Exercises
On the con side, you become responsible for the reliability of your published site, and keeping the renderer up-to-date with the changes made to the GitBook platform.
A book can contain interactive exercises (currently only in Javascript but Python and Ruby are coming soon ;) ). An exercise is a code challenge provided to the reader, which is given a code editor to write a solution which is checked against the book author's validation code.
## License
An exercise is defined by 4 simple parts:
Distributed under the [GNU GPLv3 License](https://github.com/GitBookIO/gitbook/blob/main/LICENSE).
* Exercise **Message**/Goals (in markdown/text)
* **Initial** code to show to the user, providing a starting point
* **Solution** code, being a correct solution to the exercise
* **Validation** code that tests the correctness of the user's input
If you plan to distribute the code, you must the source code public to comply with GNU GPLv3. To clone in a private repository, acquire a [commercial license](https://www.gitbook.com/pricing).
Exercises need to start and finish with a separation bar (```---``` or ```***```). It should contain 3 code elements (**base**, **solution** and **validation**). It can contain a 4th element that provides **context** code (functions, imports of libraries etc ... that shouldn't be displayed to the user).
See `LICENSE` for more information.
---
## Acknowledgements
Define a variable `x` equal to 10.
GitBook wouldn't be possible without these projects:
```js
var x =
```
- [Next.js](https://nextjs.org/)
- [Bun](https://bun.sh/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Framer Motion](https://www.npmjs.com/package/framer-motion)
```js
var x = 10;
```
## Contributors
```js
assert(x == 10);
```
<a href="https://github.com/gitbookIO/gitbook/graphs/contributors">
<img src="https://contrib.rocks/image?repo=gitbookIO/gitbook" />
</a>
```js
// This is context code available everywhere
// The user will be able to call magicFunc in his code
function magicFunc() {
return 3;
}
```
## Legacy GitBook (Deprecated)
---
#### Multi-Languages
GitBook supports building books written in multiple languages. Each language should be a sub-directory following the normal GitBook format, and a file named `LANGS.md` should be present at the root of the repository with the following format:
```
* [English](en/)
* [French](fr/)
* [Español](es/)
```
You can see a complete example with the [Learn Git](https://github.com/GitbookIO/git) book.
#### Ignoring files & folders
GitBook will read the `.gitignore`, `.bookignore` and `.ignore` files to get a list of files and folders to skip. (The format inside those files, follows the same convention as `.gitignore`)
Our previous version of GitBook and it's CLI tool are now deprecated. You can still view the old repository and it's commits on this [branch](https://github.com/GitbookIO/gitbook/tree/legacy).
Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

-50
View File
@@ -1,50 +0,0 @@
var path = require('path');
var Q = require('q');
var _ = require('lodash');
var utils = require('./utils');
var generate = require("../lib/generate");
var parse = require("../lib/parse");
var fs = require('../lib/generate/fs');
var generators = require("../lib/generate").generators;
var makeBuildFunc = function(converter) {
return function(dir, options) {
dir = dir || process.cwd();
outputDir = options.output
console.log('Starting build ...');
// Get repo's URL
return utils.gitURL(dir)
.then(function(url) {
// Get ID of repo
return utils.githubID(url);
}, function(err) {
return null;
})
.then(function(repoID) {
return converter(
_.extend(options || {}, {
input: dir,
output: outputDir,
title: options.title,
description: options.intro,
github: options.github || repoID,
githubHost: options.githubHost,
generator: options.format,
theme: options.theme
})
);
})
.then(function(output) {
console.log("Successfuly built !");
return output;
}, utils.logError);
};
}
module.exports = {
folder: makeBuildFunc(generate.folder),
file: makeBuildFunc(generate.file)
};
-86
View File
@@ -1,86 +0,0 @@
#! /usr/bin/env node
var Q = require('q');
var _ = require('lodash');
var path = require('path');
var prog = require('commander');
var pkg = require('../package.json');
var generators = require("../lib/generate").generators;
var initDir = require("../lib/generate/init");
var fs = require('../lib/generate/fs');
var utils = require('./utils');
var build = require('./build');
// General options
prog
.version(pkg.version);
var buildCommand = function(command) {
return command
.option('-o, --output <directory>', 'Path to output directory, defaults to ./_book')
.option('-f, --format <name>', 'Change generation format, defaults to site, availables are: '+_.keys(generators).join(", "))
.option('-t, --title <name>', 'Name of the book to generate, default is extracted from readme')
.option('-i, --intro <intro>', 'Description of the book to generate, default is extracted from readme')
.option('-g, --github <repo_path>', 'ID of github repo like : username/repo')
.option('--githubHost <url>', 'The url of the github host (defaults to https://github.com/')
.option('--theme <path>', 'Path to theme directory');
};
buildCommand(prog.command('build [source_dir]'))
.description('Build a gitbook from a directory')
.action(build.folder);
buildCommand(prog.command('serve [source_dir]'))
.description('Build then serve a gitbook from a directory')
.option('-p, --port <port>', 'Port for server to listen on', 4000)
.action(function(dir, options) {
build.folder(dir, options || {})
.then(function(_options) {
console.log();
console.log('Starting server ...');
return utils.serveDir(_options.output, options.port)
.fail(utils.logError);
})
.then(function() {
console.log('Serving book on http://localhost:'+options.port);
console.log();
console.log('Press CTRL+C to quit ...');
});
});
buildCommand(prog.command('pdf [source_dir]'))
.description('Build a gitbook as a PDF')
.option('-pf, --paperformat <format>', 'PDF paper format (default is A4): "5in*7.5in", "10cm*20cm", "A4", "Letter"')
.action(function(dir, options) {
build.file(dir, _.extend(options, {
extension: "pdf",
format: "pdf"
}));
});
buildCommand(prog.command('ebook [source_dir]'))
.description('Build a gitbook as a eBook')
.option('-c, --cover <path>', 'Cover image, default is cover.png if exists')
.action(function(dir, options) {
var ext = options.output ? path.extname(options.output) : "epub";
build.file(dir, _.extend(options, {
extension: ext,
format: "ebook"
}));
});
prog
.command('init [source_dir]')
.description('Create files and folders based on contents of SUMMARY.md')
.action(function(dir) {
dir = dir || process.cwd();
return initDir(dir);
});
// Parse and fallback to help if no args
if(_.isEmpty(prog.parse(process.argv).args) && process.argv.length === 2) {
prog.help();
}
-101
View File
@@ -1,101 +0,0 @@
var Q = require('q');
var _ = require('lodash');
var http = require('http');
var send = require('send');
var url = require('url');
var cp = require('child_process');
// Get the remote of a given repo
function gitURL(path) {
var d = Q.defer();
cp.exec("git config --get remote.origin.url", {
cwd: path,
env: process.env,
}, function(err, stdout, stderr) {
if(err) {
return d.reject(err);
}
return d.resolve(stdout);
});
return d.promise
.then(function(output) {
return output.replace(/(\r\n|\n|\r)/gm, "");
});
}
// Poorman's parsing
// Parse a git URL to a github ID : username/reponame
function githubID(_url) {
// Remove .git if it's in _url
var sliceEnd = _url.slice(-4) === '.git' ? -4 : _url.length;
// Detect HTTPS repos
var parsed = url.parse(_url);
if(parsed.protocol === 'https:' && parsed.host === 'github.com') {
return parsed.path.slice(1, sliceEnd);
}
// Detect SSH repos
if(_url.indexOf('git@') === 0) {
return _url.split(':', 2)[1].slice(0, sliceEnd);
}
// None found
return null;
}
function titleCase(str)
{
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}
function serveDir(dir, port) {
var d = Q.defer();
var server = http.createServer(function(req, res){
// Render error
function error(err) {
res.statusCode = err.status || 500;
res.end(err.message);
}
// Redirect to directory's index.html
function redirect() {
res.statusCode = 301;
res.setHeader('Location', req.url + '/');
res.end('Redirecting to ' + req.url + '/');
}
// Send file
send(req, url.parse(req.url).pathname)
.root(dir)
.on('error', error)
.on('directory', redirect)
.pipe(res);
}).listen(port);
d.resolve(server);
return d.promise;
}
function logError(err) {
console.log(err.stack || err.message || err);
return Q.reject(err);
};
// Exports
module.exports = {
gitURL: gitURL,
githubID: githubID,
titleCase: titleCase,
serveDir: serveDir,
logError: logError
};
Executable
BIN
View File
Binary file not shown.
+22
View File
@@ -0,0 +1,22 @@
# Caching
## Revalidating the cache
Invalidate cache can be done at two levels using tags:
- Data fetching cache
- Rendering cache
To invalidate and refetch the data cache, you can execute a POST request to `/~/gitbook/revalidate`:
```bash
curl --location --request POST 'https://gitbook/mycompany.com/~gitbook/revalidate' \
--header 'Content-Type: application/json' \
--data-raw '{"tags": ["space.id"]}'
```
To invalidate the rendering cache, the implementation mainly depends on the infrastructure serving the content, GitBook outputs a `Cache-Tag` header on every requests. The value of the header is a comma separated list of tags.
## Purging the cache
Purging the cache, without revalidating, is done by passing `"purge": true` in the request body.
+668
View File
@@ -0,0 +1,668 @@
import { argosScreenshot } from '@argos-ci/playwright';
import {
CustomizationHeaderPreset,
CustomizationLocale,
CustomizationSettings,
} from '@gitbook/api';
import { test, expect, Page } from '@playwright/test';
import jwt from 'jsonwebtoken';
import rison from 'rison';
import { getContentTestURL } from '../tests/utils';
interface Test {
name: string;
url: string;
run?: (page: Page) => Promise<unknown>;
fullPage?: boolean;
screenshot?: false;
}
interface TestsCase {
name: string;
baseUrl: string;
tests: Array<Test>;
}
const allLocales: CustomizationLocale[] = [
CustomizationLocale.Fr,
CustomizationLocale.Es,
CustomizationLocale.Ja,
CustomizationLocale.Zh,
];
async function waitForCookiesDialog(page: Page) {
const dialog = page.getByRole('dialog', { name: 'Cookies' });
const accept = dialog.getByRole('button', { name: 'Accept' });
const reject = dialog.getByRole('button', { name: 'Reject' });
await expect(accept).toBeVisible();
await expect(reject).toBeVisible();
}
const testCases: TestsCase[] = [
{
name: 'GitBook Site (Single Variant)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
tests: [
{
name: 'Home',
url: '',
run: waitForCookiesDialog,
},
{
name: 'No variants dropdown',
url: '',
run: async (page) => {
await expect(page.locator('[data-testid="space-dropdown-button"]')).toHaveCount(
0,
);
},
},
{
name: 'Search',
url: '?q=',
},
{
name: 'Search Results',
url: '?q=gitbook',
run: async (page) => {
await page.waitForSelector('[data-test="search-results"]');
},
},
{
name: 'AI Search',
url: '?q=What+is+GitBook%3F&ask=true',
run: async (page) => {
await page.waitForSelector('[data-test="search-ask-answer"]');
},
screenshot: false,
},
{
name: 'Not found',
url: 'content-not-found',
run: waitForCookiesDialog,
},
],
},
{
name: 'GitBook Site (Multi Variants)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/multi-variants/',
tests: [
{
name: 'Variants dropdown',
url: '',
run: async (page) => {
const spaceDrowpdown = page.locator('[data-testid="space-dropdown-button"]');
await spaceDrowpdown.waitFor();
},
},
{
name: 'Default variant',
url: '',
},
{
name: 'RFC variant',
url: 'v/rfcs',
},
],
},
{
name: 'GitBook Site (Navigation when switching variant)',
baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/',
tests: [
{
name: 'Keep navigation path/route when switching variant (Public)',
url: 'api-multi-versions/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
await page
.getByRole('link', {
name: '2.0',
})
.click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets?fallback=true',
);
},
},
{
name: 'Keep navigation path/route when switching variant (Share link)',
url: 'api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/reference/api-reference/pets',
screenshot: false,
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
await page
.getByRole('link', {
name: '2.0',
})
.click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets?fallback=true',
);
},
},
{
name: 'Keep navigation path/route when switching variant (VA)',
screenshot: false,
url: (() => {
const privateKey = 'c26190fc-74b2-4b54-9fc7-df9941104953';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`;
})(),
run: async (page) => {
const spaceDrowpdown = await page.waitForSelector(
'[data-testid="space-dropdown-button"]',
);
await spaceDrowpdown.click();
// Click the second variant in the dropdown
await page
.getByRole('link', {
name: '2.0',
})
.click();
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets?fallback=true',
);
},
},
],
},
{
name: 'GitBook',
baseUrl: 'https://docs.gitbook.com',
tests: [
{
name: 'Home',
url: '',
run: waitForCookiesDialog,
},
{
name: 'Search',
url: '?q=',
},
{
name: 'Search Results',
url: '?q=gitbook',
run: async (page) => {
await page.waitForSelector('[data-test="search-results"]');
},
},
{
name: 'AI Search',
url: '?q=What+is+GitBook%3F&ask=true',
run: async (page) => {
await page.waitForSelector('[data-test="search-ask-answer"]');
},
screenshot: false,
},
{
name: 'Not found',
url: 'content-not-found',
run: waitForCookiesDialog,
},
],
},
{
name: 'Versioning',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
tests: [
{
name: 'Revision',
url: '~/revisions/S55pwsEr5UVoroaOiWnP/blocks/headings',
run: waitForCookiesDialog,
},
],
},
{
name: 'PDF',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
tests: [
{
name: 'PDF',
url: '~gitbook/pdf?limit=10',
},
],
},
{
name: 'Content tests',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
tests: [
{
name: 'Text',
url: 'text-page',
run: waitForCookiesDialog,
},
{
name: 'Long text',
url: 'text-page/long-text',
run: waitForCookiesDialog,
},
{
name: 'Images',
url: 'blocks/block-images',
run: waitForCookiesDialog,
fullPage: true,
},
{
name: 'Inline Images',
url: 'blocks/inline-images',
run: waitForCookiesDialog,
},
{
name: 'Tabs',
url: 'blocks/tabs',
run: waitForCookiesDialog,
},
{
name: 'Hints',
url: 'blocks/hints',
run: waitForCookiesDialog,
},
{
name: 'Integration Blocks',
url: 'blocks/integrations',
run: waitForCookiesDialog,
},
{
name: 'Tables',
url: 'blocks/tables',
run: waitForCookiesDialog,
fullPage: true,
},
{
name: 'Expandables',
url: 'blocks/expandables',
run: waitForCookiesDialog,
},
{
name: 'API Blocks',
url: 'blocks/api-blocks',
run: waitForCookiesDialog,
},
{
name: 'Headings',
url: 'blocks/headings',
run: waitForCookiesDialog,
},
{
name: 'Marks',
url: 'blocks/marks',
run: waitForCookiesDialog,
},
{
name: 'Emojis',
url: 'blocks/emojis',
run: waitForCookiesDialog,
},
{
name: 'Links',
url: 'blocks/links',
run: waitForCookiesDialog,
},
{
name: 'Lists',
url: 'blocks/lists',
fullPage: true,
},
{
name: 'Code',
url: 'blocks/code',
fullPage: true,
},
{
name: 'Cards',
url: 'blocks/cards',
fullPage: true,
},
{
name: 'Math',
url: 'blocks/math',
},
{
name: 'Embeds',
url: 'blocks/embeds',
fullPage: true,
},
{
name: 'Annotations',
url: 'blocks/annotations',
run: async (page) => {
await page.waitForSelector('[data-testid="annotation-button"]');
await page.click('[data-testid="annotation-button"]');
},
},
],
},
{
name: 'Page options',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
tests: [
{
name: 'Hidden',
url: 'page-options/page-hidden',
run: waitForCookiesDialog,
},
{
name: 'With cover',
url: 'page-options/page-with-cover',
run: waitForCookiesDialog,
},
{
name: 'With hero cover',
url: 'page-options/page-with-hero-cover',
run: waitForCookiesDialog,
},
{
name: 'With cover and no TOC',
url: 'page-options/page-with-cover-and-no-toc',
run: waitForCookiesDialog,
},
],
},
{
name: 'Customization',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
tests: [
{
name: 'Without header',
url: getCustomizationURL({
header: {
preset: CustomizationHeaderPreset.None,
links: [],
},
}),
run: waitForCookiesDialog,
},
],
},
{
name: 'Share links',
baseUrl: 'https://gitbook.gitbook.io/test-share-links/',
tests: [
{
name: 'Valid link',
url: 'Fc6mMII9FKgnwm7qqynx/',
run: waitForCookiesDialog,
},
{
name: 'Invalid link',
url: 'invalid/',
run: async (page) => {
await expect(
page.getByText('Authentication missing to access this content'),
).toBeVisible();
},
},
],
},
{
name: 'Visitor Auth - Space',
baseUrl: `https://gitbook.gitbook.io/gbo-va-space/`,
tests: [
{
name: 'First',
url: (() => {
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `first?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
{
name: 'Second',
url: (() => {
const privateKey = '70b844d0-c519-4532-8586-5970ce48c537';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `second?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
],
},
{
name: 'Visitor Auth - Collection',
baseUrl: `https://gitbook.gitbook.io/gbo-va-collection/`,
tests: [
{
name: 'Root',
url: (() => {
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
{
name: 'Primary (Space A)',
url: (() => {
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `v/spacea?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
{
name: 'Space B',
url: (() => {
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `v/spaceb?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
{
name: 'Space C',
url: (() => {
const privateKey = 'af5688dc-f0b6-4146-9b1d-6d834c62c980';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `v/spacec?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
],
},
{
name: 'Visitor Auth - Space (custom domain)',
baseUrl: `https://test.gitbook.community/`,
tests: [
{
name: 'Root',
url: (() => {
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
{
name: 'First',
url: (() => {
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `first?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
{
name: 'Custom page',
url: (() => {
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `custom-page?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
{
name: 'Inner page',
url: (() => {
const privateKey = '19c8166f-c436-4ed1-a24e-60954b804021';
const token = jwt.sign(
{
name: 'gitbook-open-tests',
},
privateKey,
{
expiresIn: '24h',
},
);
return `custom-page/inner-page?jwt_token=${token}`;
})(),
run: waitForCookiesDialog,
},
],
},
{
name: 'Languages',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
tests: allLocales.map((locale) => ({
name: locale,
url: getCustomizationURL({
internationalization: {
locale,
inherit: false,
},
}),
run: async (page) => {
const dialog = page.getByTestId('cookies-dialog');
await expect(dialog).toBeVisible();
},
})),
},
];
for (const testCase of testCases) {
test.describe(testCase.name, () => {
for (const testEntry of testCase.tests) {
test(testEntry.name, async ({ page, baseURL }) => {
const contentUrl = new URL(testEntry.url, testCase.baseUrl);
const url = getContentTestURL(contentUrl.toString(), baseURL);
await page.goto(url);
if (testEntry.run) {
await testEntry.run(page);
}
if (testEntry.screenshot !== false) {
await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, {
viewports: ['macbook-16', 'macbook-13', 'iphone-x', 'ipad-2'],
argosCSS: `
/* Hide Intercom */
.intercom-lightweight-app {
display: none !important;
}
`,
fullPage: testEntry.fullPage ?? false,
});
}
});
}
});
}
/**
* Create a URL with customization settings.
*/
function getCustomizationURL(partial: Partial<CustomizationSettings>): string {
const encoded = rison.encode_object(partial);
const searchParams = new URLSearchParams();
searchParams.set('customization', encoded);
return `?${searchParams.toString()}`;
}
-70
View File
@@ -1,70 +0,0 @@
var util = require("util");
var path = require("path");
var Q = require("q");
var _ = require("lodash");
var exec = require('child_process').exec;
var fs = require("fs");
var parse = require("../../parse");
var BaseGenerator = require("../page");
/*
* This generator inherits from the single page generator
* and convert the page output to ebook
*/
var Generator = function() {
BaseGenerator.apply(this, arguments);
// Options for eBook generation
this.options = _.defaults(this.options, {
extension: "epub"
});
if (!this.options.cover && fs.existsSync(path.join(this.options.input, "cover.png"))) {
this.options.cover = path.join(this.options.input, "cover.png")
}
};
util.inherits(Generator, BaseGenerator);
Generator.prototype.finish = function() {
var that = this;
return BaseGenerator.prototype.finish.apply(this)
.then(function() {
var d = Q.defer();
var _options = {
"--cover": that.options.cover
};
var command = [
"ebook-convert",
path.join(that.options.output, "index.html"),
path.join(that.options.output, "index."+that.options.extension),
_.chain(_options)
.map(function(value, key) {
if (value == null) return null;
return key+"="+value;
})
.compact()
.value()
.join(" ")
].join(" ");
exec(command, function (error, stdout, stderr) {
if (error) {
if (error.code == 127) {
error.message = "Need to install ebook-convert from Calibre";
} else {
error.message = error.message + " "+stdout;
}
return d.reject(error);
}
d.resolve();
});
return d.promise;
});
};
module.exports = Generator;
-60
View File
@@ -1,60 +0,0 @@
var Q = require("q");
var fs = require("fs");
var fsExtra = require("fs-extra");
var Ignore = require("fstream-ignore");
var getFiles = function(path) {
var d = Q.defer();
// Our list of files
var files = [];
var ig = Ignore({
path: path,
ignoreFiles: ['.ignore', '.gitignore', '.bookignore']
});
// Add extra rules to ignore common folders
ig.addIgnoreRules([
'.git/',
'.gitignore',
], '__custom_stuff');
// Push each file to our list
ig.on('child', function (c) {
files.push(
c.path.substr(c.root.path.length + 1) + (c.props.Directory === true ? '/' : '')
);
});
ig.on('end', function() {
// Normalize paths on Windows
if(process.platform === 'win32') {
return d.resolve(files.map(function(file) {
return file.replace(/\\/g, '/');
}));
}
// Simply return paths otherwise
return d.resolve(files);
});
ig.on('error', d.reject);
return d.promise;
};
module.exports = {
list: getFiles,
readFile: Q.denodeify(fs.readFile),
writeFile: Q.denodeify(fs.writeFile),
mkdirp: Q.denodeify(fsExtra.mkdirp),
copy: Q.denodeify(fsExtra.copy),
remove: Q.denodeify(fsExtra.remove),
symlink: Q.denodeify(fsExtra.symlink),
exists: function(path) {
var d = Q.defer();
fs.exists(path, d.resolve);
return d.promise;
},
};
-36
View File
@@ -1,36 +0,0 @@
var path = require("path");
var Q = require("q");
var fs = require("./fs");
var BaseGenerator = function(options) {
this.options = options;
};
BaseGenerator.prototype.convertFile = function(content, input) {
return Q.reject(new Error("Could not convert "+input));
};
BaseGenerator.prototype.transferFile = function(input) {
return fs.copy(
path.join(this.options.input, input),
path.join(this.options.output, input)
);
};
BaseGenerator.prototype.transferFolder = function(input) {
return fs.mkdirp(
path.join(this.options.output, input)
);
};
BaseGenerator.prototype.langsIndex = function(langs) {
return Q.reject(new Error("Langs index is not supported in this generator"));
};
BaseGenerator.prototype.finish = function() {
return Q.reject(new Error("Could not finish generation"));
};
module.exports = BaseGenerator;
-238
View File
@@ -1,238 +0,0 @@
var Q = require("q");
var _ = require("lodash");
var path = require("path");
var swig = require('swig');
var tmp = require('tmp');
var fs = require("./fs");
var parse = require("../parse");
var generators = {
"site": require("./site"),
"page": require("./page"),
"pdf": require("./pdf"),
"ebook": require("./ebook"),
"json": require("./json")
};
var defaultDescription = "Book generated using GitBook";
/*
* Use a specific generator to convert a gitbook to a site/pdf/ebook/
* output is always a folder
*/
var generate = function(options) {
var generator = null;
var files;
options = _.defaults(options || {}, {
// Folders to use
input: null,
output: null,
// Output generator
generator: "site",
// Book title, keyword, description
title: null,
description: null,
// Origin github repository id
github: null,
githubHost: 'https://github.com/',
// Theming
theme: path.resolve(__dirname, '../../theme')
});
if (!options.input) {
return Q.reject(new Error("Need option input (book input directory)"));
}
if (!generators[options.generator]) {
return Q.reject(new Error("Invalid generator (availables are: "+_.keys(generators).join(", ")));
}
options.output = options.output || path.join(options.input, "_book");
// Clean output folder
return fs.remove(options.output)
.then(function() {
return fs.mkdirp(options.output);
})
// List all files in the repository
.then(function() {
return fs.list(options.input)
.then(function(_files) {
files = _files;
})
})
// Create the generator
.then(function() {
generator = new generators[options.generator](options);
})
// Detect multi-languages book
.then(function() {
if (_.contains(files, "LANGS.md")) {
// Multi-languages book
return fs.readFile(path.join(options.input, "LANGS.md"), "utf-8")
// Generate sub-books
.then(function(_langsSummary) {
options.langsSummary = parse.langs(_langsSummary);
// Generated a book for each valid entry
return Q.all(
_.map(options.langsSummary.list, function(entry) {
return generate(_.extend({}, options, {
input: path.join(options.input, entry.path),
output: path.join(options.output, entry.path)
}));
})
);
})
// Generate languages index
.then(function() {
return generator.langsIndex(options.langsSummary);
});
} else if (!_.contains(files, "SUMMARY.md") || !_.contains(files, "README.md")) {
// Invalid book
return Q.reject(new Error("Invalid gitbook repository, need SUMMARY.md and README.md"));
} else {
// Generate the book
return Q()
// Read readme
.then(function() {
return fs.readFile(path.join(options.input, "README.md"), "utf-8")
.then(function(_readme) {
_readme = parse.readme(_readme);
options.title = options.title || _readme.title;
options.description = options.description || _readme.description || defaultDescription;
});
})
// Get summary
.then(function() {
return fs.readFile(path.join(options.input, "SUMMARY.md"), "utf-8")
.then(function(_summary) {
options.summary = parse.summary(_summary);
// Parse navigation
options.navigation = parse.navigation(options.summary);
});
})
// Skip processing some files
.then(function() {
files = _.filter(files, function (file) {
return !(
file === 'SUMMARY.md'
);
});
})
// Copy file and replace markdown file
.then(function() {
return Q.all(
_.chain(files)
.map(function(file) {
if (!file) return;
if (file[file.length -1] == "/") {
return Q(generator.transferFolder(file));
} else if (path.extname(file) == ".md" && options.navigation[file] != null) {
return fs.readFile(path.join(options.input, file), "utf-8")
.then(function(content) {
return Q(generator.convertFile(content, file));
});
} else {
return Q(generator.transferFile(file));
}
})
.value()
);
})
// Finish gneration
.then(function() {
return generator.finish();
});
}
})
// Return all options
.then(function() {
return options;
});
};
/*
* Extract files from generate output in a temporary folder
*/
var generateFile = function(options) {
options = _.defaults(options || {}, {
input: null,
output: null,
extension: null
});
return Q.nfcall(tmp.dir)
.then(function(tmpDir) {
return generate(
_.extend({},
options,
{
output: tmpDir
})
)
.then(function(_options) {
var ext = options.extension;
var outputFile = options.output || path.resolve(options.input, "book."+ext);
var copyFile = function(lang) {
var _outputFile = outputFile;
var _tmpDir = tmpDir;
if (lang) {
_outputFile = _outputFile.slice(0, -path.extname(_outputFile).length)+"_"+lang+path.extname(_outputFile);
_tmpDir = path.join(_tmpDir, lang);
}
return fs.copy(
path.join(_tmpDir, "index."+ext),
_outputFile
);
};
// Multi-langs book
return Q()
.then(function() {
if (_options.langsSummary) {
return Q.all(
_.map(_options.langsSummary.list, function(lang) {
return copyFile(lang.lang);
})
);
} else {
return copyFile();
}
})
.then(function() {
return fs.remove(tmpDir);
});
});
});
};
module.exports = {
generators: generators,
folder: generate,
file: generateFile
};
-69
View File
@@ -1,69 +0,0 @@
var Q = require('q');
var _ = require('lodash');
var path = require('path');
var fs = require('./fs');
var parse = require('../parse');
// Extract paths out of a summary
function paths(summary) {
return _.reduce(summary.chapters, function(accu, chapter) {
return accu.concat(
_.filter([chapter.path].concat(_.pluck(chapter.articles, 'path')))
);
}, []);
}
// Get the parent folders out of a group of files
function folders(files) {
return _.chain(files)
.map(function(file) {
return path.dirname(file);
})
.uniq()
.value();
}
function initDir(dir) {
return fs.readFile(path.join(dir, 'SUMMARY.md'), 'utf8')
.then(function(src) {
// Parse summary
return parse.summary(src);
})
.then(function(summary) {
// Extract paths from summary
return paths(summary);
})
.then(function(paths) {
// Convert to absolute paths
return _.map(paths, function(file) {
return path.resolve(file);
});
})
.then(function(files) {
// Create folders
return Q.all(_.map(folders(files), function(folder) {
return fs.mkdirp(folder);
}))
.then(_.constant(files));
})
.then(function(files) {
// Create files that don't exist
return Q.all(_.map(files, function(file) {
return fs.exists(file)
.then(function(exists) {
if(exists) return;
return fs.writeFile(file, '');
});
}));
})
.fail(function(err) {
console.error(err.stack);
});
}
// Exports
module.exports = initDir;
-47
View File
@@ -1,47 +0,0 @@
var util = require("util");
var path = require("path");
var Q = require("q");
var fs = require("../fs");
var parse = require("../../parse");
var BaseGenerator = require("../generator");
var Generator = function() {
BaseGenerator.apply(this, arguments);
};
util.inherits(Generator, BaseGenerator);
Generator.prototype.transferFile = function(input) {
// ignore
};
Generator.prototype.convertFile = function(content, input) {
var that = this;
var json = {
progress: parse.progress(this.options.navigation, input)
};
return Q()
.then(function() {
return parse.page(content, {
repo: that.options.githubId,
dir: path.dirname(input) || '/'
});
})
.then(function(sections) {
json.sections = sections;
})
.then(function() {
return fs.writeFile(
path.join(that.options.output, input.replace(".md", ".json")),
JSON.stringify(json, null, 4)
);
});
};
Generator.prototype.finish = function() {
// ignore
};
module.exports = Generator;
-84
View File
@@ -1,84 +0,0 @@
var _ = require("lodash");
var util = require("util");
var path = require("path");
var Q = require("q");
var swig = require('swig');
var hljs = require('highlight.js');
var fs = require("../fs");
var parse = require("../../parse");
var BaseGenerator = require("../site");
// Swig filter: highlight coloration
swig.setFilter('code', function(code, lang) {
try {
return hljs.highlight(lang, code).value;
} catch(e) {
return hljs.highlightAuto(code).value;
}
});
/*
* This generator will generate a simple index.html which can be converted as a PDF
*/
var Generator = function() {
BaseGenerator.apply(this, arguments);
// Load base template
this.template = swig.compileFile(path.resolve(this.options.theme, 'templates/page.html'));
// List of pages content
this.pages = {};
};
util.inherits(Generator, BaseGenerator);
Generator.prototype.convertFile = function(content, input) {
var that = this;
var json = {
path: input,
progress: parse.progress(this.options.navigation, input)
};
return Q()
.then(function() {
return parse.page(content, {
repo: that.options.githubId,
dir: path.dirname(input) || '/',
outdir: './',
});
})
.then(function(sections) {
json.content = sections;
})
.then(function() {
that.pages[input] = json;
});
};
Generator.prototype.finish = function() {
var that = this;
var basePath = ".";
var output = path.join(this.options.output, "index.html");
return Q()
// Generate html
.then(function(pages) {
return that._writeTemplate(that.template, {
pages: that.pages,
basePath: basePath,
staticBase: path.join(basePath, "gitbook"),
}, output);
})
// Copy assets
.then(function() {
return fs.copy(
path.join(that.options.theme, "assets"),
path.join(that.options.output, "gitbook")
);
});
};
module.exports = Generator;
-56
View File
@@ -1,56 +0,0 @@
var util = require("util");
var path = require("path");
var Q = require("q");
var _ = require("lodash");
var exec = require('child_process').exec;
var fs = require("../fs");
var parse = require("../../parse");
var BaseGenerator = require("../page");
/*
* This generator inherits from the single page generator
* and convert the page output to pdf using gitbook-pdf
*/
var Generator = function() {
BaseGenerator.apply(this, arguments);
// Options for PDF generation
this.options = _.defaults(this.options, {
paperformat: "A4"
});
};
util.inherits(Generator, BaseGenerator);
Generator.prototype.finish = function() {
var that = this;
return BaseGenerator.prototype.finish.apply(this)
.then(function() {
var d = Q.defer();
var command = [
"gitbook-pdf",
"generate",
path.join(that.options.output, "index.html"),
path.join(that.options.output, "index.pdf"),
"--format="+that.options.paperformat
].join(" ");
exec(command, function (error, stdout, stderr) {
if (error) {
if (error.code == 127) {
error.message = "Need to install gitbook-pdf using: npm install gitbook-pdf -g";
} else {
error.message = error.message + " "+stdout;
}
return d.reject(error);
}
d.resolve();
});
return d.promise;
});
};
module.exports = Generator;
-156
View File
@@ -1,156 +0,0 @@
var util = require("util");
var path = require("path");
var Q = require("q");
var _ = require("lodash");
var swig = require('swig');
var fs = require("../fs");
var parse = require("../../parse");
var BaseGenerator = require("../generator");
var indexer = require('./search_indexer');
// Swig filter for returning the count of lines in a code section
swig.setFilter('lines', function(content) {
return content.split('\n').length;
});
// Swig filter for returning a link to the associated html file of a markdown file
swig.setFilter('mdLink', function(link) {
var link = link.replace(".md", ".html");
if (link == "README.html") link = "index.html";
return link;
});
var Generator = function() {
BaseGenerator.apply(this, arguments);
// Attach methods to instance
_.bindAll(this);
this.revision = Date.now();
this.indexer = indexer();
// Load base template
this.template = swig.compileFile(path.resolve(this.options.theme, 'templates/site.html'));
this.langsTemplate = swig.compileFile(path.resolve(this.options.theme, 'templates/langs.html'));
};
util.inherits(Generator, BaseGenerator);
// Generate a template
Generator.prototype._writeTemplate = function(tpl, options, output) {
var that = this;
return Q()
.then(function(sections) {
return tpl(_.extend({
revision: that.revision,
title: that.options.title,
description: that.options.description,
githubAuthor: that.options.github ? that.options.github.split("/")[0] : "",
githubId: that.options.github,
githubHost: that.options.githubHost,
summary: that.options.summary,
allNavigation: that.options.navigation
}, options));
})
.then(function(html) {
return fs.writeFile(
output,
html
);
});
};
Generator.prototype.indexPage = function(lexed, pagePath) {
this.indexer.add(lexed, pagePath);
return Q();
};
// Convert a markdown file to html
Generator.prototype.convertFile = function(content, _input) {
var that = this;
var progress = parse.progress(this.options.navigation, _input);
_output = _input.replace(".md", ".html");
if (_output == "README.html") _output = "index.html";
var input = path.join(this.options.input, _input);
var output = path.join(this.options.output, _output);
var basePath = path.relative(path.dirname(output), this.options.output) || ".";
return Q()
.then(function() {
// Lex page
return parse.lex(content);
})
.then(function(lexed) {
// Index page in search
return that.indexPage(lexed, _output)
.then(_.constant(lexed));
})
.then(function(lexed) {
// Get HTML generated sections
return parse.page(lexed, {
repo: that.options.githubId,
dir: path.dirname(_input) || '/',
outdir: path.dirname(_input) || '/',
});
})
.then(function(sections) {
return that._writeTemplate(that.template, {
progress: progress,
_input: _input,
content: sections,
basePath: basePath,
staticBase: path.join(basePath, "gitbook"),
}, output);
});
};
// Generate languages index
Generator.prototype.langsIndex = function(langs) {
var that = this;
var basePath = ".";
return this._writeTemplate(this.langsTemplate, {
langs: langs.list,
basePath: basePath,
staticBase: path.join(basePath, "gitbook"),
}, path.join(this.options.output, "index.html"))
.then(function() {
// Copy assets
return that.copyAssets();
});
};
// Copy assets
Generator.prototype.copyAssets = function() {
var that = this;
return fs.copy(
path.join(that.options.theme, "assets"),
path.join(that.options.output, "gitbook")
);
};
// Dump search index to disk
Generator.prototype.writeSearchIndex = function() {
return fs.writeFile(
path.join(this.options.output, 'search_index.json'),
this.indexer.dump()
);
};
Generator.prototype.finish = function() {
return this.copyAssets()
.then(this.writeSearchIndex);
};
module.exports = Generator;
-71
View File
@@ -1,71 +0,0 @@
var Q = require("q");
var _ = require("lodash");
var lunr = require('lunr');
var marked = require('marked');
var textRenderer = require('marked-text-renderer');
function Indexer() {
if(!(this instanceof Indexer)) {
return new Indexer();
}
_.bindAll(this);
// Setup lunr index
this.idx = lunr(function () {
this.ref('url');
this.field('title', { boost: 10 });
this.field('body');
});
this.renderer = textRenderer();
}
Indexer.prototype.text = function(nodes) {
// Copy section
var section = _.toArray(nodes);
// marked's Render expects this, we don't use it yet
section.links = {};
var options = _.extend({}, marked.defaults, {
renderer: this.renderer
});
return marked.parser(section, options);
};
Indexer.prototype.addSection = function(path, section) {
var url = [path, section.id].join('#');
var title = this.text(
_.filter(section, {'type': 'heading'})
);
var body = this.text(
_.omit(section, {'type': 'heading'})
);
// Add to lunr index
this.idx.add({
url: url,
title: title,
body: body,
});
};
Indexer.prototype.add = function(lexedPage, url) {
var sections = lexedPage;
_.map(sections, _.partial(this.addSection, url));
};
Indexer.prototype.dump = function() {
return JSON.stringify(this.idx);
};
// Exports
module.exports = Indexer;
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
parse: require('./parse/'),
generate: require('./generate/')
};
-9
View File
@@ -1,9 +0,0 @@
module.exports = {
summary: require('./summary'),
langs: require('./langs'),
page: require('./page'),
lex: require('./lex'),
progress: require('./progress'),
navigation: require('./navigation'),
readme: require('./readme')
};
-23
View File
@@ -1,23 +0,0 @@
var _ = require("lodash")
var parseSummary = require("./summary");
var parseLangs = function(content) {
var summary = parseSummary(content);
return {
list: _.chain(summary.chapters)
.filter(function(entry) {
return entry.path != null;
})
.map(function(entry) {
return {
title: entry.title,
path: entry.path,
lang: entry.path.replace("/", "")
};
})
.value()
}
};
module.exports = parseLangs;
-125
View File
@@ -1,125 +0,0 @@
var _ = require('lodash');
var marked = require('marked');
// Split a page up into sections (lesson, exercises, ...)
function splitSections(nodes) {
var section = [];
return _.reduce(nodes, function(sections, el) {
if(el.type === 'hr') {
sections.push(section);
section = [];
} else {
section.push(el);
}
return sections;
}, []).concat([section]); // Add remaining nodes
}
function isQuizNode(node) {
return (/^[(\[][ x][)\]]/).test(node.text || node);
}
function isExercise(nodes) {
var codeType = { type: 'code' };
// Number of code nodes in section
var len = _.filter(nodes, codeType).length;
return (
// Got 3 or 4 code blocks
(len === 3 || len === 4) &&
// Ensure all nodes are at the end
_.all(_.last(nodes, len), codeType)
);
}
function isQuiz(nodes) {
if (nodes.length < 3) {
return false;
}
// Support having a first paragraph block
// before our series of questions
var quizNodes = nodes.slice(nodes[0].type === 'paragraph' ? 1 : 0);
// No questions
if (!_.some(quizNodes, { type: 'blockquote_start' })) {
return false;
}
// Check if section has list of questions
// or table of questions
var listIdx = _.findIndex(quizNodes, { type: 'list_item_start' });
var tableIdx = _.findIndex(quizNodes, { type: 'table' });
if(
// List of questions
listIdx !== -1 && isQuizNode(quizNodes[listIdx + 1]) ||
// Table of questions
(
tableIdx !== -1 &&
_.every(quizNodes[tableIdx].cells[0].slice(1), isQuizNode)
)
) {
return true;
}
return false;
}
// What is the type of this section
function sectionType(nodes, idx) {
if(isExercise(nodes)) {
return 'exercise';
} else if(isQuiz(nodes)) {
return 'quiz';
}
return 'normal';
}
// Generate a uniqueId to identify this section in our code
function sectionId(section, idx) {
return _.uniqueId('gitbook_');
}
function lexPage(src) {
// Lex file
var nodes = marked.lexer(src);
return _.chain(splitSections(nodes))
.map(function(section, idx) {
// Detect section type
section.type = sectionType(section, idx);
return section;
})
.map(function(section, idx) {
// Give each section an ID
section.id = sectionId(section, idx);
return section;
})
.filter(function(section) {
return !_.isEmpty(section);
})
.reduce(function(sections, section) {
var last = _.last(sections);
// Merge normal sections together
if(last && last.type === section.type && last.type === 'normal') {
last.push.apply(last, [{'type': 'hr'}].concat(section));
} else {
// Add to list of sections
sections.push(section);
}
return sections;
}, [])
.value();
}
// Exports
module.exports = lexPage;
-70
View File
@@ -1,70 +0,0 @@
var _ = require('lodash');
// Cleans up an article/chapter object
// remove 'articles' attributes
function clean(obj) {
return obj && _.omit(obj, ['articles']);
}
function flattenChapters(chapters) {
return _.reduce(chapters, function(accu, chapter) {
return accu.concat([clean(chapter)].concat(chapter.articles));
}, []);
}
// Returns from a summary a map of
/*
{
"file/path.md": {
prev: ...,
next: ...,
},
...
}
*/
function navigation(summary, files) {
// Support single files as well as list
files = _.isArray(files) ? files : (_.isString(files) ? [files] : null);
// Special README nav
var README_NAV = {
path: 'README.md',
title: 'Introduction',
level: '0',
};
// List of all navNodes
var navNodes = [README_NAV].concat(flattenChapters(summary.chapters));
var prevNodes = [null].concat(navNodes.slice(0, -1));
var nextNodes = navNodes.slice(1).concat([null]);
// Mapping of prev/next for a give path
var mapping = _.chain(_.zip(navNodes, prevNodes, nextNodes))
.map(function(nodes) {
var current = nodes[0], prev = nodes[1], next = nodes[2];
// Skip if no path
if(!current.path) return null;
return [current.path, {
title: current.title,
prev: prev,
next: next,
level: current.level,
}];
})
.filter()
.object()
.value();
// Filter for only files we want
if(files) {
return _.pick(mapping, files);
}
return mapping;
}
// Exports
module.exports = navigation;
-156
View File
@@ -1,156 +0,0 @@
var _ = require('lodash');
var marked = require('marked');
var hljs = require('highlight.js');
var lex = require('./lex');
var renderer = require('./renderer');
var lnormalize = require('../utils/lang').normalize;
// Render a section using our custom renderer
function render(section, _options) {
// Copy section
section = _.toArray(section);
// marked's Render expects this, we don't use it yet
section.links = {};
// Build options using defaults and our custom renderer
var options = _.extend({}, marked.defaults, {
renderer: renderer(null, _options),
// Synchronous highlighting with highlight.js
highlight: function (code, lang) {
if(!lang) return code;
// Normalize lang
lang = lnormalize(lang);
try {
return hljs.highlight(lang, code).value;
} catch(e) { }
return code;
}
});
return marked.parser(section, options);
}
function quizQuestion(node) {
if (node.text) {
node.text = node.text.replace(/^([\[(])x([\])])/, "$1 $2");
} else {
return node.replace(/^([\[(])x([\])])/, "$1 $2");
}
}
function parsePage(src, options) {
options = options || {};
// Lex if not already lexed
return (_.isArray(src) ? src : lex(src))
.map(function(section) {
// Transform given type
if(section.type === 'exercise') {
var nonCodeNodes = _.reject(section, {
'type': 'code'
});
var codeNodes = _.filter(section, {
'type': 'code'
});
// Languages in code blocks
var langs = _.pluck(codeNodes, 'lang').map(lnormalize);
// Check that they are all the same
var validLangs = _.all(_.map(langs, function(lang) {
return lang && lang === langs[0];
}));
// Main language
var lang = validLangs ? langs[0] : null;
return {
id: section.id,
type: section.type,
content: render(nonCodeNodes),
lang: lang,
code: {
base: codeNodes[0].text,
solution: codeNodes[1].text,
validation: codeNodes[2].text,
// Context is optional
context: codeNodes[3] ? codeNodes[3].text : null,
}
};
} else if (section.type === 'quiz') {
var quiz = [], question, foundFeedback = false;
var nonQuizNodes = section[0].type === 'paragraph' && section[1].type !== 'list_start' ? [section[0]] : [];
var quizNodes = section.slice(0);
quizNodes.splice(0, nonQuizNodes.length);
for (var i = 0; i < quizNodes.length; i++) {
var node = quizNodes[i];
if (question && (((node.type === 'list_end' || node.type === 'blockquote_end') && i === quizNodes.length - 1)
|| node.type === 'table' || (node.type === 'paragraph' && !foundFeedback))) {
quiz.push({
base: render(question.questionNodes),
solution: render(question.solutionNodes),
feedback: render(question.feedbackNodes)
});
}
if (node.type === 'table' || (node.type === 'paragraph' && !foundFeedback)) {
question = { questionNodes: [], solutionNodes: [], feedbackNodes: [] };
}
if (node.type === 'blockquote_start') {
foundFeedback = true;
} else if (node.type === 'blockquote_end') {
foundFeedback = false;
}
if (node.type === 'table') {
question.solutionNodes.push(_.cloneDeep(node));
node.cells = node.cells.map(function(row) {
return row.map(quizQuestion);
});
question.questionNodes.push(node);
} else if (!/blockquote/.test(node.type)) {
if (foundFeedback) {
question.feedbackNodes.push(node);
} else if (node.type === 'paragraph' || node.type === 'text'){
question.solutionNodes.push(_.cloneDeep(node));
quizQuestion(node);
question.questionNodes.push(node);
} else {
question.solutionNodes.push(node);
question.questionNodes.push(node);
}
}
}
return {
id: section.id,
type: section.type,
content: render(nonQuizNodes),
quiz: quiz
};
}
// Render normal pages
return {
id: section.id,
type: section.type,
content: render(section, options)
};
});
}
// Exports
module.exports = parsePage;
-50
View File
@@ -1,50 +0,0 @@
var _ = require("lodash");
// Returns from a navigation and a current file, a snapshot of current detailed state
var calculProgress = function(navigation, current) {
var n = _.size(navigation);
var percent = 0, prevPercent = 0, currentChapter = null;
var done = true;
var chapters = _.chain(navigation)
.map(function(nav, path) {
nav.path = path;
return nav;
})
.sortBy(function(nav) {
return nav.level;
})
.map(function(nav, i) {
// Calcul percent
nav.percent = (i * 100) / Math.max((n - 1), 1);
// Is it done
nav.done = done;
if (nav.path == current) {
currentChapter = nav;
percent = nav.percent;
done = false;
} else if (done) {
prevPercent = nav.percent;
}
return nav;
})
.value();
return {
// Previous percent
prevPercent: prevPercent,
// Current percent
percent: percent,
// List of chapter with progress
chapters: chapters,
// Current chapter
current: currentChapter
};
}
module.exports = calculProgress;
-32
View File
@@ -1,32 +0,0 @@
var _ = require('lodash');
var marked = require('marked');
function extractFirstNode(nodes, nType) {
return _.chain(nodes)
.filter(function(node) {
return node.type == nType;
})
.pluck("text")
.first()
.value();
}
function parseReadme(src) {
var nodes, title, description;
// Parse content
nodes = marked.lexer(src);
var title = extractFirstNode(nodes, "heading");
var description = extractFirstNode(nodes, "paragraph");
return {
title: title,
description: description
};
}
// Exports
module.exports = parseReadme;
-121
View File
@@ -1,121 +0,0 @@
var url = require('url');
var inherits = require('util').inherits;
var path = require('path');
var marked = require('marked');
var rendererId = 0;
function GitBookRenderer(options, extra_options) {
if(!(this instanceof GitBookRenderer)) {
return new GitBookRenderer(options, extra_options);
}
GitBookRenderer.super_.call(this, options);
this._extra_options = extra_options;
this.quizRowId = 0;
this.id = rendererId++;
}
inherits(GitBookRenderer, marked.Renderer);
GitBookRenderer.prototype._unsanitized = function(href) {
var prot = '';
try {
prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase();
} catch (e) {
return true;
}
if(prot.indexOf('javascript:') === 0) {
return true;
}
return false;
};
GitBookRenderer.prototype.link = function(href, title, text) {
// Don't build if it looks malicious
if (this.options.sanitize && this._unsanitized(href)) {
return '';
}
// Parsed version of the url
var parsed = url.parse(href);
var o = this._extra_options;
// Relative link, rewrite it to point to github repo
if(!parsed.protocol && parsed.path && parsed.path[0] != '/' && o && o.repo && o.dir) {
href = url.resolve('https://github.com/' + o.repo + '/blob/', [o.dir, href].join("/"));
parsed = url.parse(href);
}
// Generate HTML for link
var out = '<a href="' + parsed.href + '"';
// Title if no null
if (title) {
out += ' title="' + title + '"';
}
// Target blank if external
if(parsed.protocol) {
out += ' target="_blank"';
}
out += '>' + text + '</a>';
return out;
};
GitBookRenderer.prototype.image = function(href, title, text) {
// Our "fixed" href
var _href = href;
// Parsed version of the url
var parsed = url.parse(href);
// Options
var o = this._extra_options;
// Relative image, rewrite it depending output
if(!parsed.protocol && parsed.path && parsed.path[0] != '/' && o && o.dir && o.outdir) {
// o.dir: directory parent of the file currently in rendering process
// o.outdir: directory parent from the html output
// Absolute file in source
_href = path.join(o.dir, _href);
// make it relative to output
_href = path.relative(o.outdir, _href);
}
return GitBookRenderer.super_.prototype.image.call(this, _href, title, text);
};
GitBookRenderer.prototype.tablerow = function(content) {
this.quizRowId += 1;
return GitBookRenderer.super_.prototype.tablerow(content);
};
var fieldRegex = /^([(\[])([ x])[\])]/;
GitBookRenderer.prototype._createCheckboxAndRadios = function(text) {
var match = fieldRegex.exec(text);
if (!match) {
return text;
}
var field = "<input name='quiz-row-" + this.id + "-" + this.quizRowId + "' type='";
field += match[1] === '(' ? "radio" : "checkbox";
field += match[2] === 'x' ? "' checked/>" : "'/>";
return text.replace(fieldRegex, field);
}
GitBookRenderer.prototype.tablecell = function(content, flags) {
return GitBookRenderer.super_.prototype.tablecell(this._createCheckboxAndRadios(content), flags);
};
GitBookRenderer.prototype.listitem = function(text) {
return GitBookRenderer.super_.prototype.listitem(this._createCheckboxAndRadios(text));
};
// Exports
module.exports = GitBookRenderer;
-117
View File
@@ -1,117 +0,0 @@
var _ = require('lodash');
var marked = require('marked');
// Utility function for splitting a list into groups
function splitBy(list, starter, ender) {
var starts = 0;
var ends = 0;
var group = [];
// Groups
return _.reduce(list, function(groups, value) {
// Ignore start and end delimiters in resulted groups
if(starter(value)) {
starts++;
} else if(ender(value)) {
ends++;
}
// Add current value to group
group.push(value);
// We've got a matching
if(starts === ends && starts !== 0) {
// Add group to end groups
// (remove starter and ender token)
groups.push(group.slice(1, -1));
// Reset group
group = [];
}
return groups;
}, []);
}
function listSplit(nodes, start_type, end_type) {
return splitBy(nodes, function(el) {
return el.type === start_type;
}, function(el) {
return el.type === end_type;
});
}
// Get the biggest list
// out of a list of marked nodes
function filterList(nodes) {
return _.chain(nodes)
.toArray()
.rest(function(el) {
// Get everything after list_start
return el.type !== 'list_start';
})
.reverse()
.rest(function(el) {
// Get everything after list_end (remember we're reversed)
return el.type !== 'list_end';
})
.reverse()
.value().slice(1, -1);
}
// Parses an Article or Chapter title
// supports extracting links
function parseTitle(src, nums) {
// Check if it's a link
var matches = marked.InlineLexer.rules.link.exec(src);
var level = nums.join('.');
// Not a link, return plain text
if(!matches) {
return {
title: src,
level: level,
path: null,
};
}
return {
title: matches[1],
level: level,
// Replace .md references with .html
path: matches[2],
};
}
function parseArticle(chapterNum, nodes, idx) {
return parseTitle(_.first(nodes).text, [chapterNum, idx+1]);
}
function parseChapter(nodes, idx) {
return _.extend(parseTitle(_.first(nodes).text, [idx+1]), {
articles: _.map(listSplit(filterList(nodes), 'list_item_start', 'list_item_end'), parseArticle.bind(null, idx+1))
});
}
function parseSummary(src) {
var nodes = marked.lexer(src);
// Get out list of chapters
var chapterList = filterList(nodes);
// Split out chapter sections
var chapters = _.chain(listSplit(chapterList, 'list_item_start', 'list_item_end'))
.map(parseChapter)
.value();
return {
chapters: chapters
};
}
// Exports
module.exports = parseSummary;
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
lang: require('./lang'),
};
-18
View File
@@ -1,18 +0,0 @@
var MAP = {
'py': 'python',
'js': 'javascript',
'rb': 'ruby',
};
function normalize(lang) {
if(!lang) { return null; }
var lower = lang.toLowerCase();
return MAP[lower] || lower;
}
// Exports
module.exports = {
normalize: normalize,
MAP: MAP
};
+83
View File
@@ -0,0 +1,83 @@
const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig(
{
env: {
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? 'development',
GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX,
},
webpack(config, { dev, webpack }) {
config.resolve.fallback = {
...config.resolve.fallback,
// Required for `swagger2openapi` to work:
fs: false,
path: false,
http: false,
};
// Tree shake debug code for Sentry
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-with-nextjs
if (!dev) {
config.plugins.push(
new webpack.DefinePlugin({
__SENTRY_DEBUG__: false,
// We always init Sentry with enableTracing: false for now, so this is useless
__SENTRY_TRACING__: false,
__RRWEB_EXCLUDE_IFRAME__: true,
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
}),
);
}
return config;
},
async headers() {
return [
// Cache all static assets for 1 year
{
source: '/~gitbook/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
assetPrefix: process.env.GITBOOK_ASSETS_PREFIX,
poweredByHeader: false,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.gitbook.io',
},
],
},
},
{
silent: true,
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
},
{
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
transpileClientSDK: false,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
tunnelRoute: '/~gitbook/monitoring',
// Don't hide source maps from generated client bundles
hideSourceMaps: false,
disableLogger: true,
automaticVercelMonitors: false,
},
);
+90 -52
View File
@@ -1,58 +1,96 @@
{
"name": "gitbook",
"version": "0.2.3",
"homepage": "http://www.gitbook.io/",
"description": "Library and cmd utility to generate GitBooks",
"main": "lib/index.js",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build:cloudflare": "next-on-pages",
"start": "next start",
"lint": "next lint",
"format": "prettier ./ --ignore-unknown --write",
"format:check": "prettier ./ --ignore-unknown --list-different",
"typecheck": "tsc --noEmit",
"unit": "bun test {src,packages}",
"e2e": "playwright test",
"changeset": "changeset",
"release": "changeset publish",
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
},
"workspaces": [
"packages/*"
],
"dependencies": {
"q": "1.0.1",
"lodash": "2.4.1",
"marked": "0.3.2",
"marked-text-renderer": "0.0.1",
"lunr": "0.5.2",
"swig": "1.3.2",
"send": "0.2.0",
"fstream-ignore": "0.0.7",
"commander": "2.2.0",
"fs-extra": "0.8.1",
"highlight.js": "8.0.0",
"tmp": "0.0.23"
"@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.56.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@sentry/nextjs": "^7.94.1",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.10",
"@upstash/redis": "^1.27.1",
"ajv": "^8.12.0",
"assert-never": "^1.2.1",
"bun-types": "^1.0.7",
"classnames": "^2.5.1",
"content-security-policy-merger": "^1.0.0",
"framer-motion": "^10.16.14",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
"katex": "^0.16.9",
"mathjax": "^3.2.2",
"memoizee": "^0.4.15",
"next": "^14.1.3",
"next-themes": "^0.2.1",
"nuqs": "^1.17.4",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"react": "^18",
"react-dom": "^18",
"react-hotkeys-hook": "^4.4.1",
"recoil": "^0.7.7",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"rison": "^0.1.1",
"server-only": "^0.0.1",
"shiki": "^1.2.0",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0"
},
"devDependencies": {
"mocha": "1.18.2",
"grunt": "~0.4.2",
"grunt-cli": "0.1.11",
"grunt-contrib-less": "~0.5.0",
"grunt-contrib-requirejs": "0.4.1"
},
"scripts": {
"test": "export TESTING=true; mocha --reporter list"
},
"bin": {
"gitbook": "./bin/gitbook.js"
},
"repository": {
"type": "git",
"url": "https://github.com/GitbookIO/gitbook.git"
},
"keywords": [
"git",
"book",
"gitbook"
],
"author": "FriendCode <contact@friendco.de>",
"license": "Apache 2",
"bugs": {
"url": "https://github.com/GitbookIO/gitbook/issues"
},
"contributors": [
{
"name": "Aaron O'Mullan",
"email": "aaron.omullan@gmail.com"
},
{
"name": "Samy Pessé",
"email": "samypesse@gmail.com"
}
]
"@argos-ci/playwright": "^2.0.0",
"@changesets/cli": "^2.27.7",
"@cloudflare/next-on-pages": "^1.11.3",
"@cloudflare/workers-types": "^4.20231218.0",
"@playwright/test": "^1.42.1",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/katex": "^0.16.5",
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/psi": "^4.1.6",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "13.5.6",
"eslint-plugin-import": "^2.29.0",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"prettier": "^3.0.3",
"psi": "^4.1.0",
"tailwindcss": "^3.4.0",
"typescript": "^5"
}
}
+1
View File
@@ -0,0 +1 @@
index.ts
+13
View File
@@ -0,0 +1,13 @@
# @gitbook/emoji-codepoints
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
+26
View File
@@ -0,0 +1,26 @@
import fs from 'fs';
import emojisRaws from 'emoji-assets/emoji.json';
interface EmojiData {
code_points: {
base: string;
fully_qualified: string;
};
}
const emojis = emojisRaws as Record<string, EmojiData>;
const output: Record<string, string> = {};
Object.entries(emojis).forEach(([key, value]) => {
const emoji = value.code_points?.fully_qualified;
if (emoji && key !== emoji) {
output[key] = emoji;
} else if (!emoji) {
console.log('No emoji for', key);
}
});
fs.writeFileSync(
'index.ts',
`export const emojiCodepoints: Record<string, string> = ${JSON.stringify(output, null, 4)};`,
);
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@gitbook/emoji-codepoints",
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
"version": "0.2.0",
"private": true,
"exports": "./index.ts",
"dependencies": {},
"devDependencies": {
"emoji-assets": "^8.0.0"
},
"scripts": {
"postinstall": "bun ./build.ts"
}
}
+13
View File
@@ -0,0 +1,13 @@
# @gitbook/react-contentkit
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
+13
View File
@@ -0,0 +1,13 @@
{
"name": "@gitbook/react-contentkit",
"version": "0.2.0",
"exports": "./src/index.ts",
"dependencies": {
"classnames": "^2.5.1",
"@gitbook/api": "^0.51.0",
"assert-never": "^1.2.1"
},
"peerDependencies": {
"react": "*"
}
}
@@ -0,0 +1,190 @@
'use client';
import type {
ContentKitAction,
ContentKitRenderOutput,
RequestRenderIntegrationUI,
} from '@gitbook/api';
import React from 'react';
import {
ContentKitClientContext,
ContentKitClientContextType,
ContentKitRenderUpdate,
ContentKitSecurity,
} from './context';
import { resolveDynamicBinding } from './dynamic';
type ContentKitLifecycleMode = ContentKitRenderOutput['element']['type'];
/**
* Render a ContentKit component.
* The approach is optimized to work well with server components and
* allow rendering the components in the server with the lifecycle managed on the client side.
*/
export function ContentKit(props: {
security: ContentKitSecurity;
/** Initial input being displayed */
initialInput: RequestRenderIntegrationUI;
/** Initial output being displayed */
initialOutput: ContentKitRenderOutput;
/** Initial state to display */
children?: React.ReactNode;
/** Render a new state */
render: (request: RequestRenderIntegrationUI) => Promise<{
children: React.ReactNode;
output: ContentKitRenderOutput;
}>;
/** Callback when an action is triggered */
onAction?: (action: ContentKitAction) => void;
}) {
const {
security,
initialInput,
initialOutput,
children: initialChildren,
render,
onAction,
} = props;
const [current, setCurrent] = React.useState({
/** Current input being rendered */
input: initialInput,
/** React rendered elements for the input */
children: initialChildren,
/** Output of the rendering */
output: initialOutput,
/** Local state */
state: initialOutput.state ?? {},
});
const [subView, setSubView] = React.useState<null | {
mode: ContentKitLifecycleMode;
initialInput: RequestRenderIntegrationUI;
initialOutput: ContentKitRenderOutput;
initialChildren: React.ReactNode;
}>(null);
const update = React.useCallback(
async (update: ContentKitRenderUpdate) => {
const newInput = {
...current.input,
// Use the props from the output if output sent new props
...(current.output?.props ? { props: current.output.props } : {}),
...update,
// Merge the state
state: {
...current.input.state,
...current.state,
...update.state,
},
};
console.log('transition to input', newInput);
const result = await render(newInput);
console.log('and got output', result.output, 'for', newInput);
setCurrent((prev) => ({
input: newInput,
children: result.children,
output: result.output,
state: prev.state,
}));
},
[setCurrent, current, render],
);
const renderer = React.useMemo<ContentKitClientContextType>(() => {
return {
security,
state: current.state,
setState: (newState) => {
setCurrent((latest) => ({
...latest,
state: {
...latest.state,
...newState,
},
}));
},
update,
dispatchAction: async (inputAction, bubble = true) => {
const action = resolveDynamicBinding<ContentKitAction>(current.state, inputAction);
if (bubble) {
onAction?.(action);
}
console.log('action', action);
switch (action.action) {
case '@ui.modal.open': {
const modalInput: RequestRenderIntegrationUI = {
componentId: action.componentId,
props: action.props,
context: current.input.context,
action,
};
// Prefetch the modal content to show a loading in the button opening the button
const result = await render(modalInput);
setSubView({
mode: 'modal',
initialInput: modalInput,
initialOutput: result.output,
initialChildren: result.children,
});
break;
}
case '@ui.url.open': {
window.open(action.url, '_blank');
break;
}
default: {
await update({
action,
});
break;
}
}
},
};
}, [update, security, current.state, current.input.context, setCurrent, render]);
const onSubViewAction = React.useCallback(async (action: ContentKitAction) => {
switch (action.action) {
case '@ui.modal.close': {
update({
action,
});
setSubView(null);
break;
}
}
}, []);
return (
<>
<ContentKitClientContext.Provider value={renderer}>
{current.children}
</ContentKitClientContext.Provider>
{subView ? (
<ContentKit
security={security}
initialInput={subView.initialInput}
initialOutput={subView.initialOutput}
render={render}
onAction={onSubViewAction}
>
{subView.initialChildren}
</ContentKit>
) : null}
</>
);
}
@@ -0,0 +1,23 @@
import { type ContentKitRenderOutput } from '@gitbook/api';
import { Element } from './Element';
import { ContentKitServerContext } from './types';
/**
* Generic component to render a ContentKit output.
* The component can be used both as a client and server one.
*/
export function ContentKitOutput(props: {
context: ContentKitServerContext;
output: ContentKitRenderOutput;
}) {
const { output, context } = props;
return (
<>
{process.env.NODE_ENV === 'development' ? (
<pre style={{ display: 'none' }}>{JSON.stringify(props.output, null, 2)}</pre>
) : null}
<Element element={output.element} context={context} state={output.state} />
</>
);
}
+201
View File
@@ -0,0 +1,201 @@
import {
ContentKitDescendantElement,
ContentKitInlineElement,
ContentKitRootElement,
} from '@gitbook/api';
import { ElementBlock } from './ElementBlock';
import { ElementWebframe } from './ElementWebframe';
import { ContentKitServerContext } from './types';
import { ElementCard } from './ElementCard';
import { ElementIcon } from './ElementIcon';
import { ElementCodeBlock } from './ElementCodeBlock';
import { ElementImage } from './ElementImage';
import { ElementStack } from './ElementStack';
import { ElementText } from './ElementText';
import React from 'react';
import { ElementButton } from './ElementButton';
import { ElementModal } from './ElementModal';
import { ElementBox } from './ElementBox';
import { ElementMarkdown } from './ElementMarkdown';
import { ElementTextInput } from './ElementTextInput';
import { ElementDivider } from './ElementDivider';
export function Element(props: {
element: ContentKitDescendantElement | ContentKitRootElement | ContentKitInlineElement;
context: ContentKitServerContext;
state: object;
}) {
const { element, context, state } = props;
switch (element.type) {
case 'text':
return (
<ElementText element={element} context={context} state={state}>
<Elements elements={element.children} context={context} state={state} />
</ElementText>
);
case 'block':
return (
<ElementBlock element={element} context={context} state={state}>
<Elements elements={element.children} context={context} state={state} />
</ElementBlock>
);
case 'box':
return (
<ElementBox element={element} context={context} state={state}>
<Elements elements={element.children} context={context} state={state} />
</ElementBox>
);
case 'divider':
return <ElementDivider element={element} context={context} state={state} />;
case 'markdown':
return <ElementMarkdown element={element} context={context} state={state} />;
case 'modal': {
const content = (
<ElementModal
element={element}
subtitle={
element.subtitle ? (
<Elements
elements={ensureStackElements('hstack', element.subtitle, 'start')}
context={context}
state={state}
/>
) : null
}
>
<Elements elements={element.children} context={context} state={state} />
</ElementModal>
);
if (context.modalWrapper) {
const ModalWrapper = context.modalWrapper;
return <ModalWrapper>{content}</ModalWrapper>;
}
return content;
}
case 'image': {
return <ElementImage element={element} context={context} state={state} />;
}
case 'button': {
return (
<ElementButton
element={element}
icon={
element.icon ? <ElementIcon icon={element.icon} context={context} /> : null
}
trailingIcon={
element.trailingIcon ? (
<ElementIcon icon={element.trailingIcon} context={context} />
) : null
}
/>
);
}
case 'textinput': {
return <ElementTextInput element={element} />;
}
case 'hstack':
case 'vstack':
return (
<ElementStack element={element} context={context} state={state}>
<Elements elements={element.children} context={context} state={state} />
</ElementStack>
);
case 'card':
return (
<ElementCard
element={element}
icon={
element.icon ? (
typeof element.icon === 'string' ? (
<ElementIcon icon={element.icon} context={context} />
) : (
<Element element={element.icon} context={context} state={state} />
)
) : null
}
hint={
element.hint ? (
typeof element.hint === 'string' ? (
element.hint
) : (
<Elements
elements={ensureStackElements('hstack', element.hint, 'start')}
context={context}
state={state}
/>
)
) : null
}
buttons={
element.buttons
? element.buttons.map((button, index) => (
<Element
key={index}
element={button}
context={context}
state={state}
/>
))
: []
}
>
{element.children ? (
<Elements elements={element.children} context={context} state={state} />
) : null}
</ElementCard>
);
case 'codeblock':
return <ElementCodeBlock element={element} context={context} state={state} />;
case 'webframe':
return <ElementWebframe element={element} />;
default:
return (
<pre
style={{ display: process.env.NODE_ENV === 'development' ? 'block' : 'hidden' }}
>
ContentKit element not implemented "{element.type}":{' '}
{JSON.stringify(element, null, 2)}
</pre>
);
}
}
function Elements(props: {
elements: string | Array<string | ContentKitDescendantElement>;
context: ContentKitServerContext;
state: object;
}) {
const { context, state } = props;
const elements = Array.isArray(props.elements) ? props.elements : [props.elements];
return (
<>
{elements.map((element, index) => {
if (typeof element === 'string') {
return <React.Fragment key={index}>{element}</React.Fragment>;
}
return <Element key={index} element={element} context={context} state={state} />;
})}
</>
);
}
/**
* Ensure a set of elements are going to be rendered as a stack (either hstack or vstack).
* If the elements are already a stack, return them as is.
*/
function ensureStackElements(
type: 'hstack' | 'vstack',
elements: ContentKitDescendantElement[],
align?: 'start' | 'center' | 'end',
): ContentKitDescendantElement[] {
if (elements.length === 1) {
return elements;
}
return [{ type, align, children: elements }];
}
@@ -0,0 +1,11 @@
import { ContentKitBlock } from '@gitbook/api';
import { ContentKitServerElementProps } from './types';
export function ElementBlock(
props: React.PropsWithChildren<ContentKitServerElementProps<ContentKitBlock>>,
) {
const { element, children } = props;
return <>{children}</>;
}
@@ -0,0 +1,11 @@
import { ContentKitBox } from '@gitbook/api';
import { ContentKitServerElementProps } from './types';
export function ElementBox(
props: React.PropsWithChildren<ContentKitServerElementProps<ContentKitBox>>,
) {
const { element, children } = props;
return <div style={{ flexGrow: element.grow ?? 0 }}>{children}</div>;
}
@@ -0,0 +1,53 @@
'use client';
import { ContentKitButton } from '@gitbook/api';
import { ContentKitClientElementProps } from './types';
import classNames from 'classnames';
import { useContentKitClientContext } from './context';
import React from 'react';
export function ElementButton(
props: ContentKitClientElementProps<ContentKitButton> & {
icon: React.ReactNode | null;
trailingIcon: React.ReactNode | null;
},
) {
const { element, icon, trailingIcon } = props;
const clientContext = useContentKitClientContext();
const [loading, setLoading] = React.useState(false);
// TODO:
// - loading
// - confirm
return (
<button
title={element.tooltip}
className={classNames(
'contentkit-button',
`contentkit-button-style-${element.style ?? 'secondary'}`,
loading ? 'contentkit-button-loading' : null,
)}
onClick={(event) => {
if (element.disabled || loading) {
return;
}
event.stopPropagation();
setLoading(true);
clientContext.dispatchAction(element.onPress).finally(() => {
setLoading(false);
});
}}
>
{icon}
{element.label ? (
<span className="contentkit-button-label">{element.label}</span>
) : null}
{trailingIcon}
</button>
);
}
@@ -0,0 +1,55 @@
'use client';
import React from 'react';
import classNames from 'classnames';
import { ContentKitClientElementProps } from './types';
import { ContentKitCard } from '@gitbook/api';
import { useContentKitClientContext } from './context';
/**
* Interactive card element.
*/
export function ElementCard(
props: React.PropsWithChildren<
ContentKitClientElementProps<ContentKitCard> & {
icon: React.ReactNode | null;
hint: React.ReactNode | null;
buttons: React.ReactNode[];
}
>,
) {
const { element, children, icon, hint, buttons } = props;
const clientContext = useContentKitClientContext();
return (
<div
className={classNames(
'contentkit-card',
element.onPress ? 'contentkit-card-pressable' : null,
)}
onClick={() => {
if (element.onPress) {
clientContext.dispatchAction(element.onPress);
}
}}
>
{element.title ? (
<div className={classNames('contentkit-card-header')}>
{icon ? <div className={classNames('contentkit-card-icon')}>{icon}</div> : null}
<div className={classNames('contentkit-card-header-content')}>
<div className={classNames('contentkit-card-title')}>{element.title}</div>
{hint ? (
<div className={classNames('contentkit-card-hint')}>{hint}</div>
) : null}
</div>
{buttons && buttons.length > 0 ? (
<div className={classNames('contentkit-card-buttons')}>{buttons}</div>
) : null}
</div>
) : null}
{children ? <div className={classNames('contentkit-card-body')}>{children}</div> : null}
</div>
);
}
@@ -0,0 +1,22 @@
import { ContentKitCodeBlock } from '@gitbook/api';
import { ContentKitServerElementProps } from './types';
import { resolveDynamicBinding } from './dynamic';
import { DefaultCodeBlock, ElementCodeBlockClient } from './ElementCodeBlockClient';
export function ElementCodeBlock(props: ContentKitServerElementProps<ContentKitCodeBlock>) {
const { element, context, state } = props;
const C = context.codeBlock ?? DefaultCodeBlock;
const initialCode = resolveDynamicBinding(state, element.content);
return (
<ElementCodeBlockClient element={element}>
<C
code={initialCode}
syntax={element.syntax ?? 'plain'}
lineNumbers={element.lineNumbers ?? true}
/>
</ElementCodeBlockClient>
);
}
@@ -0,0 +1,28 @@
import { ContentKitCodeBlock } from '@gitbook/api';
import { ContentKitClientElementProps } from './types';
export function ElementCodeBlockClient(
props: ContentKitClientElementProps<ContentKitCodeBlock> & {
/**
* Render a code block on the server as a fallback and swap only when needed.
*/
children: React.ReactNode;
},
) {
const { element, children } = props;
return <>{children}</>;
}
export function DefaultCodeBlock(props: {
code: string;
syntax: string;
lineNumbers: number | boolean;
}) {
return (
<pre>
<code>{props.code}</code>
</pre>
);
}
@@ -0,0 +1,17 @@
import { ContentKitDivider } from '@gitbook/api';
import { ContentKitServerElementProps } from './types';
import classNames from 'classnames';
export function ElementDivider(props: ContentKitServerElementProps<ContentKitDivider>) {
const { element } = props;
return (
<div
className={classNames(
'contentkit-divider',
`contentkit-divider-${element.size ?? 'medium'}`,
)}
/>
);
}
@@ -0,0 +1,12 @@
import { ContentKitIcon } from '@gitbook/api';
import { ContentKitServerContext } from './types';
export function ElementIcon(props: { icon: ContentKitIcon; context: ContentKitServerContext }) {
const { icon, context } = props;
const C = context.icons[icon];
if (!C) {
return <span className="contentkit-icon" />;
}
return <C className="contentkit-icon" />;
}
@@ -0,0 +1,11 @@
import { ContentKitImage } from '@gitbook/api';
import { ContentKitServerElementProps } from './types';
export function ElementImage(props: ContentKitServerElementProps<ContentKitImage>) {
const { element } = props;
// TODO: do block display with aspect ratio
return <img src={element.source.url} className="contentkit-image" />;
}
@@ -0,0 +1,27 @@
import { ContentKitMarkdown } from '@gitbook/api';
import { ContentKitServerElementProps } from './types';
import { ElementMarkdownClient } from './ElementMarkdownClient';
import { resolveDynamicBinding } from './dynamic';
export function ElementMarkdown(props: ContentKitServerElementProps<ContentKitMarkdown>) {
const { element, context, state } = props;
const Markdown = context.markdown;
const initialMarkdown = resolveDynamicBinding(state, element.content);
async function renderMarkdown(markdown: string) {
'use server';
return <Markdown className="contentkit-markdown" markdown={markdown} />;
}
return (
<ElementMarkdownClient
element={element}
initialMarkdown={initialMarkdown !== element.content ? initialMarkdown : undefined}
renderMarkdown={renderMarkdown}
>
<Markdown className="contentkit-markdown" markdown={initialMarkdown} />
</ElementMarkdownClient>
);
}
@@ -0,0 +1,52 @@
'use client';
import { ContentKitMarkdown } from '@gitbook/api';
import { ContentKitClientElementProps } from './types';
import { resolveDynamicBinding } from './dynamic';
import React from 'react';
import { useContentKitClientContext } from './context';
/**
* Client component to render the default markdown output and then update it on state update.
*/
export function ElementMarkdownClient(
props: ContentKitClientElementProps<ContentKitMarkdown> & {
initialMarkdown: string | undefined;
renderMarkdown: (markdown: string) => React.ReactNode | Promise<React.ReactNode>;
children: React.ReactNode;
},
) {
const {
element,
initialMarkdown = element.content,
renderMarkdown,
children: initialChildren,
} = props;
const [children, setChildren] = React.useState<React.ReactNode>(null);
const context = useContentKitClientContext();
const markdown = resolveDynamicBinding(context.state, element.content);
React.useEffect(() => {
if (initialMarkdown === markdown) {
setChildren(null);
return;
}
let cancelled = false;
(async () => {
const parsed = await renderMarkdown(markdown);
if (!cancelled) {
setChildren(parsed);
}
})();
return () => {
cancelled = true;
};
}, [initialMarkdown, markdown]);
return <>{children || initialChildren}</>;
}
@@ -0,0 +1,57 @@
'use client';
import { ContentKitModal } from '@gitbook/api';
import { ContentKitClientElementProps } from './types';
import classNames from 'classnames';
import { useContentKitClientContext } from './context';
import React from 'react';
export function ElementModal(
props: ContentKitClientElementProps<ContentKitModal> & {
subtitle: React.ReactNode | null;
children: React.ReactNode;
},
) {
const { element, subtitle, children } = props;
const clientContext = useContentKitClientContext();
// TODO:
// - close button
// - invalid rendering on close?
// - submit
const [opened, setOpened] = React.useState(false);
React.useEffect(() => {
setOpened(true);
}, []);
const onClose = async () => {
await clientContext.dispatchAction({
action: '@ui.modal.close',
returnValue: element.returnValue || {},
});
};
return (
<div className={classNames('contentkit-modal-backdrop')} onClick={onClose}>
<div
className={classNames(
'contentkit-modal',
opened ? 'contentkit-modal-opened' : null,
)}
onClick={(event) => {
event.stopPropagation();
}}
>
<div className={classNames('contentkit-modal-header')}>
{element.title ? (
<h1 className={classNames('contentkit-modal-title')}>{element.title}</h1>
) : null}
{subtitle ? <div className="contentkit-modal-subtitle">{subtitle}</div> : null}
</div>
<div className={classNames('contentkit-modal-body')}>{children}</div>
</div>
</div>
);
}
@@ -0,0 +1,24 @@
import { ContentKitHStack, ContentKitVStack } from '@gitbook/api';
import { ContentKitServerElementProps } from './types';
import classNames from 'classnames';
export function ElementStack(
props: React.PropsWithChildren<
ContentKitServerElementProps<ContentKitHStack | ContentKitVStack>
>,
) {
const { element, children } = props;
return (
<div
className={classNames(
'contentkit-stack',
`contentkit-${element.type}`,
`contentkit-stack-align-${element.align ?? 'start'}`,
)}
>
{children}
</div>
);
}
@@ -0,0 +1,21 @@
import { ContentKitText } from '@gitbook/api';
import { ContentKitServerElementProps } from './types';
import classNames from 'classnames';
export function ElementText(
props: React.PropsWithChildren<ContentKitServerElementProps<ContentKitText>>,
) {
const { element, children } = props;
return (
<span
className={classNames(
'contentkit-text',
element.style ? `contentkit-text-${element.style}` : null,
)}
>
{children}
</span>
);
}
@@ -0,0 +1,46 @@
'use client';
import { ContentKitTextInput } from '@gitbook/api';
import { ContentKitClientElementProps } from './types';
import classNames from 'classnames';
import { useContentKitClientContext } from './context';
import React from 'react';
import { getStateStringValue } from './dynamic';
export function ElementTextInput(props: ContentKitClientElementProps<ContentKitTextInput>) {
const { element } = props;
const clientContext = useContentKitClientContext();
const value =
getStateStringValue(clientContext.state, element.state) ?? element.initialValue ?? '';
const onChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = (event) => {
clientContext.setState({
[element.state]: event.target.value,
});
};
if (element.multiline) {
return (
<textarea
disabled={element.disabled}
className={classNames('contentkit-textinput')}
value={value}
placeholder={element.placeholder}
onChange={onChange}
/>
);
}
return (
<input
type={element.inputType ?? 'text'}
disabled={element.disabled}
className={classNames('contentkit-textinput')}
value={value}
placeholder={element.placeholder}
onChange={onChange}
/>
);
}
@@ -0,0 +1,167 @@
'use client';
import { ContentKitWebFrame } from '@gitbook/api';
import React from 'react';
import { useContentKitClientContext } from './context';
import { ContentKitClientElementProps } from './types';
import { resolveDynamicBinding } from './dynamic';
export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWebFrame>) {
const { element } = props;
const renderer = useContentKitClientContext();
const iframeRef = React.useRef<HTMLIFrameElement>(null);
const [size, setSize] = React.useState<{
maxWidth?: number;
maxHeight?: number;
aspectRatio?: number;
}>({});
const readyRef = React.useRef(false);
const messagesQueueRef = React.useRef<object[]>([]);
const sendMessage = React.useCallback(
(message: object) => {
if (!iframeRef.current) {
return;
}
const target = new URL(element.source.url);
// For security reasons, only iframe from our integrations domains are allowed
// to send and receive messages
if (!renderer.security.firstPartyDomains.includes(target.host)) {
return;
}
if (readyRef.current) {
iframeRef.current.contentWindow!.postMessage(
message,
`${target.protocol}//${target.host}`,
);
} else {
messagesQueueRef.current.push(message);
}
},
[renderer.security],
);
//
// Listen to message coming from the webframe
//
React.useEffect(() => {
const callback = (event: MessageEvent) => {
if (!iframeRef.current) {
return;
}
const message = event.data;
const origin = new URL(event.origin);
// For security reasons, only iframe from our integrations domains are allowed
// to send and receive messages
if (!renderer.security.firstPartyDomains.includes(origin.host) && 0) {
return;
}
const contentWindow = iframeRef.current.contentWindow;
// Discard any messages coming other potential webframes on the page.
if (!contentWindow || event.source !== contentWindow) {
return;
}
if (typeof message === 'string') {
try {
// We support the default oembed iframe protocol
// https://docs.embed.ly/reference/provider-height-resizing
const parsed = JSON.parse(message);
if (parsed.context === 'iframe.resize' && typeof parsed.height === 'number') {
const width = contentWindow.outerWidth;
const height = parsed.height;
setSize({
maxWidth: width,
aspectRatio: width / height,
maxHeight: height,
});
}
} catch (err) {
return;
}
}
if (message.action) {
switch (message.action.action) {
case '@webframe.ready':
readyRef.current = true;
messagesQueueRef.current.forEach((message) => {
sendMessage(message);
});
messagesQueueRef.current = [];
break;
case '@webframe.resize':
setSize({
maxWidth: Number(message.action.size.maxWidth),
maxHeight: Number(message.action.size.maxHeight),
aspectRatio: Number(message.action.size.aspectRatio),
});
break;
default:
renderer.update({
action: message.action,
});
}
}
};
window.addEventListener('message', callback);
return () => {
window.removeEventListener('message', callback);
};
}, [renderer, sendMessage]);
//
// Send data to the webframe
//
React.useEffect(() => {
if (!element.data) {
return;
}
const state: Record<string, string> = {};
Object.entries(element.data).forEach(([key, value]) => {
state[key] = resolveDynamicBinding(renderer.state, value);
});
return sendMessage({ state });
}, [element.data, renderer.state, sendMessage]);
return (
<div
className={`contentkit-webframe`}
style={{
aspectRatio: element.aspectRatio,
...size,
}}
>
<iframe
ref={iframeRef}
src={element.source.url}
allowFullScreen
allow="clipboard-write"
style={{
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
width: '100%',
height: '100%',
border: 'none',
}}
/>
</div>
);
}
+57
View File
@@ -0,0 +1,57 @@
import { ContentKitAction, RequestRenderIntegrationUI } from '@gitbook/api';
import * as React from 'react';
/**
* Security restrictions for elements.
*/
export interface ContentKitSecurity {
/**
* Domains allowed for webframe communication.
* These domains will be allowed to send messages to ContentKit,
* and receive messages from ContentKit.
*/
firstPartyDomains: string[];
}
export type ContentKitRenderUpdate = Partial<
Pick<RequestRenderIntegrationUI, 'action' | 'props' | 'state'>
>;
export interface ContentKitClientContextType {
security: ContentKitSecurity;
/**
* Current value of the state.
*/
state: object;
/**
* Update the state.
*/
setState: React.Dispatch<React.SetStateAction<object>>;
/**
* Re-render the ContentKit component with a new set of props/state or an action.
*/
update: (newState: ContentKitRenderUpdate) => Promise<void>;
/**
* Dispatch an action.
*/
dispatchAction: (action: ContentKitAction, bubble?: boolean) => Promise<void>;
}
export const ContentKitClientContext = React.createContext<ContentKitClientContextType | null>(
null,
);
/**
* Get the current contentkit context.
*/
export function useContentKitClientContext(): ContentKitClientContextType {
const context = React.useContext(ContentKitClientContext);
if (!context) {
throw new Error('ContentKit component should be wrapped in <ContentKit>');
}
return context;
}
+55
View File
@@ -0,0 +1,55 @@
import { ContentKitDynamicBinding } from '@gitbook/api';
/**
* Get a value from the state.
*/
export function getStateStringValue(state: object, key: string): string | undefined {
// @ts-ignore
const value = state[key];
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number') {
return `${value}`;
}
return undefined;
}
/**
* Resolve a potential dynamic binding as a plain value.
*/
export function resolveDynamicBinding<T extends {}>(
state: object,
value: ContentKitDynamicBinding | T,
): T {
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'undefined'
) {
// Primitives
return value;
}
if (Array.isArray(value)) {
// @ts-ignore
return value.map((v) => resolveDynamicBinding(localState, v));
}
if ('$state' in value && typeof value.$state === 'string') {
// @ts-ignore
return state[value.$state];
}
// Plain object
const result = {};
Object.entries(value).forEach(([key, keyValue]) => {
// @ts-ignore
result[key] = resolveDynamicBinding(state, keyValue);
});
// @ts-ignore
return result;
}
+3
View File
@@ -0,0 +1,3 @@
export * from './ContentKit';
export * from './ContentKitOutput';
export type { ContentKitServerContext } from './types';
+61
View File
@@ -0,0 +1,61 @@
import { ContentKitIcon } from '@gitbook/api';
import React from 'react';
interface CodeBlockCommonProps {
code: string;
syntax: string;
lineNumbers: number | boolean;
}
/**
* Context to render ContentKit elements.
* This context is designed to work in a server environment.
*/
export interface ContentKitServerContext {
/**
* Components to render icons.
*/
icons: { [icon in ContentKitIcon]: React.ComponentType<{ className: string | undefined }> };
/**
* Components to wrap a modal.
* It can be used to render modals in a portal.
*/
modalWrapper?: React.ComponentType<{ children: React.ReactNode }>;
/**
* Component to render a code block.
* This component is used to render code blocks in the server environment.
*/
codeBlock?: React.ComponentType<CodeBlockCommonProps>;
/**
* Component to render a code block in the client side
*/
codeBlockClient?: React.ComponentType<
CodeBlockCommonProps & {
onChange: (code: string) => void;
}
>;
/**
* Server component to render markdown.
*/
markdown: React.ComponentType<{ className: string; markdown: string }>;
}
/**
* Props for a ContentKit element that is rendered on the server side.
*/
export interface ContentKitServerElementProps<ElementType> {
element: ElementType;
context: ContentKitServerContext;
state: object;
}
/**
* Props for a ContentKit element that is rendered on the client side.
*/
export interface ContentKitClientElementProps<ElementType> {
element: ElementType;
}
+13
View File
@@ -0,0 +1,13 @@
# @gitbook/react-math
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
+11
View File
@@ -0,0 +1,11 @@
{
"name": "@gitbook/react-math",
"exports": "./src/index.ts",
"version": "0.2.0",
"dependencies": {
"object-hash": "^3.0.0"
},
"peerDependencies": {
"react": "*"
}
}
+35
View File
@@ -0,0 +1,35 @@
import katex from 'katex';
import React from 'react';
const KaTeXCSS = React.lazy(() => import('./KaTeXCSS'));
/**
* Server component to compile the KaTeX formula to HTML.
*/
export function KaTeX(props: {
formula: string;
inline: boolean;
className?: string;
fallback?: React.ReactNode;
}) {
const { formula, inline, className } = props;
try {
const html = katex.renderToString(formula, {
displayMode: !inline,
output: 'htmlAndMathml',
throwOnError: true,
strict: false,
});
const Tag = inline ? 'span' : 'div';
return (
<>
<KaTeXCSS />
<Tag className={className} dangerouslySetInnerHTML={{ __html: html }} />
</>
);
} catch (error) {
return <>{props.fallback}</>;
}
}
+27
View File
@@ -0,0 +1,27 @@
'use client';
/**
* Dummy component to lazy load the KaTeX CSS.
*/
export default function KaTeXCSS(props: {}) {
// Load the CSS as soon as possible (this is why we don't use an effect hook here)
// We lazy load the CSS to avoidNext bundling it in the main bundle
loadCSS();
return null;
}
let loaded = false;
function loadCSS() {
if (loaded || typeof window === 'undefined') {
return;
}
loaded = true;
// @ts-ignore
import('katex/dist/katex.min.css').then(() => {
document.body.classList.add('katex-loaded');
});
}
+65
View File
@@ -0,0 +1,65 @@
import React from 'react';
import { KaTeX } from './KaTeX';
import { MathJaXLazy } from './MathJaXLazy';
import './default.css';
export interface MathFormulaProps {
/**
* The formula to render.
*/
formula: string;
/**
* Whether to render the formula inline or as a block.
*/
inline?: boolean;
/**
* Additional class name to apply to the rendered formula.
*/
className?: string;
/**
* Fallback to render while loading the formula with MathJax or KateX.
*/
fallback?: React.ReactNode;
/**
* URL to load MathJax from.
*/
mathJaxUrl: string;
}
/**
* Render a math formula using KaTeX, and fallback to MathJax if KaTeX fails.
*/
export function MathFormula(props: MathFormulaProps) {
const {
formula,
inline = false,
className,
fallback = React.createElement(props.inline ? 'span' : 'div', {
className: props.className,
children: props.formula,
}),
mathJaxUrl,
} = props;
return (
<KaTeX
formula={formula}
inline={inline}
className={className}
fallback={
<MathJaXLazy
formula={formula}
inline={inline}
className={className}
fallback={fallback}
mathJaxUrl={mathJaxUrl}
/>
}
/>
);
}
+106
View File
@@ -0,0 +1,106 @@
'use client';
import * as React from 'react';
declare var MathJax: {
startup: {
promise: Promise<void>;
};
tex2chtml: (formula: string, options: { display: boolean }) => any;
typesetPromise: (promise: void | Promise<void>) => Promise<void>;
};
export interface MathJaXFormulaProps {
formula: string;
inline: boolean;
fallback?: React.ReactNode;
className?: string;
mathJaxUrl: string;
}
/**
* Client component that loads MathJax and renders the formula.
*/
export default function MathJaXFormula(props: MathJaXFormulaProps) {
const { formula, inline, className, mathJaxUrl } = props;
React.use(loadMathJaxScript(mathJaxUrl));
const [html, setHTML] = React.useState('');
const containerRef = React.useRef<HTMLDivElement | HTMLSpanElement>(null);
// Typeset the formula
React.useEffect(() => {
let cancelled = false;
typeset(() => {
if (cancelled) {
return;
}
const domNode = MathJax.tex2chtml(formula, { display: !inline });
setHTML(domNode.outerHTML);
});
return () => {
cancelled = true;
};
}, [inline, formula]);
return React.createElement(inline ? 'span' : 'div', {
className,
ref: containerRef,
dangerouslySetInnerHTML: { __html: html },
});
}
let mathJaxPromise: Promise<void> | null = null;
function loadMathJaxScript(url: string) {
if (mathJaxPromise) {
return mathJaxPromise;
}
mathJaxPromise = new Promise<void>((resolve, reject) => {
if (typeof window === 'undefined') {
resolve();
return;
} else {
// @ts-ignore
window.MathJax = {
tex: {
inlineMath: [],
},
options: {
enableMenu: false,
},
startup: {
elements: null,
typeset: false,
},
};
const script = document.createElement('script');
script.src = url;
script.id = 'MathJax-script';
script.async = true;
document.head.appendChild(script);
script.onload = () => {
resolve();
};
script.onerror = () => {
reject(new Error('Failed to load MathJax'));
};
}
});
return mathJaxPromise;
}
function typeset(code: () => void | Promise<void>) {
MathJax.startup.promise = MathJax.startup.promise
.then(() => MathJax.typesetPromise(code()))
.catch((err) => console.log('Typeset failed: ' + err.message));
return MathJax.startup.promise;
}
+17
View File
@@ -0,0 +1,17 @@
'use client';
import React from 'react';
import type { MathJaXFormulaProps } from './MathJaX';
const MathJaXFormula = React.lazy(() => import('./MathJaX'));
/**
* Lazy component that loads MathJax and renders the formula.
*/
export function MathJaXLazy(props: MathJaXFormulaProps) {
return (
<React.Suspense fallback={props.fallback}>
<MathJaXFormula {...props} />
</React.Suspense>
);
}
+9
View File
@@ -0,0 +1,9 @@
/** Hide the KaTeX HTML output, while it's loading */
body:not(.katex-loaded) .katex-html {
display: none;
}
/** Align the MathJax output with the font-size used by KaTeX */
mjx-container[jax='CHTML'] {
font-size: 1.21em;
}
+1
View File
@@ -0,0 +1 @@
export * from './MathFormula';
+13
View File
@@ -0,0 +1,13 @@
# @gitbook/react-openapi
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
+12
View File
@@ -0,0 +1,12 @@
# `@gitbook/react-openapi`
React components to render OpenAPI operations.
## Features
- Generate code samples for the request
- Support custom cde samples with `x-codeSamples` (Redocly syntax)
## TODO
- Support for trying out the request
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@gitbook/react-openapi",
"exports": "./src/index.ts",
"version": "0.2.0",
"dependencies": {
"@scalar/api-client-react": "^0.3.7",
"@scalar/oas-utils": "0.1.6",
"classnames": "^2.5.1",
"flatted": "^3.2.9",
"openapi-types": "^12.1.3",
"yaml": "1.10.2",
"swagger2openapi": "^7.0.8"
},
"devDependencies": {
"@types/swagger2openapi": "^7.0.4"
},
"peerDependencies": {
"react": "*"
}
}
@@ -0,0 +1,129 @@
'use client';
import classNames from 'classnames';
import React from 'react';
/**
* To optimize rendering, most of the components are server-components,
* and the interactiveness is mainly handled by a few key components like this one.
*/
export function InteractiveSection(props: {
id?: string;
/** Class name to be set on the section, sub-elements will use it as prefix */
className: string;
/** If true, the content can be toggeable */
toggeable?: boolean;
/** Default state of the toggle */
defaultOpened?: boolean;
/** Icons to display for the toggle */
toggleOpenIcon?: React.ReactNode;
toggleCloseIcon?: React.ReactNode;
/** Tabs of content to display */
tabs?: Array<{
key: string;
label: string;
body: React.ReactNode;
}>;
/** Default tab to have opened */
defaultTab?: string;
/** Content of the header */
header: React.ReactNode;
/** Body of the section */
children?: React.ReactNode;
/** Children to display within the container */
overlay?: React.ReactNode;
}) {
const {
id,
className,
toggeable = false,
defaultOpened = true,
tabs = [],
defaultTab = tabs[0]?.key,
header,
children,
overlay,
toggleOpenIcon = '▶',
toggleCloseIcon = '▼',
} = props;
const [opened, setOpened] = React.useState(defaultOpened);
const [selectedTab, setSelectedTab] = React.useState(defaultTab);
const tabBody = tabs.find((tab) => tab.key === selectedTab)?.body;
return (
<div
id={id}
className={classNames(
'openapi-section',
toggeable ? 'openapi-section-toggeable' : null,
className,
toggeable ? `${className}-${opened ? 'opened' : 'closed'}` : null,
)}
>
<div
onClick={() => {
if (toggeable) {
setOpened(!opened);
}
}}
className={classNames('openapi-section-header', `${className}-header`)}
>
<div
className={classNames(
'openapi-section-header-content',
`${className}-header-content`,
)}
>
{header}
</div>
<div
className={classNames(
'openapi-section-header-controls',
`${className}-header-controls`,
)}
onClick={(event) => {
event.stopPropagation();
}}
>
{tabs.length ? (
<select
className={classNames(
'openapi-section-select',
'openapi-select',
`${className}-tabs-select`,
)}
value={selectedTab}
onChange={(event) => {
setSelectedTab(event.target.value);
setOpened(true);
}}
>
{tabs.map((tab) => (
<option key={tab.key} value={tab.key}>
{tab.label}
</option>
))}
</select>
) : null}
{(children || tabBody) && toggeable ? (
<button
className={classNames('openapi-section-toggle', `${className}-toggle`)}
onClick={() => setOpened(!opened)}
>
{opened ? toggleCloseIcon : toggleOpenIcon}
</button>
) : null}
</div>
</div>
{(!toggeable || opened) && (children || tabBody) ? (
<div className={classNames('openapi-section-body', `${className}-body`)}>
{children}
{tabBody}
</div>
) : null}
{overlay}
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
import classNames from 'classnames';
export function Markdown(props: { source: string; className?: string }) {
const { source, className } = props;
return (
<div
className={classNames('openapi-markdown', className)}
dangerouslySetInnerHTML={{ __html: source }}
/>
);
}
@@ -0,0 +1,111 @@
import { OpenAPIV3 } from 'openapi-types';
import { CodeSampleInput, codeSampleGenerators } from './code-samples';
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
import { generateMediaTypeExample } from './generateSchemaExample';
import { InteractiveSection } from './InteractiveSection';
import { getServersURL } from './OpenAPIServerURL';
import { ScalarApiButton } from './ScalarApiButton';
import { OpenAPIContextProps } from './types';
import { noReference } from './utils';
/**
* Display code samples to execute the operation.
* It supports the Redocly custom syntax as well (https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples/)
*/
export function OpenAPICodeSample(props: {
data: OpenAPIOperationData;
context: OpenAPIContextProps;
}) {
const { data, context } = props;
const requestBody = noReference(data.operation.requestBody);
const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined;
const input: CodeSampleInput = {
url: getServersURL(data.servers) + data.path,
method: data.method,
body: requestBodyContent
? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })
: undefined,
headers: {
...getSecurityHeaders(data.securities),
...(requestBodyContent
? {
'Content-Type': requestBodyContent[0],
}
: undefined),
},
};
const autoCodeSamples = codeSampleGenerators.map((generator) => ({
key: `default-${generator.id}`,
label: generator.label,
body: <context.CodeBlock code={generator.generate(input)} syntax={generator.syntax} />,
}));
// Use custom samples if defined
let customCodeSamples: null | Array<{
key: string;
label: string;
body: React.ReactNode;
}> = null;
(['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const).forEach((key) => {
const customSamples = data.operation[key];
if (customSamples) {
customCodeSamples = customSamples.map((sample) => ({
key: `redocly-${sample.lang}`,
label: sample.label,
body: <context.CodeBlock code={sample.source} syntax={sample.lang} />,
}));
}
});
const samples = customCodeSamples ?? (data['x-codeSamples'] !== false ? autoCodeSamples : []);
if (samples.length === 0) {
return null;
}
async function fetchOperationData() {
'use server';
return toJSON(data);
}
return (
<InteractiveSection
header="Request"
className="openapi-codesample"
tabs={samples}
overlay={
data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
<ScalarApiButton fetchOperationData={fetchOperationData} />
)
}
/>
);
}
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
[key: string]: string;
} {
const security = securities[0];
if (!security) {
return {};
}
switch (security[1].type) {
case 'http': {
let scheme = security[1].scheme;
if (scheme === 'bearer') {
scheme = 'Bearer';
}
return {
Authorization: scheme + ' ' + (security[1].bearerFormat ?? '<token>'),
};
}
default: {
return {};
}
}
}
@@ -0,0 +1,65 @@
import classNames from 'classnames';
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
import { Markdown } from './Markdown';
import { OpenAPICodeSample } from './OpenAPICodeSample';
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
import { OpenAPIServerURL } from './OpenAPIServerURL';
import { OpenAPISpec } from './OpenAPISpec';
import { ScalarApiClient } from './ScalarApiButton';
import { OpenAPIClientContext, OpenAPIContextProps } from './types';
/**
* Display an interactive OpenAPI operation.
*/
export function OpenAPIOperation(props: {
className?: string;
data: OpenAPIOperationData;
context: OpenAPIContextProps;
}) {
const { className, data, context } = props;
const { operation, servers, method, path } = data;
const clientContext: OpenAPIClientContext = {
defaultInteractiveOpened: context.defaultInteractiveOpened,
icons: context.icons,
};
return (
<ScalarApiClient>
<div className={classNames('openapi-operation', className)}>
<div className="openapi-intro">
<h2 className="openapi-summary">{operation.summary}</h2>
{operation.description ? (
<Markdown className="openapi-description" source={operation.description} />
) : null}
<div className="openapi-target">
<span
className={classNames(
'openapi-method',
`openapi-method-${method.toLowerCase()}`,
)}
>
{method.toUpperCase()}
</span>
<span className="openapi-url">
<OpenAPIServerURL servers={servers} />
{path}
</span>
</div>
</div>
<div className={classNames('openapi-columns')}>
<div className={classNames('openapi-column-spec')}>
<OpenAPISpec rawData={toJSON(data)} context={clientContext} />
</div>
<div className={classNames('openapi-column-preview')}>
<div className={classNames('openapi-column-preview-body')}>
<OpenAPICodeSample {...props} />
<OpenAPIResponseExample {...props} />
</div>
</div>
</div>
</div>
</ScalarApiClient>
);
}
@@ -0,0 +1,45 @@
import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIRootSchema } from './OpenAPISchema';
import { noReference } from './utils';
import { OpenAPIClientContext } from './types';
import { InteractiveSection } from './InteractiveSection';
import { Markdown } from './Markdown';
/**
* Display an interactive request body.
*/
export function OpenAPIRequestBody(props: {
requestBody: OpenAPIV3.RequestBodyObject;
context: OpenAPIClientContext;
}) {
const { requestBody, context } = props;
return (
<InteractiveSection
header="Body"
className="openapi-requestbody"
tabs={Object.entries(requestBody.content ?? {}).map(
([contentType, mediaTypeObject]) => {
return {
key: contentType,
label: contentType,
body: (
<OpenAPIRootSchema
schema={noReference(mediaTypeObject.schema) ?? {}}
context={context}
/>
),
};
},
)}
defaultOpened={context.defaultInteractiveOpened}
>
{requestBody.description ? (
<Markdown
source={requestBody.description}
className="openapi-requestbody-description"
/>
) : null}
</InteractiveSection>
);
}
@@ -0,0 +1,71 @@
import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIRootSchema, OpenAPISchemaProperties } from './OpenAPISchema';
import { noReference } from './utils';
import { OpenAPIClientContext } from './types';
import { InteractiveSection } from './InteractiveSection';
import { Markdown } from './Markdown';
/**
* Display an interactive response body.
*/
export function OpenAPIResponse(props: {
response: OpenAPIV3.ResponseObject;
context: OpenAPIClientContext;
}) {
const { response, context } = props;
const content = Object.entries(response.content ?? {});
const headers = Object.entries(response.headers ?? {}).map(
([name, header]) => [name, noReference(header) ?? {}] as const,
);
if (content.length === 0 && !response.description && headers.length === 0) {
return null;
}
return (
<>
{response.description ? (
<Markdown source={response.description} className="openapi-response-description" />
) : null}
{headers.length > 0 ? (
<InteractiveSection
toggeable
defaultOpened={!!context.defaultInteractiveOpened}
toggleCloseIcon={context.icons.chevronDown}
toggleOpenIcon={context.icons.chevronRight}
header="Headers"
className={classNames('openapi-responseheaders')}
>
<OpenAPISchemaProperties
properties={headers.map(([name, header]) => ({
propertyName: name,
schema: noReference(header.schema) ?? {},
required: header.required,
}))}
context={context}
/>
</InteractiveSection>
) : null}
{content.length > 0 ? (
<InteractiveSection
header="Body"
className={classNames('openapi-responsebody')}
tabs={content.map(([contentType, mediaType]) => {
return {
key: contentType,
label: contentType,
body: (
<OpenAPIRootSchema
schema={noReference(mediaType.schema) ?? {}}
context={context}
/>
),
};
})}
/>
) : null}
</>
);
}
@@ -0,0 +1,71 @@
import { InteractiveSection } from './InteractiveSection';
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
import { generateSchemaExample } from './generateSchemaExample';
import { OpenAPIContextProps } from './types';
import { noReference } from './utils';
/**
* Display an example of the response content.
*/
export function OpenAPIResponseExample(props: {
data: OpenAPIOperationData;
context: OpenAPIContextProps;
}) {
const { data, context } = props;
// if there are no responses defined for the operation
if (!data.operation.responses) {
return null;
}
const responses = Object.entries(data.operation.responses);
// Sort response to get 200, and 2xx first
responses.sort(([a], [b]) => {
if (a === 'default') {
return 1;
}
if (b === 'default') {
return -1;
}
if (a === '200') {
return -1;
}
if (b === '200') {
return 1;
}
return Number(a) - Number(b);
});
// Take the first one
const response = responses[0];
if (!response) {
return null;
}
const responseObject = noReference(response[1]);
const schema = noReference(
(
responseObject.content?.['application/json'] ??
responseObject.content?.[Object.keys(responseObject.content)[0]]
)?.schema,
);
if (!schema) {
return null;
}
const example = generateSchemaExample(schema);
if (example === undefined) {
return null;
}
return (
<InteractiveSection header="Response" className="openapi-response-example">
<context.CodeBlock
code={typeof example === 'string' ? example : JSON.stringify(example, null, 2)}
syntax="json"
/>
</InteractiveSection>
);
}
@@ -0,0 +1,30 @@
import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types';
import { noReference } from './utils';
import { OpenAPIResponse } from './OpenAPIResponse';
import { OpenAPIClientContext } from './types';
import { InteractiveSection } from './InteractiveSection';
/**
* Display an interactive response body.
*/
export function OpenAPIResponses(props: {
responses: OpenAPIV3.ResponsesObject;
context: OpenAPIClientContext;
}) {
const { responses, context } = props;
return (
<InteractiveSection
header="Response"
className={classNames('openapi-responses')}
tabs={Object.entries(responses).map(([statusCode, response]) => {
return {
key: statusCode,
label: statusCode,
body: <OpenAPIResponse response={noReference(response)} context={context} />,
};
})}
/>
);
}
@@ -0,0 +1,101 @@
import { it, describe, expect } from 'bun:test';
import { getSchemaAlternatives } from './OpenAPISchema';
import { OpenAPIV3 } from 'openapi-types';
describe('getSchemaAlternatives', () => {
it('should flatten oneOf', () => {
expect(
getSchemaAlternatives({
oneOf: [
{
oneOf: [
{
type: 'number',
},
{
type: 'boolean',
},
],
},
{
type: 'string',
},
],
}),
).toEqual([
[
{
type: 'number',
},
{
type: 'boolean',
},
{
type: 'string',
},
],
undefined,
]);
});
it('should not flatten oneOf and allOf', () => {
expect(
getSchemaAlternatives({
oneOf: [
{
allOf: [
{
type: 'number',
},
{
type: 'boolean',
},
],
},
{
type: 'string',
},
],
}),
).toEqual([
[
{
allOf: [
{
type: 'number',
},
{
type: 'boolean',
},
],
},
{
type: 'string',
},
],
undefined,
]);
});
it('should stop at circular references', () => {
const a: OpenAPIV3.SchemaObject = {
anyOf: [
{
type: 'string',
},
],
};
a.anyOf!.push(a);
expect(getSchemaAlternatives(a)).toEqual([
[
{
type: 'string',
},
a,
],
undefined,
]);
});
});
@@ -0,0 +1,401 @@
import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types';
import React, { useId } from 'react';
import { InteractiveSection } from './InteractiveSection';
import { Markdown } from './Markdown';
import { SYMBOL_REF_RESOLVED } from './resolveOpenAPIPath';
import { OpenAPIClientContext } from './types';
import { noReference } from './utils';
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
interface OpenAPISchemaPropertyEntry {
propertyName?: string;
required?: boolean;
schema: OpenAPIV3.SchemaObject;
}
/**
* Render a property of an OpenAPI schema.
*/
export function OpenAPISchemaProperty(
props: OpenAPISchemaPropertyEntry & {
/** Set of objects already observed as parents */
circularRefs?: CircularRefsIds;
context: OpenAPIClientContext;
className?: string;
},
) {
const {
propertyName,
required,
schema,
circularRefs: parentCircularRefs = new Map<OpenAPIV3.SchemaObject, string>(),
context,
className,
} = props;
const id = useId();
const parentCircularRef = parentCircularRefs.get(schema);
const circularRefs = new Map(parentCircularRefs).set(schema, id);
// Avoid recursing infinitely, and instead render a link to the parent schema
const properties = parentCircularRef ? null : getSchemaProperties(schema);
const alternatives = parentCircularRef
? null
: getSchemaAlternatives(schema, new Set(circularRefs.keys()));
const shouldDisplayExample = (schema: OpenAPIV3.SchemaObject): boolean => {
return (
typeof schema.example === 'string' ||
typeof schema.example === 'number' ||
typeof schema.example === 'boolean'
);
};
return (
<InteractiveSection
id={id}
className={classNames('openapi-schema', className)}
toggeable={!!properties || !!alternatives}
defaultOpened={!!context.defaultInteractiveOpened}
toggleOpenIcon={context.icons.chevronRight}
toggleCloseIcon={context.icons.chevronDown}
tabs={alternatives?.[0].map((alternative, index) => ({
key: `${index}`,
label: getSchemaTitle(alternative, alternatives[1]),
body: circularRefs.has(alternative) ? (
<OpenAPISchemaCircularRef
id={circularRefs.get(alternative)!}
schema={alternative}
/>
) : (
<OpenAPISchemaAlternative
schema={alternative}
circularRefs={circularRefs}
context={context}
/>
),
}))}
header={
<div className={classNames('openapi-schema-presentation')}>
<div className={classNames('openapi-schema-name')}>
{propertyName ? (
<span className={classNames('openapi-schema-propertyname')}>
{propertyName}
</span>
) : null}
{required ? (
<span className={classNames('openapi-schema-required')}>*</span>
) : null}
<span className={classNames('openapi-schema-type')}>
{getSchemaTitle(schema)}
</span>
</div>
{schema.description ? (
<Markdown
source={schema.description}
className="openapi-schema-description"
/>
) : null}
{shouldDisplayExample(schema) ? (
<span className="openapi-schema-example">
Example: <code>{JSON.stringify(schema.example)}</code>
</span>
) : null}
</div>
}
>
{(properties && properties.length > 0) ||
(schema.enum && schema.enum.length > 0) ||
parentCircularRef ? (
<>
{properties?.length ? (
<OpenAPISchemaProperties
properties={properties}
circularRefs={circularRefs}
context={context}
/>
) : null}
{schema.enum && schema.enum.length > 0 ? (
<OpenAPISchemaEnum enumValues={schema.enum} />
) : null}
{parentCircularRef ? (
<OpenAPISchemaCircularRef id={parentCircularRef} schema={schema} />
) : null}
</>
) : null}
</InteractiveSection>
);
}
/**
* Render a set of properties of an OpenAPI schema.
*/
export function OpenAPISchemaProperties(props: {
id?: string;
properties: OpenAPISchemaPropertyEntry[];
circularRefs?: CircularRefsIds;
context: OpenAPIClientContext;
}) {
const { id, properties, circularRefs, context } = props;
if (!properties.length) {
return null;
}
return (
<div id={id} className={classNames('openapi-schema-properties')}>
{properties.map((property) => (
<OpenAPISchemaProperty
key={property.propertyName}
circularRefs={circularRefs}
{...property}
context={context}
/>
))}
</div>
);
}
/**
* Render a root schema (such as the request body or response body).
*/
export function OpenAPIRootSchema(props: {
schema: OpenAPIV3.SchemaObject;
context: OpenAPIClientContext;
}) {
const { schema, context } = props;
// Avoid recursing infinitely, and instead render a link to the parent schema
const properties = getSchemaProperties(schema);
if (properties && properties.length > 0) {
return <OpenAPISchemaProperties properties={properties} context={context} />;
}
return (
<OpenAPISchemaProperty schema={schema} context={context} className="openapi-schema-root" />
);
}
/**
* Render a tab for an alternative schema.
* It renders directly the properties if relevant;
* for primitives, it renders the schema itself.
*/
function OpenAPISchemaAlternative(props: {
schema: OpenAPIV3.SchemaObject;
circularRefs?: CircularRefsIds;
context: OpenAPIClientContext;
}) {
const { schema, circularRefs, context } = props;
const id = useId();
const subProperties = getSchemaProperties(schema);
return (
<OpenAPISchemaProperties
id={id}
properties={subProperties ?? [{ schema }]}
circularRefs={subProperties ? new Map(circularRefs).set(schema, id) : circularRefs}
context={context}
/>
);
}
/**
* Render a circular reference to a schema.
*/
function OpenAPISchemaCircularRef(props: { id: string; schema: OpenAPIV3.SchemaObject }) {
const { id, schema } = props;
return (
<div className="openapi-schema-circular">
Circular reference to <a href={`#${id}`}>{getSchemaTitle(schema)}</a>{' '}
<span className="openapi-schema-circular-glyph"></span>
</div>
);
}
/**
* Render the enum value for a schema.
*/
export function OpenAPISchemaEnum(props: { enumValues: any[] }) {
const { enumValues } = props;
return (
<div className="openapi-schema-enum">
{enumValues.map((value, index) => (
<span key={index} className="openapi-schema-enum-value">{`${value}`}</span>
))}
</div>
);
}
/**
* Get the sub-properties of a schema.
*/
function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] {
if (schema.allOf) {
return schema.allOf.reduce((acc, subSchema) => {
const properties = getSchemaProperties(noReference(subSchema)) ?? [
{
schema: noReference(subSchema),
},
];
return [...acc, ...properties];
}, [] as OpenAPISchemaPropertyEntry[]);
}
// check array AND schema.items as this is sometimes null despite what the type indicates
if (schema.type === 'array' && !!schema.items) {
const items = noReference(schema.items);
const itemProperties = getSchemaProperties(items);
if (itemProperties) {
return itemProperties;
}
return [
{
propertyName: 'items',
schema: items,
},
];
}
if (schema.type === 'object' || schema.properties) {
const result: OpenAPISchemaPropertyEntry[] = [];
if (schema.properties) {
Object.entries(schema.properties).forEach(([propertyName, rawPropertySchema]) => {
const propertySchema = noReference(rawPropertySchema);
if (propertySchema.deprecated) {
return;
}
result.push({
propertyName,
required: Array.isArray(schema.required)
? schema.required.includes(propertyName)
: undefined,
schema: propertySchema,
});
});
}
if (schema.additionalProperties) {
const additionalProperties = noReference(schema.additionalProperties);
result.push({
propertyName: 'Other properties',
schema: additionalProperties === true ? {} : additionalProperties,
});
}
return result;
}
return null;
}
/**
* Get the alternatives to display for a schema.
*/
export function getSchemaAlternatives(
schema: OpenAPIV3.SchemaObject,
ancestors: Set<OpenAPIV3.SchemaObject> = new Set(),
): null | [OpenAPIV3.SchemaObject[], OpenAPIV3.DiscriminatorObject | undefined] {
const downAncestors = new Set(ancestors).add(schema);
if (schema.anyOf) {
return [
flattenAlternatives('anyOf', schema.anyOf.map(noReference), downAncestors),
noReference(schema.discriminator),
];
}
if (schema.oneOf) {
return [
flattenAlternatives('oneOf', schema.oneOf.map(noReference), downAncestors),
noReference(schema.discriminator),
];
}
if (schema.allOf) {
// allOf is managed in `getSchemaProperties`
return null;
}
return null;
}
function flattenAlternatives(
alternativeType: 'oneOf' | 'allOf' | 'anyOf',
alternatives: OpenAPIV3.SchemaObject[],
ancestors: Set<OpenAPIV3.SchemaObject>,
): OpenAPIV3.SchemaObject[] {
return alternatives.reduce((acc, alternative) => {
if (!!alternative[alternativeType] && !ancestors.has(alternative)) {
return [...acc, ...(getSchemaAlternatives(alternative, ancestors)?.[0] || [])];
}
return [...acc, alternative];
}, [] as OpenAPIV3.SchemaObject[]);
}
function getSchemaTitle(
schema: OpenAPIV3.SchemaObject,
/** If the title is inferred in a oneOf with discriminator, we can use it to optimize the title */
discriminator?: OpenAPIV3.DiscriminatorObject,
): string {
if (schema.title) {
// If the schema has a title, use it
return schema.title;
}
// Try using the discriminator
if (discriminator && schema.properties) {
const discriminatorProperty = noReference(schema.properties[discriminator.propertyName]);
if (discriminatorProperty) {
if (discriminatorProperty.enum) {
return discriminatorProperty.enum.map((value) => value.toString()).join(' | ');
}
}
}
// Otherwise try to infer a nice title
let type = 'any';
if (schema.enum) {
type = 'enum';
// check array AND schema.items as this is sometimes null despite what the type indicates
} else if (schema.type === 'array' && !!schema.items) {
type = `array of ${getSchemaTitle(noReference(schema.items))}`;
} else if (schema.type || schema.properties) {
type = schema.type ?? 'object';
if (schema.format) {
type += ` (${schema.format})`;
}
} else if ('anyOf' in schema) {
type = 'any of';
} else if ('oneOf' in schema) {
type = 'one of';
} else if ('allOf' in schema) {
type = 'all of';
} else if ('not' in schema) {
type = 'not';
}
if (SYMBOL_REF_RESOLVED in schema) {
type = `${schema[SYMBOL_REF_RESOLVED]} (${type})`;
}
if (schema.nullable) {
type = `nullable ${type}`;
}
return type;
}

Some files were not shown because too many files have changed in this diff Show More