Compare commits

..

534 Commits

Author SHA1 Message Date
Samy Pessé 32a73b55d4 Version Packages (#2403)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-25 15:12:18 +02:00
Samy Pessé 0426312d6c Reset to first tab when tabs change in an InteractiveSection (#2407) 2024-07-25 15:09:21 +02:00
Samy Pessé ede2335fb3 Fix x-codeSamples: false not working at the single operation level (#2408) 2024-07-25 14:59:30 +02:00
Amrit Kahlon 709f1a1ccb Update Scalar Api Client to new version (#2406)
Co-authored-by: marclave <marc@scalar.com>
Co-authored-by: Samy Pessé <samypesse@gmail.com>
2024-07-25 10:25:50 +02:00
Samy Pessé 24b785cbfb Update shiki to fix diff code highlighting (#2338) 2024-07-25 09:33:42 +02:00
Samy Pessé acc3f2f418 Fix Scalar proxy failing on Cloudflare (#2402) 2024-07-24 11:41:35 +02:00
Scott Cazan d4a653d6a9 Use SpaceURL in getURL function to for cross-space searches (#2399) 2024-07-23 12:07:32 +02:00
Samy Pessé 7e74d25447 Version Packages (#2397)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-22 16:26:53 +02:00
Samy Pessé 3445db41ce Fix packages not including all files (#2396) 2024-07-22 16:20:06 +02:00
Samy Pessé d1bd02a6f5 Version Packages (#2395)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-22 15:43:27 +02:00
Samy Pessé 24cd72e088 Use a personal token for the changeset GitHub workflow (#2394) 2024-07-22 15:30:11 +02:00
github-actions[bot] 47f7558c38 Version Packages (#2393)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-22 15:05:57 +02:00
Samy Pessé de747b7b33 Better structure monorepo and publish JS files for react-openapi and react-math (#2392) 2024-07-22 15:02:04 +02:00
Samy Pessé 8e8b73fbd9 Publish packages as public with changeset (#2391) 2024-07-21 21:25:43 +02:00
github-actions[bot] e966e33e8f Version Packages (#2390)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-21 19:45:18 +02:00
Samy Pessé bd0ca5babd Fix @gitbook/react-openapi when react import are mandatory (#2389) 2024-07-21 19:43:50 +02:00
Samy Pessé f6bacea1ae Skip hidden pages in sitemap.xml (#2388) 2024-07-19 09:20:16 +02:00
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
585 changed files with 30110 additions and 11170 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": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
-10
View File
@@ -1,10 +0,0 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
+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: ./packages/gitbook/.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 ./packages/gitbook/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
+37
View File
@@ -0,0 +1,37 @@
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:
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+18 -27
View File
@@ -1,31 +1,22 @@
# 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
theme/javascript/vendors
theme/vendors
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Turbo
.turbo
# Vercel
.vercel
# vim swapfile
*.swp
+6
View File
@@ -0,0 +1,6 @@
.next
.vercel
# Generated
packages/emoji-codepoints/index.ts
packages/gitbook/public/~gitbook/static/
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 100,
"singleQuote": true,
"tabWidth": 4
}
-7
View File
@@ -1,7 +0,0 @@
sudo: false
language: node_js
node_js:
- "0.11"
- "0.10"
before_install:
- npm install svgexport -g
+11
View File
@@ -0,0 +1,11 @@
{
"tailwindCSS.experimental.classRegex": [
["Style \\=([^;]*);", "'([^']*)'"],
["Style \\=([^;]*);", "\"([^\"]*)\""],
["Style \\=([^;]*);", "\\`([^\\`]*)\\`"],
["style \\=([^;]*);", "'([^']*)'"],
["style \\=([^;]*);", "\"([^\"]*)\""],
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
],
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"]
}
-52
View File
@@ -1,52 +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> (@SamyPesse)
- Aaron O'Mullan <aaron.omullan@gmail.com> (@AaronO)
Contributors
------------
- Nijiko Yonskai (@Nijikokun)
- Herman Starikov (@Hermanya)
Translators
------------
- German
- Winnie (@winniehell)
- Italian
- Giulio Bonanome (@gbonanome)
- Russian
- Andrey Akinshin (@AndreyAkinshin)
- Norwegian
- Knut Melvær (@kmelve)
- Persian (Farsi)
- Mohammad Hossein Mojtahedi (@mhm5000)
- Polish
- Łukasz Szeremeta (@lszeremeta)
- Portuguese
- Ryan O'Mullan (@RyanOM)
- Spanish
- Ryan O'Mullan (@RyanOM)
- Simplifiled Chinese
- Hu Hao (@howiehu)
- Traditional Chinese
- Hu Hao (@howiehu)
- French
- Samy Pessé (@SamyPesse)
- Romanian
- Iancu Aurel (@awrelll)
- Finnish
- Tommi Savikko (@savikko)
- Japanese
- Iancu Aurel (@awrelll)
- Korean
- Iancu Aurel (@awrelll)
-197
View File
@@ -1,197 +0,0 @@
# Release notes
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 2.0.3
- Fix `gitbook init` for SUMMARY with empty entries
- Fix escaping of code blocks in markdown
## 2.0.2
- Fix relative links in windows
- Improve watcher in serve command (switch to chokidar)
- Add Romanian translation (`ro`)
- Add Finish translation (`fi`)
- Add Japanese translation (`jp`)
- Add Korean translation (`kr`)
## 2.0.1
- Improve error logging (display file, line and column)
- Add back support for `options.originalInput`
- Don't process math in markdown parser (delegated to `mathjax` plugin)
- Fix some cases of code blocks escaping
- Fix i18n for introduction title in json format
- Fix reload when book configuration is updated
- Fix backslashes in url when building on windows
## 2.0.0
- Fix page title of introduction
- Ignore codeblocks when replacing glossary terms
- Improve Unit Tests
- Fix scrolling position in website when preparing page
## 2.0.0-beta.5
- Fix progress order in json format
## 2.0.0-beta.4
- Fix default generator for use programmatically
- Add option "author" for html meta tags
- Fix links normalization (content and hash)
## 2.0.0-beta.3
- Fix odd cases with code blocks escaping
## 2.0.0-beta.2
- Fix definition of entry point title using SUMMARY.md
## 2.0.0-beta.1
- Fix windows incompatibility
- Add support for rtl (enabled by default for `ar` and `fa`)
- Escape code blocks in markdown parser
- Add Persian/Farsi translation (`fa`)
- Add Arabic translation (`ar`)
- Add Bengali translation (`bn`)
- Provide generator name in template context
## 2.0.0-alpha.9
- Fix links in sidebar
- Fix normalization of html link (README to index)
- Fix html snippets escaping
## 2.0.0-alpha.8
- Improve locale detection for i18n
- Fix chapter name for Glossary in pdf
- Don't escape html in glossary items
- Fix generation of multilingual book as ebook
- Add "post" block attribute to post-process
## 2.0.0-alpha.7
- Fix display of glossary in ebook formats
- Add default footer and header to pdf
- Fix generation of json format compatible with 1.x.x
- Add Simplifiled Chinese and Traditional Chinese translations
## 2.0.0-alpha.6
- Add es and pt translations
- Fix replacement of glossary terms
## 2.0.0-alpha.5
- Fix copy of files/covers
- Add back `finish:before` hook
## 2.0.0-alpha.4
- Fix copy of cover for multilingal books
## 2.0.0-alpha.3
- Norwegian translation
- Load plugins from book in priority
## 2.0.0-alpha.3
- Fix init command
- Update parsers to fix spaces in summary (`gitbook-parsers@0.3.1`)
## 2.0.0-alpha.1
- Externalize parsing into `gitbook-parsers` module
- Supports AsciiDoc and reStructuredText
- Hooks for page (`page:*`) are now deprecated, plugins should extend filters and blocks instead
- Hooks `summary` and `glossary` (after and before) have been removed
- Exercises and Quizzes are no longer parsed in the markdown parser
- Support for more markdown extensions: `.markdown`, `.mdown`
- Templates are rendered with nunjucks instead of swig, syntax is almost compatible, there is some changes with contexts and filters. `{{ super() }}` should be use instead of `{% parent %}`
- Clean output folder on build without removing `.git` and `.svn`
- MathJAX is no longer a default plugin
- SVG images are converted to PNG during generation of ebooks
- i18n in website and ebook (ru, it, de, fr)
- New templating syntax
- Content references (both internal and external)
- Glossary terms are handled during generation (also in ebook format)
## 1.5.0
- Fix `serve` command, broken by `1.4.2`
- Add nicer `dark` theme :)
## 1.4.2
- Force `process.exit` after builds, to prevent (possibly) lingering plugins
## 1.4.1
- Fix command 'install' without arguments
## 1.4.0
- Add command `gitbook install` to install plugins from book.json
- `package.json` is no longer necessary
## 1.3.4
- Add glossary to ebooks
- Fix autocover with new hook "finish:before"
- Add X-UA-Compatible meta tag for IE
## 1.3.3
- Fix parsing of lexed content using the client library
## 1.3.2
- ePub files are now passing validation from epubcheck
- Fix replacement of multiple glossary terms in a single sentence
- Fix on windows deep relative links
- Fix search indexer
## 1.3.1
- Fix error with links in markdown
## 1.3.0
- Bundle gitbook parsing library as a client side library in `gitbook.js` and `gitbook.min.js`
## 1.2.0
- Improvements on ebook generation
- Fix incorrect follow of links in ebook generation
- Move Table of Contents at the beginning of the ebook
- Update to last highlight.js (includes Swift)
- Includes of templates and variables (from book.json)
## 1.1.1
- Rewrite quiz logic to be more robust
- Improve integration of glossary
- Improve generation of ebook by using a multiple HTML pages input source
- Fix incorrect page breaks after h1 and h2 divs
- New options to set header and footer in PDF generation
## 1.1.0
- Plugins can now extend the ebook generation (pdf, epub, mobi)
- Update `kramed` to version 0.4.3
## 1.0.3
- Update `mathjax` plugin and MathJAx to version 2.4
- Update `highlight.js` to 8.2.0
## 1.0.2
- Update `mathjax` plugin, fixes issues with inline math rendering (no longer wanted)
## 1.0.1
- New inline math convention (kramdown's), using `$$` rather than `$` as delimiters
- Fix instapaper sharing
- The `exercises` & `quizzes` plugins are now by default
## 1.0.0
- New design
- Support for glossary
- Support for sharing to instapaper
- Support for footnotes
## 0.7.1
- Update `fs-extra` to `0.10.0` (fixes potential race conditions)
## 0.7.0
- Add page break in ebook (pdf, epub, mobi) between chapters/articles
- Start using kramed instead of marked
- Fix display of inline math
- Switch to graceful-fs to fix EMFILE errors
- Add sharing to weibo.com
## 0.6.2
- Support generating a plugin's book info dynamically
- Improve navigation on dark theme
- Improve path normalization when parsing SUMMARY.md
## 0.6.0
- Generate header id the same as github
- Custom links can be added at top of sidebar
- Summary can now be transformed by plugins
- Support importing code snippets
-96
View File
@@ -1,96 +0,0 @@
module.exports = function (grunt) {
var path = require("path");
// Load NPM tasks
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-requirejs');
grunt.loadNpmTasks("grunt-bower-install-simple");
// Init GRUNT configuraton
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
'bower-install-simple': {
options: {
color: true,
production: false,
directory: "theme/vendors"
}
},
less: {
development: {
options: {
compress: true,
yuicompress: true,
optimization: 2
},
files: {
"theme/assets/style.css": "theme/stylesheets/website.less",
"theme/assets/print.css": "theme/stylesheets/ebook.less"
}
}
},
requirejs: {
compile: {
options: {
name: "gitbook",
baseUrl: "theme/javascript/",
out: "theme/assets/app.js",
preserveLicenseComments: false,
optimize: "uglify",
include: ["requireLib"],
paths: {
"jQuery": '../vendors/jquery/dist/jquery',
"lodash": '../vendors/lodash/dist/lodash',
"requireLib": '../vendors/requirejs/require',
"Mousetrap": '../vendors/mousetrap/mousetrap',
"lunr": '../vendors/lunr.js/lunr',
"URIjs": '../vendors/URIjs/src/',
"ace": '../vendors/ace-builds/src-noconflict/'
},
shim: {
'jQuery': {
exports: '$'
},
'lodash': {
exports: '_'
},
'Mousetrap': {
exports: 'Mousetrap'
},
'lunr': {
exports: 'lunr'
}
}
}
}
},
copy: {
vendors: {
files: [
{
expand: true,
cwd: 'theme/vendors/fontawesome/fonts/',
src: ['**'],
dest: 'theme/assets/fonts/fontawesome/',
filter: 'isFile'
}
]
}
}
});
grunt.registerTask("bower-install", [ "bower-install-simple" ]);
// Build
grunt.registerTask('build', [
'bower-install',
'less',
'requirejs',
'copy:vendors'
]);
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 2014 FriendCode Inc.
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>.
+82 -181
View File
@@ -1,229 +1,130 @@
GitBook
=======
<h1 align="center">GitBook</h1>
[![NPM version](https://badge.fury.io/js/gitbook.svg)](http://badge.fury.io/js/gitbook)
[![Build Status](https://travis-ci.org/GitbookIO/gitbook.png?branch=master)](https://travis-ci.org/GitbookIO/gitbook)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/GitbookIO/gitbook?branch=master&svg=true)](https://ci.appveyor.com/project/SamyPesse/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 books using GitHub/Git and Markdown (or AsciiDoc). Here is an example: [Learn Javascript](https://www.gitbook.com/book/GitBookIO/javascript).
<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>
You can publish and host book easily online using [gitbook.com](https://www.gitbook.com), a web-editor is [also available](https://www.gitbook.com/editor).
<p align="center">Welcome to GitBook, the platform for managing technical knowledge for teams.</p>
Stay updated by following [@GitBookIO](https://twitter.com/GitBookIO) on Twitter or [GitBook](https://www.facebook.com/gitbookcom) on Facebook.
<p align="center">This repository contains the open-source code used to render GitBook's published content.</p>
Complete documentation is available at [help.gitbook.com](http://help.gitbook.com/).
<p align="center">
<img alt="GitBook Open Published Site" src="./assets/published-site.png">
</p>
![Image](https://raw.github.com/GitbookIO/gitbook/master/preview.png)
## Table of Contents
## How to use it:
- [Getting Started](#getting-started)
- [Contributing](#contributing)
- [Types of contributions](#types-of-contributions)
- [Licensing](#license)
- [Acknowledgements](#acknowledgements)
- [Legacy GitBook](#legacy-gitbook-deprecated)
GitBook can be installed from **NPM** using:
## 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-cli -g
git clone https://github.com/gitbookIO/gitbook.git
```
Create the directories and files for a book from its [SUMMARY.md](https://github.com/GitbookIO/gitbook#book-format) file (if existing) using
2. Install the project's dependencies through Bun.
```
$ gitbook init
bun install
```
You can serve a repository as a book using:
3. Start your local development server.
```
$ gitbook serve
bun dev
```
Or simply build the static website using:
Then open the space in your web browser, using http://localhost:3000/<host>/<path> (example: http://localhost:3000/docs.gitbook.com).
```
$ gitbook build
```
### Other development commands
## Features
- `bun format`: format the code
- `bun lint`: lint the code
* [Output as a website or ebook (pdf, epub, mobi)](http://help.gitbook.com/format/output.html)
* [Multi-Languages](http://help.gitbook.com/format/languages.html)
* [Glossary](http://help.gitbook.com/format/glossary.html)
* [Cover](http://help.gitbook.com/format/cover.html)
* [AsciiDoc Support](http://help.gitbook.com/format/asciidoc.html)
* [Variables and Templating](http://help.gitbook.com/format/templating.html)
* [Content References](http://help.gitbook.com/format/conrefs.html)
* [Plugins](http://help.gitbook.com/plugins/index.html)
* Interractive reader website:
* Search
* Font Settings (Serif, Sans Serif)
* Themes: white, sepia, night
### CI and testing
## Output Formats
All pull-requests will be tested against both visual and performances testing to prevent regressions.
GitBook can generate your book in the following formats:
## Contributing
* **Static Website**: This is the default format. It generates a complete interactive static website that can be, for example, hosted on GitHub Pages.
* **eBook**: You need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed. You can specify the eBook filename as the second argument, otherwise `book` will be used.
* Generate a **PDF** using: `gitbook pdf ./myrepo ./mybook.pdf`
* Generate a **ePub** using: `gitbook epub ./myrepo ./mybook.epub`
* Generate a **MOBI** using: `gitbook mobi ./myrepo ./mybook.mobi`
* **JSON**: This format is used for debugging or extracting metadata from a book. Generate this format using: ```gitbook build ./myrepo --format=json```.
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.
## Book Format
### Types of contributions
A book is a Git repository containing at least 2 files: `README.md` and `SUMMARY.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.
#### README.md
#### Translations
Typically, this should be the introduction for your book. It will be automatically added to the final summary.
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.
#### SUMMARY.md
#### Bugs
The `SUMMARY.md` defines your book's structure. It should contain a list of chapters, linking to their respective pages.
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.
Example:
## Deployment
```markdown
# Summary
> [!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.
This is the summary of my book.
This project allows you to self-host the rendering portion of your GitBook published content. Self-hosting has pros and cons.
* [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)
```
On the pro side, you can customize the look and feel of your content, and better embed your documentation in your application.
Files that are not included in `SUMMARY.md` will not be processed by `gitbook`.
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.
#### Multi-Languages
## License
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:
Distributed under the [GNU GPLv3 License](https://github.com/GitBookIO/gitbook/blob/main/LICENSE).
```markdown
* [English](en/)
* [French](fr/)
* [Español](es/)
```
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).
You can see a complete example with the [Learn Git](https://github.com/GitbookIO/git) book.
See `LICENSE` for more information.
#### Glossary
## Acknowledgements
Allows you to specify terms and their respective definitions to be displayed in the glossary. Based on those terms, `gitbook` will automatically build an index and highlight those terms in pages.
GitBook wouldn't be possible without these projects:
The `GLOSSARY.md` format is very simple :
- [Next.js](https://nextjs.org/)
- [Bun](https://bun.sh/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Framer Motion](https://www.npmjs.com/package/framer-motion)
```markdown
# term
Definition for this term
## Contributors
# Another term
With it's definition, this can contain bold text and all other kinds of inline markup ...
```
<a href="https://github.com/gitbookIO/gitbook/graphs/contributors">
<img src="https://contrib.rocks/image?repo=gitbookIO/gitbook" />
</a>
#### Variables and Templating
## Legacy GitBook (Deprecated)
A set of variables can be defined in the `book.json`:
```js
{
"variables": {
"host": "mybook.com"
}
}
```
These variables can be used in the markdown files:
```
The host is {{ book.host }}
```
You can also use condition with these variables:
```
{% if book.host == "mybook.com" %}
{% else %}
{% endif %}
```
Variables of `book.json` are available in the `book` namespace. You can also access informations about the `file` itself and the `gitbook` version:
```
My file is {{ file.path }}
Modified at {{ file.mtime }}
Book built with GitBook {{ gitbook.version }}
```
#### Content References
You can use "content references," or conrefs, when writing books or documentation using GitBook.
Include a file from the same book:
```
{% include "./test.md" %}
```
or from a git repository (with a specific revision):
```
{% include "git+https://github.com/GitbookIO/documentation.git/README.md#1.0.1" %}
```
Includes can be used with variables (see [Variables and Templating](#variables-and-templating)):
```
{% include book.ref_doc_readme %}
```
#### 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`).
Best practices for the `.gitignore` is to ignore build files from **node.js** (`node_modules`, ...) and build files from GitBook: `_book`, `*.epub`, `*.mobi` and `*.pdf` ([Download GitBook.gitignore](https://github.com/github/gitignore/blob/master/GitBook.gitignore)).
#### Cover
A cover image can be set by creating a file: **/cover.jpg**.
The best resolution is **1800x2360**. The generation of the cover can be done automatically using the plugin [autocover](https://github.com/GitbookIO/plugin-autocover).
A small version of the cover can also be set by creating a file: **/cover_small.jpg**.
#### AsciiDoc
Since version 2.0.0, AsciiDoc can be used instead of Markdown, simply replace the `.md` by the `.adoc` extension. Chapters in the summary are detected from an ordered list in the `SUMMARY.adoc`.
## Publish your book
The platform [GitBook.com](https://www.gitbook.com/) is like an "Heroku for books": you can create a book on it (public, paid, or private) and update it using **git push**.
## Plugins
Plugins can be used to extend your book's functionality. Read [GitbookIO/plugin](https://github.com/GitbookIO/plugin) for more information about how to build a plugin for GitBook.
Plugins needed to build a book can be installed using: `gitbook install ./`. You can find plugins at [plugins.gitbook.com](http://plugins.gitbook.com).
## Debugging
You can use the options `--log=debug` and `--debug` to get better error messages (with stack trace). For example:
```
$ gitbook build ./ -- log=debug --debug
```
#### How to use the latest commit from GitBook in gitbook-cli
To use the latest commit from `GitBook/gitbook` with `gitbook-cli`:
```
$ git clone https://github.com/GitbookIO/gitbook.git ./gitbook
$ gitbook versions:link 2.0.0 ./gitbook
```
Now the version tag `2.0.0` wil be associated with the `./gitbook` folder.
You can uninstall it using: `gitbook versions:uninstall 2.0.0`.
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).
-29
View File
@@ -1,29 +0,0 @@
# Fix line endings in Windows. (runs before repo cloning)
init:
- git config --global core.autocrlf input
# Test against these versions of Node.js.
environment:
matrix:
- nodejs_version: "0.10"
- nodejs_version: "0.11"
# Install scripts. (runs after repo cloning)
install:
# Get the latest stable version of Node.js or io.js
- ps: Install-Product node $env:nodejs_version
# install svgexport
- npm install svgexport -g
# install modules
- npm install
# Post-install test scripts.
test_script:
# Output useful info for debugging.
- node --version
- npm --version
# run tests
- npm test
# Don't actually build.
build: off
Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

-7
View File
@@ -1,7 +0,0 @@
#! /usr/bin/env node
var color = require('bash-color');
console.log(color.red("You need to install 'gitbook-cli' to have access to the gitbook command anywhere on your system."));
console.log(color.red("If you've installed this package globally, you need to uninstall it."));
console.log(color.red(">> Run 'npm uninstall -g gitbook' then 'npm install -g gitbook-cli'"));
-13
View File
@@ -1,13 +0,0 @@
{
"name": "GitBook",
"version": "1.0.0",
"dependencies": {
"jquery": "2.1.1",
"lodash": "2.4.1",
"requirejs": "2.1.11",
"URIjs": "1.13.1",
"mousetrap": "1.4.6",
"lunr.js": "0.5.2",
"fontawesome": "4.1.0"
}
}
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.
-865
View File
@@ -1,865 +0,0 @@
var Q = require("q");
var _ = require("lodash");
var path = require("path");
var util = require("util");
var lunr = require("lunr");
var parsers = require("gitbook-parsers");
var color = require("bash-color");
var fs = require("./utils/fs");
var parseNavigation = require("./utils/navigation");
var parseProgress = require("./utils/progress");
var pageUtil = require("./utils/page");
var batch = require("./utils/batch");
var links = require("./utils/links");
var i18n = require("./utils/i18n");
var logger = require("./utils/logger");
var Configuration = require("./configuration");
var TemplateEngine = require("./template");
var Plugin = require("./plugin");
var PluginsList = require("./pluginslist");
var generators = require("./generators");
var Book = function(root, context, parent) {
var that = this;
this.context = _.defaults(context || {}, {
// Extend book configuration
config: {},
// Log function
log: function(msg) {
process.stdout.write(msg);
},
// Log level
logLevel: "info"
});
// Log
this.log = logger(this.context.log, this.context.logLevel);
// Root folder of the book
this.root = path.resolve(root);
// Parent book
this.parent = parent;
// Configuration
this.config = new Configuration(this, this.context.config);
Object.defineProperty(this, "options", {
get: function () {
return this.config.options;
}
});
// Template
this.template = new TemplateEngine(this);
// Summary
this.summary = {};
this.navigation = [];
// Glossary
this.glossary = [];
// Langs
this.langs = [];
// Sub-books
this.books = [];
// Files in the book
this.files = [];
// List of plugins
this.plugins = new PluginsList(this);
// Structure files
this.summaryFile = null;
this.glossaryFile = null;
this.readmeFile = null;
this.langsFile = null;
// Search Index
this.searchIndex = lunr(function () {
this.ref('url');
this.field('title', { boost: 10 });
this.field('body');
});
// Bind methods
_.bindAll(this);
};
// Return string representation
Book.prototype.toString = function() {
return "[Book "+this.root+"]";
};
// Initialize and parse the book: config, summary, glossary
Book.prototype.parse = function() {
var that = this;
var multilingual = false;
return this.parseConfig()
.then(function() {
return that.parsePlugins();
})
.then(function() {
return that.parseLangs()
.then(function() {
multilingual = that.langs.length > 0;
if (multilingual) that.log.info.ln("Parsing multilingual book, with", that.langs.length, "languages");
// Sub-books that inherit from the current book configuration
that.books = _.map(that.langs, function(lang) {
that.log.info.ln("Preparing language book", lang.lang);
return new Book(
path.join(that.root, lang.path),
_.merge({}, that.context, {
config: _.extend({}, that.options, {
'output': path.join(that.options.output, lang.lang),
'language': lang.lang
})
}),
that
)
});
});
})
.then(function() {
if (multilingual) return;
return that.listAllFiles();
})
.then(function() {
if (multilingual) return;
return that.parseReadme();
})
.then(function() {
if (multilingual) return;
return that.parseSummary();
})
.then(function() {
if (multilingual) return;
return that.parseGlossary();
})
.then(function() {
// Init sub-books
return _.reduce(that.books, function(prev, book) {
return prev.then(function() {
return book.parse();
});
}, Q());
})
.thenResolve(this);
};
// Generate the output
Book.prototype.generate = function(generator) {
var that = this;
that.options.generator = generator || that.options.generator;
that.log.info.ln("start generation with", that.options.generator, "generator");
return Q()
// Clean output folder
.then(function() {
that.log.info("clean", that.options.generator, "generator");
return fs.clean(that.options.output)
.progress(function(p) {
that.log.debug.ln("remove", p.file, "("+p.i+"/"+p.count+")");
})
.then(function() {
that.log.info.ok();
});
})
// Create generator
.then(function() {
var Generator = generators[generator];
if (!Generator) throw "Generator '"+that.options.generator+"' doesn't exist";
generator = new Generator(that);
return generator.prepare();
})
// Generate content
.then(function() {
if (that.isMultilingual()) {
return that.generateMultiLingual(generator);
} else {
// Separate list of files into the different operations needed
var ops = _.groupBy(that.files, function(file) {
if (file[file.length -1] == "/") {
return "directories";
} else if (_.contains(parsers.extensions, path.extname(file)) && that.navigation[file]) {
return "content";
} else {
return "files";
}
});
return Q()
// First, let's create folder
.then(function() {
return _.reduce(ops["directories"] || [], function(prev, folder) {
return prev.then(function() {
that.log.debug.ln("transferring folder", folder);
return Q(generator.transferFolder(folder));
});
}, Q());
})
// Then, let's copy other files
.then(function() {
return Q.all(_.map(ops["files"] || [], function(file) {
that.log.debug.ln("transferring file", file);
return Q(generator.transferFile(file));
}));
})
// Finally let's generate content
.then(function() {
var nFiles = (ops["content"] || []).length;
return _.reduce(ops["content"] || [], function(prev, file, i) {
return prev.then(function() {
var p = ((i*100)/nFiles).toFixed(0)+"%";
that.log.debug.ln("processing", file, p);
return Q(generator.convertFile(file))
.fail(function(err) {
// Transform error message to signal file
throw that.normError(err, {
fileName: file
});
});
});
}, Q());
});
}
})
// Finish generation
.then(function() {
return generator.callHook("finish:before");
})
.then(function() {
return generator.finish();
})
.then(function() {
return generator.callHook("finish");
})
.then(function() {
that.log.info.ln("generation is finished");
});
};
// Generate the output for a multilingual book
Book.prototype.generateMultiLingual = function(generator) {
var that = this;
return Q()
.then(function() {
// Generate sub-books
return _.reduce(that.books, function(prev, book) {
return prev.then(function() {
return book.generate(that.options.generator);
});
}, Q());
});
};
// Extract files from ebook generated
Book.prototype.generateFile = function(output, options) {
var book = this;
options = _.defaults(options || {}, {
ebookFormat: path.extname(output).slice(1)
});
output = output || path.resolve(book.root, "book."+options.ebookFormat);
return fs.tmp.dir()
.then(function(tmpDir) {
book.setOutput(tmpDir);
return book.generate(options.ebookFormat)
.then(function(_options) {
var copyFile = function(lang) {
var _outputFile = output;
var _tmpDir = tmpDir;
if (lang) {
_outputFile = _outputFile.slice(0, -path.extname(_outputFile).length)+"_"+lang+path.extname(_outputFile);
_tmpDir = path.join(_tmpDir, lang);
}
book.log.debug.ln("copy ebook to", _outputFile);
return fs.copy(
path.join(_tmpDir, "index."+options.ebookFormat),
_outputFile
);
};
// Multi-langs book
return Q()
.then(function() {
if (book.isMultilingual()) {
return Q.all(
_.map(book.langs, function(lang) {
return copyFile(lang.lang);
})
)
.thenResolve(book.langs.length);
} else {
return copyFile().thenResolve(1);
}
})
.then(function(n) {
book.log.info.ok(n+" file(s) generated");
return fs.remove(tmpDir);
});
});
});
};
// Parse configuration
Book.prototype.parseConfig = function() {
var that = this;
that.log.info("loading book configuration....")
return that.config.load()
.then(function() {
that.log.info.ok();
});
};
// Parse list of plugins
Book.prototype.parsePlugins = function() {
var that = this;
var failed = [];
// Load plugins
return that.plugins.load(that.options.plugins)
.then(function() {
if (_.size(that.plugins.failed) > 0) return Q.reject(new Error("Error loading plugins: "+that.plugins.failed.join(",")+". Run 'gitbook install' to install plugins from NPM."));
that.log.info.ok(that.plugins.count()+" plugins loaded");
that.log.debug.ln("normalize plugins list");
});
};
// Parse readme to extract defaults title and description
Book.prototype.parseReadme = function() {
var that = this;
var structure = that.config.getStructure("readme");
that.log.debug.ln("start parsing readme:", structure);
return that.findFile(structure)
.then(function(readme) {
if (!readme) throw "No README file";
if (!_.contains(that.files, readme.path)) throw "README file is ignored";
that.readmeFile = readme.path;
that._defaultsStructure(that.readmeFile);
that.log.debug.ln("readme located at", that.readmeFile);
return that.template.renderFile(that.readmeFile)
.then(function(content) {
return readme.parser.readme(content)
.fail(function(err) {
throw that.normError(err, {
name: err.name || "Readme Parse Error",
fileName: that.readmeFile
});
});
});
})
.then(function(readme) {
that.options.title = that.options.title || readme.title;
that.options.description = that.options.description || readme.description;
});
};
// Parse langs to extract list of sub-books
Book.prototype.parseLangs = function() {
var that = this;
var structure = that.config.getStructure("langs");
that.log.debug.ln("start parsing languages index:", structure);
return that.findFile(structure)
.then(function(langs) {
if (!langs) return [];
that.langsFile = langs.path;
that._defaultsStructure(that.langsFile);
that.log.debug.ln("languages index located at", that.langsFile);
return that.template.renderFile(that.langsFile)
.then(function(content) {
return langs.parser.langs(content)
.fail(function(err) {
throw that.normError(err, {
name: err.name || "Langs Parse Error",
fileName: that.langsFile
});
});
});
})
.then(function(langs) {
that.langs = langs;
});
};
// Parse summary to extract list of chapters
Book.prototype.parseSummary = function() {
var that = this;
var structure = that.config.getStructure("summary");
that.log.debug.ln("start parsing summary:", structure);
return that.findFile(structure)
.then(function(summary) {
if (!summary) throw "No SUMMARY file";
// Remove the summary from the list of files to parse
that.summaryFile = summary.path;
that._defaultsStructure(that.summaryFile);
that.files = _.without(that.files, that.summaryFile);
that.log.debug.ln("summary located at", that.summaryFile);
return that.template.renderFile(that.summaryFile)
.then(function(content) {
return summary.parser.summary(content, {
entryPoint: that.readmeFile,
entryPointTitle: that.i18n('SUMMARY_INTRODUCTION'),
files: that.files
})
.fail(function(err) {
throw that.normError(err, {
name: err.name || "Summary Parse Error",
fileName: that.summaryFile
});
});
});
})
.then(function(summary) {
that.summary = summary;
that.navigation = parseNavigation(that.summary, that.files);
});
};
// Parse glossary to extract terms
Book.prototype.parseGlossary = function() {
var that = this;
var structure = that.config.getStructure("glossary");
that.log.debug.ln("start parsing glossary: ", structure);
return that.findFile(structure)
.then(function(glossary) {
if (!glossary) return [];
// Remove the glossary from the list of files to parse
that.glossaryFile = glossary.path;
that._defaultsStructure(that.glossaryFile);
that.files = _.without(that.files, that.glossaryFile);
that.log.debug.ln("glossary located at", that.glossaryFile);
return that.template.renderFile(that.glossaryFile)
.then(function(content) {
return glossary.parser.glossary(content)
.fail(function(err) {
throw that.normError(err, {
name: err.name || "Glossary Parse Error",
fileName: that.glossaryFile
});
});
});
})
.then(function(glossary) {
that.glossary = glossary;
});
};
// Parse a page
Book.prototype.parsePage = function(filename, options) {
var that = this, page = {};
options = _.defaults(options || {}, {
// Transform svg images
convertImages: false,
// Interpolate before templating
interpolateTemplate: _.identity,
// Interpolate after templating
interpolateContent: _.identity
});
var interpolate = function(fn) {
return Q(fn(page))
.then(function(_page) {
page = _page;
});
};
that.log.debug.ln("start parsing file", filename);
var extension = path.extname(filename);
var filetype = parsers.get(extension);
if (!filetype) return Q.reject(new Error("Can't parse file: "+filename));
// Type of parser used
page.type = filetype.name;
// Path relative to book
page.path = filename;
// Path absolute in the system
page.rawPath = path.resolve(that.root, filename);
// Progress in the book
page.progress = parseProgress(that.navigation, filename);
that.log.debug.ln("render template", filename);
// Read file content
return that.readFile(page.path)
.then(function(content) {
page.content = content;
return interpolate(options.interpolateTemplate);
})
// Prepare page markup
.then(function() {
return filetype.page.prepare(page.content)
.then(function(content) {
page.content = content;
});
})
// Generate template
.then(function() {
return that.template.renderPage(page);
})
// Prepare and Parse markup
.then(function(content) {
page.content = content;
that.log.debug.ln("use file parser", filetype.name, "for", filename);
return filetype.page(page.content);
})
// Post process sections
.then(function(_page) {
return _.reduce(_page.sections, function(prev, section) {
return prev.then(function(_sections) {
return that.template.postProcess(section.content || "")
.then(function(content) {
section.content = content;
return _sections.concat([section]);
})
});
}, Q([]));
})
// Prepare html
.then(function(_sections) {
return pageUtil.normalize(_sections, {
book: that,
convertImages: options.convertImages,
input: filename,
navigation: that.navigation,
base: path.dirname(filename) || './',
output: path.dirname(filename) || './',
glossary: that.glossary
});
})
// Interpolate output
.then(function(_sections) {
page.sections = _sections;
return interpolate(options.interpolateContent);
})
.then(function() {
that.indexPage(page);
return page;
});
};
// Find file that can be parsed with a specific filename
Book.prototype.findFile = function(filename) {
var that = this;
return _.reduce(parsers.extensions, function(prev, ext) {
return prev.then(function(output) {
// Stop if already find a parser
if (output) return output;
var filepath = filename+ext;
return that.fileExists(filepath)
.then(function(exists) {
if (!exists) return null;
return {
parser: parsers.get(ext),
path: filepath
};
})
});
}, Q(null));
};
// Check if a file exists in the book
Book.prototype.fileExists = function(filename) {
return fs.exists(
path.join(this.root, filename)
);
};
// Read a file
Book.prototype.readFile = function(filename) {
return fs.readFile(
path.join(this.root, filename),
{ encoding: "utf8" }
);
};
// Return stat for a file
Book.prototype.statFile = function(filename) {
return fs.stat(path.join(this.root, filename));
};
// List all files in the book
Book.prototype.listAllFiles = function() {
var that = this;
return fs.list(this.root, {
ignoreFiles: ['.ignore', '.gitignore', '.bookignore'],
ignoreRules: [
// Skip Git stuff
'.git/',
'.gitignore',
// Skip OS X meta data
'.DS_Store',
// Skip stuff installed by plugins
'node_modules',
// Skip book outputs
'_book',
'*.pdf',
'*.epub',
'*.mobi',
// Skip config files
'.ignore',
'.bookignore',
'book.json',
]
})
.then(function(_files) {
that.files = _files;
});
};
// Return true if the book is a multilingual book
Book.prototype.isMultilingual = function(filename) {
return this.books.length > 0;
};
// Return root of the parent
Book.prototype.parentRoot = function() {
if (this.parent) return this.parent.parentRoot();
return this.root;
};
// Return true if it's a sub-book
Book.prototype.isSubBook = function() {
return !!this.parent;
};
// Test if the file is the entry point
Book.prototype.isEntryPoint = function(fp) {
return fp == this.readmeFile;
};
// Resolve a path in book
Book.prototype.resolve = function(p) {
return path.resolve(this.root, p);
};
// Normalize a path to .html and convert README -> index
Book.prototype.contentPath = function(link) {
if (
path.basename(link, path.extname(link)) == "README"
|| link == this.readmeFile
) {
link = path.join(path.dirname(link), "index"+path.extname(link));
}
link = links.changeExtension(link, ".html");
return link;
}
// Normalize a link to .html and convert README -> index
Book.prototype.contentLink = function(link) {
return links.normalize(this.contentPath(link));
}
// Index a page into the search index
Book.prototype.indexPage = function(page) {
var nav = this.navigation[page.path];
if (!nav) return;
this.log.debug.ln("index page", page.path);
this.searchIndex.add({
url: this.contentLink(page.path),
title: nav.title,
body: pageUtil.extractText(page.sections),
});
};
// Default structure paths to an extension
Book.prototype._defaultsStructure = function(filename) {
var that = this;
var extension = path.extname(filename);
that.readmeFile = that.readmeFile || that.config.getStructure("readme")+extension;
that.summaryFile = that.summaryFile || that.config.getStructure("summary")+extension;
that.glossaryFile = that.glossaryFile || that.config.getStructure("glossary")+extension;
that.langsFile = that.langsFile || that.config.getStructure("langs")+extension;
}
// Change output path
Book.prototype.setOutput = function(p) {
var that = this;
this.options.output = path.resolve(p);
_.each(this.books, function(book) {
book.setOutput(path.join(that.options.output, book.options.language));
});
};
// Translate a strign according to the book language
Book.prototype.i18n = function(phrase) {
var args = Array.prototype.slice.call(arguments);
return i18n.__.apply({}, [this.config.normalizeLanguage()].concat(args));
};
// Normalize error
Book.prototype.normError = function(err, opts, defs) {
if (_.isString(err)) err = new Error(err);
// Extend err
_.extend(err, opts || {});
_.defaults(err, defs || {});
err.lineNumber = err.lineNumber || err.lineno;
err.columnNumber = err.columnNumber || err.colno;
err.toString = function() {
var attributes = [];
if (this.fileName) attributes.push("In file '"+this.fileName+"'");
if (this.lineNumber) attributes.push("Line "+this.lineNumber);
if (this.columnNumber) attributes.push("Column "+this.columnNumber);
return (this.name || "Error")+": "+this.message+((attributes.length > 0)? " ("+attributes.join(", ")+")" : "")
};
return err;
};
// Init and return a book
Book.init = function(root, opts) {
var book = new Book(root, opts);
var extensionToUse = ".md";
var chaptersPaths = function(chapters) {
return _.reduce(chapters || [], function(accu, chapter) {
var o = {
title: chapter.title
};
if (chapter.path) o.path = chapter.path;
return accu.concat(
[o].concat(chaptersPaths(chapter.articles))
);
}, []);
};
book.log.info.ln("init book at", root);
return fs.mkdirp(root)
.then(function() {
book.log.info.ln("detect structure from SUMMARY (if it exists)");
return book.parseSummary();
})
.fail(function() {
return Q();
})
.then(function() {
var summary = book.summaryFile || "SUMMARY.md";
var chapters = book.summary.chapters || [];
extensionToUse = path.extname(summary);
if (chapters.length == 0) {
chapters = [
{
title: "Summary",
path: "SUMMARY"+extensionToUse
},
{
title: "Introduction",
path: "README"+extensionToUse
}
];
}
return Q(chaptersPaths(chapters));
})
.then(function(chapters) {
// Create files that don't exist
return Q.all(_.map(chapters, function(chapter) {
if (!chapter.path) return Q();
var absolutePath = path.resolve(book.root, chapter.path);
return fs.exists(absolutePath)
.then(function(exists) {
if(exists) {
book.log.info.ln("found", chapter.path);
return;
} else {
book.log.info.ln("create", chapter.path);
}
return fs.mkdirp(path.dirname(absolutePath))
.then(function() {
return fs.writeFile(absolutePath, '# '+chapter.title+'\n');
});
});
}));
})
.then(function() {
book.log.info.ln("initialization is finished");
});
};
module.exports= Book;
-265
View File
@@ -1,265 +0,0 @@
var _ = require("lodash");
var Q = require("q");
var path = require("path");
var semver = require('semver');
var pkg = require('../package.json');
var fs = require("./utils/fs");
var i18n = require("./utils/i18n");
// Default plugins added to each books
var defaultsPlugins = [];
// Normalize a list of plugins to use
function normalizePluginsList(plugins) {
// Normalize list to an array
plugins = _.isString(plugins) ? plugins.split(",") : (plugins || []);
// Remove empty parts
plugins = _.compact(plugins);
// Divide as {name, version} to handle format like "myplugin@1.0.0"
plugins = _.map(plugins, function(plugin) {
if (plugin.name) return plugin;
var parts = plugin.split("@");
return {
'name': parts[0],
'version': parts[1] // optional
}
});
// List plugins to remove
var toremove = _.chain(plugins)
.filter(function(plugin) {
return plugin.name.length > 0 && plugin.name[0] == "-";
})
.map(function(plugin) {
return plugin.name.slice(1);
})
.value();
// Merge with defaults
plugins = _.chain(plugins)
.concat(_.map(defaultsPlugins, function(plugin) {
return { 'name': plugin }
}))
.uniq()
.value();
// Build final list
plugins = _.filter(plugins, function(plugin) {
return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == "-");
});
return plugins;
}
// Normalize a list of plugin name to use
function normalizePluginsNames(plugins) {
return _.pluck(normalizePluginsList(plugins), "name");
};
var Configuration = function(book, options) {
var that = this;
this.book = book;
this.options = _.cloneDeep(Configuration.DEFAULT);
this.options = _.merge(this.options, options || {});
// options.input == book.root
Object.defineProperty(this.options, "input", {
get: function () {
return that.book.root;
}
});
// options.originalInput == book.parent.root
Object.defineProperty(this.options, "originalInput", {
get: function () {
return that.book.parent? that.book.parent.root : undefined;
}
});
// options.originalOutput == book.parent.options.output
Object.defineProperty(this.options, "originalOutput", {
get: function () {
return that.book.parent? that.book.parent.options.output : undefined;
}
});
};
// Read and parse the configuration
Configuration.prototype.load = function() {
var that = this;
return Q()
.then(function() {
var configPath, config;
try {
configPath = require.resolve(
path.resolve(that.book.root, that.options.configFile)
);
// Invalidate node.js cache for livreloading
delete require.cache[configPath];
_config = require(configPath);
that.options = _.merge(
that.options,
_.omit(_config, 'configFile', 'defaultsPlugins', 'generator', 'extension')
);
}
catch(err) {
if (err instanceof SyntaxError) return Q.reject(err);
return Q();
}
})
.then(function() {
if (!semver.satisfies(pkg.version, that.options.gitbook)) {
throw "GitBook version doesn't satisfy version required by the book: "+that.options.gitbook;
}
if (that.options.gitbook == "*") {
that.book.log.warn.ln("you should specify a gitbook version to use in your book.json, for example: "+(_.first(pkg.version.split("."))+".x.x"));
}
that.options.output = path.resolve(that.options.output || path.join(that.book.root, "_book"));
that.options.plugins = normalizePluginsList(that.options.plugins);
that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || "");
that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins);
that.options.plugins = _.uniq(that.options.plugins, "name");
// Default value for text direction (from language)
if (!that.options.direction) {
var lang = i18n.getCatalog(that.options.language);
if (lang) that.options.direction = lang.direction;
}
that.options.gitbook = pkg.version;
});
};
// Extend the configuration
Configuration.prototype.extend = function(options) {
_.extend(this.options, options);
};
// Get structure file
Configuration.prototype.getStructure = function(name) {
return this.options.structure[name].split(".").slice(0, -1).join(".");
};
// Return normalized language
Configuration.prototype.normalizeLanguage = function() {
return i18n.normalizeLanguage(this.options.language);
};
// Default configuration
Configuration.DEFAULT = {
// Options that can't be extend
"configFile": "book",
"generator": "website",
"extension": null,
// Book metadats (somes are extracted from the README by default)
"title": null,
"description": null,
"isbn": null,
"language": "en",
"direction": null,
"author": null,
// version of gitbook to use
"gitbook": "*",
// Structure
"structure": {
"langs": "LANGS.md",
"readme": "README.md",
"glossary": "GLOSSARY.md",
"summary": "SUMMARY.md"
},
// CSS Styles
"styles": {
"website": "styles/website.css",
"ebook": "styles/ebook.css",
"pdf": "styles/pdf.css",
"mobi": "styles/mobi.css",
"epub": "styles/epub.css"
},
// Plugins list, can contain "-name" for removing default plugins
"plugins": [],
// Global configuration for plugins
"pluginsConfig": {
"fontSettings": {
"theme": null, //"sepia", "night" or "white",
"family": "sans",// "serif" or "sans",
"size": 2 // 1 - 4
}
},
// Variables for templating
"variables": {},
// Set another theme with your own layout
// It's recommended to use plugins or add more options for default theme, though
// See https://github.com/GitbookIO/gitbook/issues/209
"theme": path.resolve(__dirname, '../theme'),
// Links in template (null: default, false: remove, string: new value)
"links": {
// Custom links at top of sidebar
"sidebar": {
//"Custom link name": "https://customlink.com"
},
// Sharing links
"sharing": {
"google": null,
"facebook": null,
"twitter": null,
"weibo": null,
"all": null
}
},
// Options for PDF generation
"pdf": {
// Add toc at the end of the file
"toc": true,
// Add page numbers to the bottom of every page
"pageNumbers": false,
// Font size for the file content
"fontSize": 12,
// Paper size for the pdf
// Choices are [ua0, ua1, ua2, ua3, ua4, ua5, ua6, ub0, ub1, ub2, ub3, ub4, ub5, ub6, ulegal, uletter]
"paperSize": "a4",
// Margin (in pts)
// Note: 72 pts equals 1 inch
"margin": {
"right": 62,
"left": 62,
"top": 56,
"bottom": 56
},
//Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
"headerTemplate": "",
//Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
"footerTemplate": ""
}
};
module.exports= Configuration;
-78
View File
@@ -1,78 +0,0 @@
var _ = require("lodash");
var path = require("path");
var Q = require("q");
var fs = require("./utils/fs");
var Plugin = require("./plugin");
var BaseGenerator = function(book) {
this.book = book;
Object.defineProperty(this, "options", {
get: function () {
return this.book.options;
}
});
_.bindAll(this);
};
BaseGenerator.prototype.callHook = function(name, data) {
return this.book.plugins.hook(name, data);
};
// Prepare the genertor
BaseGenerator.prototype.prepare = function() {
var that = this;
return that.callHook("init");
};
// Write a parsed file to the output
BaseGenerator.prototype.convertFile = function(input) {
return Q.reject(new Error("Could not convert "+input));
};
// Copy file to the output (non parsable)
BaseGenerator.prototype.transferFile = function(input) {
return fs.copy(
path.join(this.book.root, input),
path.join(this.options.output, input)
);
};
// Copy a folder to the output
BaseGenerator.prototype.transferFolder = function(input) {
return fs.mkdirp(
path.join(this.book.options.output, input)
);
};
// Copy the cover picture
BaseGenerator.prototype.copyCover = function() {
var that = this;
return Q.all([
fs.copy(path.join(that.book.root, "cover.jpg"), path.join(that.options.output, "cover.jpg")),
fs.copy(path.join(that.book.root, "cover_small.jpg"), path.join(that.options.output, "cover_small.jpg"))
])
.fail(function() {
// If orignaly from multi-lang, try copy from parent
if (!that.book.isSubBook()) return;
return Q.all([
fs.copy(path.join(that.book.parentRoot(), "cover.jpg"), path.join(that.options.output, "cover.jpg")),
fs.copy(path.join(that.book.parentRoot(), "cover_small.jpg"), path.join(that.options.output, "cover_small.jpg"))
]);
})
.fail(function(err) {
return Q();
});
};
// At teh end of the generation
BaseGenerator.prototype.finish = function() {
return Q.reject(new Error("Could not finish generation"));
};
module.exports = BaseGenerator;
-139
View File
@@ -1,139 +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("../utils/fs");
var stringUtils = require("../utils/string");
var BaseGenerator = require("./website");
var Generator = function(book, format) {
BaseGenerator.apply(this, arguments);
// eBook format
this.ebookFormat = format;
// Resources namespace
this.namespace = "ebook";
// Styles to use
this.styles = _.compact(["ebook", this.ebookFormat]);
// Convert images (svg -> png)
this.convertImages = true;
};
util.inherits(Generator, BaseGenerator);
Generator.prototype.prepareTemplates = function() {
this.templates["page"] = this.book.plugins.template("ebook:page") || path.resolve(this.options.theme, 'templates/ebook/page.html');
this.templates["summary"] = this.book.plugins.template("ebook:summary") || path.resolve(this.options.theme, 'templates/ebook/summary.html');
this.templates["glossary"] = this.book.plugins.template("ebook:glossary") || path.resolve(this.options.theme, 'templates/ebook/glossary.html');
return Q();
};
// Generate table of contents
Generator.prototype.writeSummary = function() {
var that = this;
that.book.log.info.ln("write SUMMARY.html");
return this._writeTemplate(this.templates["summary"], {}, path.join(this.options.output, "SUMMARY.html"));
};
Generator.prototype.finish = function() {
var that = this;
return Q()
.then(this.copyAssets)
.then(this.copyCover)
.then(this.writeGlossary)
.then(this.writeSummary)
.then(function() {
if (!that.ebookFormat) return Q();
var d = Q.defer();
if (!that.options.cover && fs.existsSync(path.join(that.options.output, "cover.jpg"))) {
that.options.cover = path.join(that.options.output, "cover.jpg");
}
var _options = {
"--cover": that.options.cover,
"--title": that.options.title,
"--comments": that.options.description,
"--isbn": that.options.isbn,
"--authors": that.options.author,
"--language": that.options.language,
"--book-producer": "GitBook",
"--publisher": "GitBook",
"--chapter": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter ')]",
"--chapter-mark": "pagebreak",
"--page-breaks-before": "/",
"--level1-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-1 ')]",
"--level2-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-2 ')]",
"--level3-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-3 ')]",
"--no-chapters-in-toc": true,
"--max-levels": "1",
"--breadth-first": true
};
if (that.ebookFormat == "pdf") {
var pdfOptions = that.options.pdf;
_.extend(_options, {
"--margin-left": String(pdfOptions.margin.left),
"--margin-right": String(pdfOptions.margin.right),
"--margin-top": String(pdfOptions.margin.top),
"--margin-bottom": String(pdfOptions.margin.bottom),
"--pdf-default-font-size": String(pdfOptions.fontSize),
"--pdf-mono-font-size": String(pdfOptions.fontSize),
"--paper-size": String(pdfOptions.paperSize),
"--pdf-page-numbers": Boolean(pdfOptions.pageNumbers),
"--pdf-header-template": String(pdfOptions.headerTemplate) || "<p class='header'><span>"+that.options.title+"</span></p>",
"--pdf-footer-template": String(pdfOptions.footerTemplate) || "<p class='footer'><span>_SECTION_</span> <span style='float:right;'>_PAGENUM_</span></p>"
});
} else if (that.ebookFormat == "epub") {
_.extend(_options, {
"--dont-split-on-page-breaks": true
});
}
var command = [
"ebook-convert",
path.join(that.options.output, "SUMMARY.html"),
path.join(that.options.output, "index."+that.ebookFormat),
stringUtils.optionsToShellArgs(_options)
].join(" ");
that.book.log.info("start conversion to", that.ebookFormat, "....");
var child = exec(command, function (error, stdout, stderr) {
if (error) {
that.book.log.info.fail();
if (error.code == 127) {
error.message = "Need to install ebook-convert from Calibre";
} else {
error.message = error.message + " "+stdout;
}
return d.reject(error);
}
that.book.log.info.ok();
d.resolve();
});
child.stdout.on('data', function (data) {
that.book.log.debug(data);
});
child.stderr.on('data', function (data) {
that.book.log.debug(data);
});
return d.promise;
});
};
module.exports = Generator;
-11
View File
@@ -1,11 +0,0 @@
var _ = require("lodash");
var EbookGenerator = require("./ebook");
module.exports = {
json: require("./json"),
website: require("./website"),
ebook: EbookGenerator,
pdf: _.partialRight(EbookGenerator, "pdf"),
mobi: _.partialRight(EbookGenerator, "mobi"),
epub: _.partialRight(EbookGenerator, "epub")
};
-76
View File
@@ -1,76 +0,0 @@
var util = require("util");
var path = require("path");
var Q = require("q");
var _ = require("lodash");
var fs = require("../utils/fs");
var BaseGenerator = require("../generator");
var links = require("../utils/links");
var Generator = function() {
BaseGenerator.apply(this, arguments);
};
util.inherits(Generator, BaseGenerator);
// Ignore some methods
Generator.prototype.transferFile = function(input) { };
// Convert an input file
Generator.prototype.convertFile = function(input) {
var that = this;
return that.book.parsePage(input)
.then(function(page) {
var json = {
progress: page.progress,
sections: page.sections
};
var output = links.changeExtension(page.path, ".json");
output = path.join(that.options.output, output);
return fs.writeFile(
output,
JSON.stringify(json, null, 4)
);
});
};
// Finish generation
Generator.prototype.finish = function() {
return this.writeReadme();
};
// Write README.json
Generator.prototype.writeReadme = function() {
var that = this;
var mainlang, langs;
return Q()
.then(function() {
langs = that.book.langs;
mainLang = langs.length > 0? _.first(langs).lang : null;
readme = links.changeExtension(that.book.readmeFile, ".json");
// Read readme from main language
return fs.readFile(
mainLang? path.join(that.options.output, mainLang, readme) : path.join(that.options.output, readme)
);
})
.then(function(content) {
// Extend it with infos about the languages
var json = JSON.parse(content);
_.extend(json, {
langs: langs
});
// Write it as README.json
return fs.writeFile(
path.join(that.options.output, "README.json"),
JSON.stringify(json, null, 4)
);
});
};
module.exports = Generator;
-274
View File
@@ -1,274 +0,0 @@
var util = require("util");
var path = require("path");
var Q = require("q");
var _ = require("lodash");
var nunjucks = require("nunjucks");
var AutoEscapeExtension = require("nunjucks-autoescape");
var FilterExtension = require("nunjucks-filter");
var fs = require("../utils/fs");
var BaseGenerator = require("../generator");
var links = require("../utils/links");
var pageUtil = require("../utils/page");
var i18n = require("../utils/i18n");
var pkg = require("../../package.json");
var Generator = function() {
BaseGenerator.apply(this, arguments);
// Revision
this.revision = new Date();
// Resources namespace
this.namespace = "website";
// Style to integrates in the output
this.styles = ["website"];
// Convert images (svg -> png)
this.convertImages = false;
// Templates
this.templates = {};
};
util.inherits(Generator, BaseGenerator);
// Prepare the genertor
Generator.prototype.prepare = function() {
return BaseGenerator.prototype.prepare.apply(this)
.then(this.prepareStyles)
.then(this.prepareTemplates)
.then(this.prepareTemplateEngine);
};
// Prepare all styles
Generator.prototype.prepareStyles = function() {
var that = this;
this.styles = _.chain(this.styles)
.map(function(style) {
var stylePath = that.options.styles[style];
if (fs.existsSync(path.resolve(that.book.root, stylePath))) {
return stylePath;
}
return null;
})
.compact()
.value();
return Q();
};
// Prepare templates
Generator.prototype.prepareTemplates = function() {
this.templates["page"] = this.book.plugins.template("site:page") || path.resolve(this.options.theme, 'templates/website/page.html');
this.templates["langs"] = this.book.plugins.template("site:langs") || path.resolve(this.options.theme, 'templates/website/langs.html');
this.templates["glossary"] = this.book.plugins.template("site:glossary") || path.resolve(this.options.theme, 'templates/website/glossary.html');
return Q();
};
// Prepare template engine
Generator.prototype.prepareTemplateEngine = function() {
var that = this;
return Q()
.then(function() {
var language = that.book.config.normalizeLanguage();
if (!i18n.hasLocale(language)) {
that.book.log.warn.ln("Language '"+language+"' is not available as a layout locales (en, "+i18n.getLocales().join(", ")+")");
}
var folders = _.chain(that.templates)
.values()
.map(path.dirname)
.uniq()
.value();
that.env = new nunjucks.Environment(
new nunjucks.FileSystemLoader(folders),
{
autoescape: true
}
);
// Add filter
that.env.addFilter("contentLink", that.book.contentLink.bind(that.book));
that.env.addFilter('lvl', function(lvl) {
return lvl.split(".").length;
});
that.env.addGlobal('__', that.book.i18n.bind(that.book));
// Add extension
that.env.addExtension('AutoEscapeExtension', new AutoEscapeExtension(that.env));
that.env.addExtension('FilterExtension', new FilterExtension(that.env));
});
};
// Finis generation
Generator.prototype.finish = function() {
return this.copyAssets()
.then(this.copyCover)
.then(this.writeGlossary)
.then(this.writeSearchIndex)
.then(this.writeLangsIndex)
};
// Convert an input file
Generator.prototype.convertFile = function(input) {
var that = this;
return that.book.parsePage(input, {
convertImages: that.convertImages,
interpolateTemplate: function(page) {
return that.callHook("page:before", page);
},
interpolateContent: function(page) {
return that.callHook("page", page);
}
})
.then(function(page) {
var relativeOutput = that.book.contentPath(page.path);
var output = path.join(that.options.output, relativeOutput);
var basePath = path.relative(path.dirname(output), that.options.output) || ".";
if (process.platform === 'win32') basePath = basePath.replace(/\\/g, '/');
that.book.log.debug.ln("write parsed file", page.path, "to", relativeOutput);
return that._writeTemplate(that.templates["page"], {
progress: page.progress,
_input: page.path,
content: page.sections,
basePath: basePath,
staticBase: links.join(basePath, "gitbook")
}, output);
});
};
// Write the index for langs
Generator.prototype.writeLangsIndex = function() {
var that = this;
if (!this.book.langs.length) return Q();
return this._writeTemplate(this.templates["langs"], {
langs: this.book.langs
}, path.join(this.options.output, "index.html"));
};
// Write glossary
Generator.prototype.writeGlossary = function() {
var that = this;
// No glossary
if (this.book.glossary.length == 0) return Q();
return this._writeTemplate(this.templates["glossary"], {}, path.join(this.options.output, "GLOSSARY.html"));
};
// Write the search index
Generator.prototype.writeSearchIndex = function() {
var that = this;
return fs.writeFile(
path.join(this.options.output, "search_index.json"),
JSON.stringify(this.book.searchIndex)
);
};
// Convert a page into a normalized data set
Generator.prototype.normalizePage = function(page) {
var that = this;
var _callHook = function(name) {
return that.callHook(name, page)
.then(function(_page) {
page = _page;
return page;
});
};
return Q()
.then(function() {
return _callHook("page");
})
.then(function() {
return page;
});
};
// Generate a template
Generator.prototype._writeTemplate = function(tpl, options, output, interpolate) {
var that = this;
interpolate = interpolate || _.identity;
return Q()
.then(function(sections) {
return that.env.render(
tpl,
_.extend({
gitbook: {
version: pkg.version
},
styles: that.styles,
revision: that.revision,
title: that.options.title,
description: that.options.description,
language: that.book.config.normalizeLanguage(),
glossary: that.book.glossary,
summary: that.book.summary,
allNavigation: that.book.navigation,
plugins: {
resources: that.book.plugins.resources(that.namespace)
},
pluginsConfig: JSON.stringify(that.options.pluginsConfig),
htmlSnippet: _.partial(_.partialRight(that.book.plugins.html, that, options), that.namespace),
options: that.options,
basePath: ".",
staticBase: path.join(".", "gitbook"),
}, options)
);
})
.then(interpolate)
.then(function(html) {
return fs.writeFile(
output,
html
);
});
};
// Copy assets
Generator.prototype.copyAssets = function() {
var that = this;
// Copy gitbook assets
return fs.copy(
path.join(that.options.theme, "assets"),
path.join(that.options.output, "gitbook")
)
// Copy plugins assets
.then(function() {
return Q.all(
_.map(that.book.plugins.list, function(plugin) {
var pluginAssets = path.join(that.options.output, "gitbook/plugins/", plugin.name);
return plugin.copyAssets(pluginAssets, that.namespace);
})
);
});
};
module.exports = Generator;
-210
View File
@@ -1,210 +0,0 @@
var Q = require("q");
var _ = require("lodash");
var path = require("path");
var tinylr = require('tiny-lr');
var color = require('bash-color');
var Book = require("./book");
var Plugin = require("./plugin");
var Server = require("./utils/server");
var stringUtils = require("./utils/string");
var watch = require("./utils/watch");
var logger = require("./utils/logger");
var LOG_OPTION = {
name: "log",
description: "Minimum log level to display",
values: _.chain(logger.LEVELS).keys().map(stringUtils.toLowerCase).value(),
defaults: "info"
};
var FORMAT_OPTION = {
name: "format",
description: "Format to build to",
values: ["website", "json", "ebook"],
defaults: "website"
};
module.exports = {
Book: Book,
LOG_LEVELS: logger.LEVELS,
commands: _.flatten([
{
name: "build [book] [output]",
description: "build a book",
options: [
FORMAT_OPTION,
LOG_OPTION
],
exec: function(args, kwargs) {
var input = args[0] || process.cwd();
var output = args[1] || path.join(input, "_book");
var book = new Book(input, _.extend({}, {
'config': {
'output': output
},
'logLevel': kwargs.log
}));
return book.parse()
.then(function() {
return book.generate(kwargs.format);
})
.then(function(){
console.log("");
console.log(color.green("Done, without error"));
});
}
},
_.map(["pdf", "epub", "mobi"], function(ebookType) {
return {
name: ebookType+" [book] [output]",
description: "build a book to "+ebookType,
options: [
LOG_OPTION
],
exec: function(args, kwargs) {
var input = args[0] || process.cwd();
var output = args[1];
var book = new Book(input, _.extend({}, {
'logLevel': kwargs.log
}));
return book.parse()
.then(function() {
return book.generateFile(output, {
ebookFormat: ebookType
});
})
.then(function(){
console.log("");
console.log(color.green("Done, without error"));
});
}
};
}),
{
name: "serve [book]",
description: "Build then serve a gitbook from a directory",
options: [
{
name: "port",
description: "Port for server to listen on",
defaults: 4000
},
{
name: "lrport",
description: "Port for livereload server to listen on",
defaults: 35729
},
{
name: "watch",
description: "Enable/disable file watcher",
defaults: true
},
FORMAT_OPTION,
LOG_OPTION
],
exec: function(args, kwargs) {
var input = args[0] || process.cwd();
var server = new Server();
// Init livereload server
var lrServer = tinylr({});
var lrPath = undefined;
var generate = function() {
if (server.isRunning()) console.log("Stopping server");
return server.stop()
.then(function() {
var book = new Book(input, _.extend({}, {
'config': {
'defaultsPlugins': ["livereload"]
},
'logLevel': kwargs.log
}));
return book.parse()
.then(function() {
return book.generate(kwargs.format);
})
.thenResolve(book);
})
.then(function(book) {
console.log();
console.log('Starting server ...');
return server.start(book.options.output, kwargs.port)
.then(function() {
console.log('Serving book on http://localhost:'+kwargs.port);
if (lrPath) {
// trigger livereload
lrServer.changed({
body: {
files: [lrPath]
}
});
}
if (!kwargs.watch) return;
return watch(book.root)
.then(function(filepath) {
// set livereload path
lrPath = filepath;
console.log("Restart after change in files");
console.log('');
return generate();
})
})
});
};
return Q.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
.then(function() {
console.log('Live reload server started on port:', kwargs.lrport);
console.log('Press CTRL+C to quit ...');
console.log('')
return generate();
});
}
},
{
name: "install [book]",
description: "install plugins dependencies",
exec: function(args, kwargs) {
var input = args[0] || process.cwd();
var book = new Book(input);
return book.config.load()
.then(function() {
return book.plugins.install();
})
.then(function(){
console.log("");
console.log(color.green("Done, without error"));
});
}
},
{
name: "init [directory]",
description: "create files and folders based on contents of SUMMARY.md",
exec: function(args, kwargs) {
return Book.init(args[0] || process.cwd())
.then(function(){
console.log("");
console.log(color.green("Done, without error"));
});
}
}
])
};
-183
View File
@@ -1,183 +0,0 @@
var _ = require("lodash");
var Q = require("q");
var semver = require("semver");
var path = require("path");
var url = require("url");
var fs = require("./utils/fs");
var resolve = require('resolve');
var pkg = require("../package.json");
var Plugin = function(book, name) {
this.book = book;
this.name = name;
this.packageInfos = {};
this.infos = {};
// Bind methods
_.bindAll(this);
_.each([
"gitbook-plugin-"+name,
"gitbook-"+name,
name,
], function(_name) {
// Load from the book
if (this.load(_name, book.root)) return false;
// Load from default plugins
if (this.load(_name, __dirname)) return false;
}, this);
};
// Type of plugins resources
Plugin.RESOURCES = ["js", "css"];
Plugin.HOOKS = [
"init", "finish", "finish:before"
]
// Load from a name
Plugin.prototype.load = function(name, baseDir) {
try {
var res = resolve.sync(name+"/package.json", { basedir: baseDir });
this.baseDir = path.dirname(res);
this.packageInfos = require(res);
this.infos = require(resolve.sync(name, { basedir: baseDir }));
this.name = name;
return true;
} catch (e) {
this.packageInfos = {};
this.infos = {};
return false;
}
};
Plugin.prototype.normalizeResource = function(resource) {
// Parse the resource path
var parsed = url.parse(resource);
// This is a remote resource
// so we will simply link to using it's URL
if (parsed.protocol) {
return {
"url": resource
};
}
// This will be copied over from disk
// and shipped with the book's build
return { "path": this.name+"/"+resource };
};
// Return resources
Plugin.prototype._getResources = function(base) {
base = base;
var book = this.infos[base];
// Compatibility with version 1.x.x
if (base == "website") book = book || this.infos["book"];
// Nothing specified, fallback to default
if (!book) {
return Q({});
}
// Dynamic function
if(typeof book === "function") {
// Call giving it the context of our book
return Q().then(book.bind(this.book));
}
// Plain data object
return Q(_.cloneDeep(book));
};
// Normalize resources and return them
Plugin.prototype.getResources = function(base) {
var that = this;
return this._getResources(base)
.then(function(resources) {
_.each(Plugin.RESOURCES, function(resourceType) {
resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource);
});
return resources;
});
};
// Normalize filters and return them
Plugin.prototype.getFilters = function() {
return this.infos.filters || {};
};
// Normalize blocks and return them
Plugin.prototype.getBlocks = function() {
return this.infos.blocks || {};
};
// Test if it's a valid plugin
Plugin.prototype.isValid = function() {
var that = this;
var isValid = (
this.packageInfos &&
this.packageInfos.name &&
this.packageInfos.engines &&
this.packageInfos.engines.gitbook &&
semver.satisfies(pkg.version, this.packageInfos.engines.gitbook)
);
// Valid hooks
_.each(this.infos.hooks, function(hook, hookName) {
if (_.contains(Plugin.HOOKS, hookName)) return;
that.book.log.warn.ln("Hook '"+hookName+" 'used by plugin '"+that.packageInfos.name+"' has been removed or is deprecated");
});
return isValid;
};
// Resolve file path
Plugin.prototype.resolveFile = function(filename) {
return path.resolve(this.baseDir, filename);
};
// Resolve file path
Plugin.prototype.callHook = function(name, data) {
// Our book will be the context to apply
var context = this.book;
var hookFunc = this.infos.hooks? this.infos.hooks[name] : null;
data = data || {};
if (!hookFunc) return Q(data);
this.book.log.debug.ln("call hook", name);
if (!_.contains(Plugin.HOOKS, name)) this.book.log.warn.ln("hook '"+name+"' used by plugin '"+this.name+"' is deprecated, and will be removed in the coming versions");
return Q()
.then(function() {
return hookFunc.apply(context, [data]);
});
};
// Copy plugin assets fodler
Plugin.prototype.copyAssets = function(out, base) {
var that = this;
return this.getResources(base)
.get('assets')
.then(function(assets) {
// Assets are undefined
if(!assets) return false;
return fs.copy(
that.resolveFile(assets),
out
).then(_.constant(true));
}, _.constant(false));
};
module.exports = Plugin;
-225
View File
@@ -1,225 +0,0 @@
var _ = require("lodash");
var Q = require("q");
var npmi = require('npmi');
var npm = require('npm');
var semver = require('semver');
var Plugin = require("./plugin");
var pkg = require("../package.json");
var initNPM = _.memoize(function() {
return Q.nfcall(npm.load, { silent: true, loglevel: 'silent' });
});
var PluginsList = function(book, plugins) {
this.book = book;
this.log = this.book.log;
// List of Plugin objects
this.list = [];
// List of names of failed plugins
this.failed = [];
// Namespaces
this.namespaces = _.chain(["website", "ebook"])
.map(function(namespace) {
return [
namespace,
{
html: {},
resources: _.chain(Plugin.RESOURCES)
.map(function(type) {
return [type, []];
})
.object()
.value()
}
];
})
.object()
.value();
// Bind methods
_.bindAll(this);
if (plugins) this.load(plugins);
};
// return count of plugins
PluginsList.prototype.count = function() {
return this.list.length;
};
// Add and load a plugin
PluginsList.prototype.load = function(plugin, options) {
var that = this;
if (_.isArray(plugin)) {
return _.reduce(plugin, function(prev, p) {
return prev.then(function() {
return that.load(p);
});
}, Q());
}
if (_.isObject(plugin) && !(plugin instanceof Plugin)) plugin = plugin.name;
if (_.isString(plugin)) plugin = new Plugin(this.book, plugin);
that.log.info("load plugin", plugin.name, "....");
if (!plugin.isValid()) {
that.log.info.fail();
that.failed.push(plugin.name);
return Q();
} else {
that.log.info.ok();
// Push in the list
that.list.push(plugin);
}
// Extract filters
_.each(plugin.getFilters(), function(filterFunc, filterName) {
that.book.template.addFilter(filterName, filterFunc);
});
// Extract blocks
_.each(plugin.getBlocks(), function(block, blockName) {
that.book.template.addBlock(blockName, block);
});
return _.reduce(_.keys(that.namespaces), function(prev, namespaceName) {
return prev.then(function() {
return plugin.getResources(namespaceName)
.then(function(plResources) {
var namespace = that.namespaces[namespaceName];
// Extract js and css
_.each(Plugin.RESOURCES, function(resourceType) {
namespace.resources[resourceType] = (namespace.resources[resourceType] || []).concat(plResources[resourceType] || []);
});
// Map of html resources by name added by each plugin
_.each(plResources.html || {}, function(value, tag) {
// Turn into function if not one already
if (!_.isFunction(value)) value = _.constant(value);
namespace.html[tag] = namespace.html[tag] || [];
namespace.html[tag].push(value);
});
})
});
}, Q());
};
// Call a hook
PluginsList.prototype.hook = function(name, data) {
return _.reduce(this.list, function(prev, plugin) {
return prev.then(function(ret) {
return plugin.callHook(name, ret);
});
}, Q(data));
};
// Return a template from a plugin
PluginsList.prototype.template = function(name) {
var withTpl = _.find(this.list, function(plugin) {
return (
plugin.infos.templates &&
plugin.infos.templates[name]
);
});
if (!withTpl) return null;
return withTpl.resolveFile(withTpl.infos.templates[name]);
};
// Return an html snippet
PluginsList.prototype.html = function(namespace, tag, context, options) {
var htmlSnippets = this.namespaces[namespace].html[tag];
return _.map(htmlSnippets || [], function(code) {
return code.call(context, options);
}).join("\n");
};
// Return a resources map for a namespace
PluginsList.prototype.resources = function(namespace) {
return this.namespaces[namespace].resources;
};
// Install plugins from a book
PluginsList.prototype.install = function() {
var that = this;
var defaultsPlugins = _.pluck(that.book.options.defaultsPlugins)
// Remove defaults (no need to install)
var plugins = _.filter(that.book.options.plugins, function(plugin) {
return !_.contains(defaultsPlugins, plugin.name);
});
// Install plugins one by one
that.book.log.info.ln(plugins.length+" plugins to install");
return _.reduce(plugins, function(prev, plugin) {
return prev.then(function() {
var fullname = "gitbook-plugin-"+plugin.name;
return Q()
// Resolve version if needed
.then(function() {
if (plugin.version) return plugin.version;
that.book.log.info.ln("No version specified, resolve plugin", plugin.name);
return initNPM()
.then(function() {
return Q.nfcall(npm.commands.view, [fullname+"@*", "engines"], true);
})
.then(function(versions) {
return _.chain(versions)
.pairs()
.map(function(v) {
return {
version: v[0],
gitbook: (v[1].engines || {})["gitbook"]
}
})
.filter(function(v) {
return v.gitbook && semver.satisfies(pkg.version, v.gitbook);
})
.sort(function(v1, v2) {
return semver.lt(v1.version, v2.version)? 1 : -1;
})
.pluck("version")
.first()
.value();
});
})
// Install the plugin with the resolved version
.then(function(version) {
if (!version) {
throw "Found no satisfactory version for plugin "+plugin.name;
}
that.book.log.info.ln("install plugin", plugin.name, "from npm ("+fullname+") with version", version);
return Q.nfcall(npmi, {
'name': fullname,
'version': version,
'path': that.book.root,
'npmLoad': {
'loglevel': 'silent',
'loaded': true,
'prefix': that.book.root
}
});
})
.then(function() {
that.book.log.info.ok("plugin", plugin.name, "installed with success");
});
});
}, Q());
};
module.exports = PluginsList;
-400
View File
@@ -1,400 +0,0 @@
var _ = require("lodash");
var Q = require("q");
var path = require("path");
var nunjucks = require("nunjucks");
var escapeStringRegexp = require("escape-string-regexp");
var git = require("./utils/git");
var fs = require("./utils/fs");
var batch = require("./utils/batch");
var pkg = require("../package.json");
// The loader should handle relative and git url
var BookLoader = nunjucks.Loader.extend({
async: true,
init: function(book) {
this.book = book;
},
getSource: function(fileurl, callback) {
var that = this;
git.resolveFile(fileurl)
.then(function(filepath) {
// Is local file
if (!filepath) filepath = path.resolve(that.book.root, fileurl);
else that.book.log.debug.ln("resolve from git", fileurl, "to", filepath)
// Read file from absolute path
return fs.readFile(filepath)
.then(function(source) {
return {
src: source.toString(),
path: filepath
}
});
})
.nodeify(callback);
},
resolve: function(from, to) {
return path.resolve(path.dirname(from), to);
}
});
var TemplateEngine = function(book) {
this.book = book;
this.log = this.book.log;
// Nunjucks env
this.env = new nunjucks.Environment(
new BookLoader(book),
{
// Escaping is done after by the markdown parser
autoescape: false,
// Tags
tags: {
blockStart: '{%',
blockEnd: '%}',
variableStart: '{{',
variableEnd: '}}',
commentStart: '{###',
commentEnd: '###}'
}
}
);
// List of tags shortcuts
this.shortcuts = [];
// Map of blocks
this.blocks = {};
// Bind methods
_.bindAll(this);
// Default block "html" that return html not parsed
this.addBlock("html", {
process: _.identity
});
};
// Process a block in a context
TemplateEngine.prototype.processBlock = function(blk) {
if (_.isString(blk)) blk = { body: blk };
blk = _.defaults(blk, {
parse: false,
post: undefined
});
blk.id = _.uniqueId("blk");
var toAdd = (!blk.parse) || (blk.post != undefined);
// Add to global map
if (toAdd) this.blocks[blk.id] = blk;
//Parsable block, just return it
if (blk.parse) {
return blk.body;
}
// Return it as a macro
return "%+%"+blk.id+"%+%";
};
// Replace blocks by body after processing
// This is done to avoid that markdown processer parse the block content
TemplateEngine.prototype.replaceBlocks = function(content) {
var that = this;
return content.replace(/\%\+\%([\s\S]+?)\%\+\%/g, function(match, key) {
var blk = that.blocks[key];
if (!blk) return match;
var body = blk.body;
return body;
});
};
// Bind a function to a context
TemplateEngine.prototype.bindContext = function(func) {
var that = this;
return function() {
var ctx = {
ctx: this.ctx,
book: that.book,
generator: that.book.options.generator
};
return func.apply(ctx, arguments);
};
};
// Add filter
TemplateEngine.prototype.addFilter = function(filterName, func) {
try {
this.env.getFilter(filterName);
this.log.warn.ln("conflict in filters, '"+filterName+"' is already set");
return false;
} catch(e) {}
this.log.debug.ln("add filter '"+filterName+"'");
this.env.addFilter(filterName, this.bindContext(function() {
var ctx = this;
var args = Array.prototype.slice.apply(arguments);
var callback = _.last(args);
Q()
.then(function() {
return func.apply(ctx, args.slice(0, -1));
})
.nodeify(callback);
}), true);
return true;
};
// Add a block
TemplateEngine.prototype.addBlock = function(name, block) {
var that = this;
block = _.defaults(block || {}, {
shortcuts: [],
end: "end"+name,
process: _.identity,
blocks: []
});
var extName = 'Block'+name+'Extension';
if (this.env.getExtension(extName)) {
this.log.warn.ln("conflict in blocks, '"+name+"' is already defined");
return false;
}
this.log.debug.ln("add block '"+name+"'");
var Ext = function () {
this.tags = [name];
this.parse = function(parser, nodes, lexer) {
var body = null;
var lastBlockName = null;
var lastBlockArgs = null;
var allBlocks = block.blocks.concat([block.end]);
var subbodies = {};
var tok = parser.nextToken();
var args = parser.parseSignature(null, true);
parser.advanceAfterBlockEnd(tok.value);
while (1) {
// Read body
var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks);
// Handle body with previous block name and args
if (lastBlockName) {
subbodies[lastBlockName] = subbodies[lastBlockName] || [];
subbodies[lastBlockName].push({
body: currentBody,
args: lastBlockArgs
});
} else {
body = currentBody;
}
// Read new block
lastBlockName = parser.peekToken().value;
if (lastBlockName == block.end) {
break;
}
// Parse signature and move to the end of the block
lastBlockArgs = parser.parseSignature(null, true);
parser.advanceAfterBlockEnd(lastBlockName);
}
parser.advanceAfterBlockEnd();
var bodies = [body];
_.each(block.blocks, function(blockName) {
subbodies[blockName] = subbodies[blockName] || [];
if (subbodies[blockName].length == 0) {
subbodies[blockName].push({
args: new nodes.NodeList(),
body: new nodes.NodeList()
});
}
bodies.push(subbodies[blockName][0].body);
});
return new nodes.CallExtensionAsync(this, 'run', args, bodies);
};
this.run = function(context) {
var args = Array.prototype.slice.call(arguments, 1);
var callback = args.pop();
// Extract blocks
var blocks = args
.concat([])
.slice(-block.blocks.length);
// Eliminate blocks from list
if (block.blocks.length > 0) args = args.slice(0, -block.blocks.length);
// Extract main body and kwargs
var body = args.pop();
var kwargs = args.pop() || {};
// Extract blocks body
var _blocks = _.map(block.blocks, function(blockName, i){
return {
name: blockName,
body: blocks[i]()
};
});
var func = that.bindContext(block.process);
Q()
.then(function() {
return func.call(context, {
body: body(),
args: args,
kwargs: kwargs,
blocks: _blocks
});
})
// process the block returned
.then(that.processBlock)
.nodeify(callback)
};
};
// Add the Extension
this.env.addExtension(extName, new Ext());
// Add shortcuts
if (!_.isArray(block.shortcuts)) block.shortcuts = [block.shortcuts];
_.each(block.shortcuts, function(shortcut) {
this.log.debug.ln("add template shortcut from '"+shortcut.start+"' to block '"+name+"' for parsers ", shortcut.parsers);
this.shortcuts.push({
parsers: shortcut.parsers,
start: shortcut.start,
end: shortcut.end,
tag: {
start: name,
end: block.end
}
});
}, this);
};
// Apply a shortcut to a string
TemplateEngine.prototype._applyShortcut = function(parser, content, shortcut) {
if (!_.contains(shortcut.parsers, parser)) return content;
var regex = new RegExp(
escapeStringRegexp(shortcut.start) + "([\\s\\S]*?[^\\$])" + escapeStringRegexp(shortcut.end),
'g'
);
return content.replace(regex, function(all, match) {
return "{% "+shortcut.tag.start+" %}"+ match + "{% "+shortcut.tag.end+" %}";
});
};
// Render a string from the book
TemplateEngine.prototype.renderString = function(content, context, options) {
var context = _.extend({}, context, {
// Variables from book.json
book: this.book.options.variables,
// infos about gitbook
gitbook: {
version: pkg.version,
generator: this.book.options.generator
}
});
options = _.defaults(options || {}, {
path: null,
type: null
});
if (options.path) options.path = this.book.resolve(options.path);
// Replace shortcuts
content = _.reduce(this.shortcuts, _.partial(this._applyShortcut.bind(this), options.type), content);
return Q.nfcall(this.env.renderString.bind(this.env), content, context, options)
.fail(function(err) {
if (_.isString(err)) err = new Error(err);
err.message = err.message.replace(/^Error: /, "");
throw err;
});
};
// Render a file from the book
TemplateEngine.prototype.renderFile = function(filename, options) {
var that = this, context;
return that.book.readFile(filename)
.then(function(content) {
return that.renderString(content, {}, {
path: filename
});
});
};
// Render a page from the book
TemplateEngine.prototype.renderPage = function(page) {
var that = this, context;
return that.book.statFile(page.path)
.then(function(stat) {
context = {
// infos about the file
file: {
path: page.path,
mtime: stat.mtime
}
};
return that.renderString(page.content, context, {
path: page.path,
type: page.type
});
});
};
// Post process content
TemplateEngine.prototype.postProcess = function(content) {
var that = this;
return Q(content)
.then(that.replaceBlocks)
.then(function(_content) {
return batch.execEach(that.blocks, {
max: 20,
fn: function(blk, blkId) {
return Q()
.then(function() {
if (!blk.post) return Q();
return blk.post();
})
.then(function() {
delete that.blocks[blkId];
});
}
})
.thenResolve(_content);
});
};
module.exports = TemplateEngine;
-52
View File
@@ -1,52 +0,0 @@
var Q = require("q");
var _ = require("lodash");
// Execute a method for all element
function execEach(items, options) {
if (_.size(items) == 0) return Q();
var concurrents = 0, d = Q.defer(), pending = [];
options = _.defaults(options || {}, {
max: 100,
fn: function(item) {}
});
function startItem(item, i) {
if (concurrents >= options.max) {
pending.push([item, i]);
return;
}
concurrents++;
Q()
.then(function() {
return options.fn(item, i);
})
.then(function() {
concurrents--;
// Next pending
var next = pending.shift();
if (concurrents == 0 && !next) {
d.resolve();
} else if (next) {
startItem.apply(null, next);
}
})
.fail(function(err) {
pending = [];
d.reject(err);
})
}
_.each(items, startItem);
return d.promise;
}
module.exports = {
execEach: execEach
};
-36
View File
@@ -1,36 +0,0 @@
var hljs = require('highlight.js');
var MAP = {
'py': 'python',
'js': 'javascript',
'json': 'javascript',
'rb': 'ruby',
'csharp': 'cs',
};
function normalize(lang) {
if(!lang) { return null; }
var lower = lang.toLowerCase();
return MAP[lower] || lower;
}
function highlight(lang, code) {
if(!lang) return code;
// Normalize lang
lang = normalize(lang);
try {
return hljs.highlight(lang, code).value;
} catch(e) { }
return code;
}
// Exports
module.exports = {
highlight: highlight,
normalize: normalize,
MAP: MAP
};
-178
View File
@@ -1,178 +0,0 @@
var _ = require("lodash");
var Q = require("q");
var tmp = require("tmp");
var path = require("path");
var fs = require("graceful-fs");
var fsExtra = require("fs-extra");
var Ignore = require("fstream-ignore");
var fsUtils = {
tmp: {
file: function(opt) {
return Q.nfcall(tmp.file.bind(tmp), opt).get(0)
},
dir: function() {
return Q.nfcall(tmp.dir.bind(tmp)).get(0)
}
},
list: listFiles,
stat: Q.denodeify(fs.stat),
readdir: Q.denodeify(fs.readdir),
readFile: Q.denodeify(fs.readFile),
writeFile: writeFile,
writeStream: writeStream,
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;
},
existsSync: fs.existsSync.bind(fs),
readFileSync: fs.readFileSync.bind(fs),
clean: cleanFolder,
getUniqueFilename: getUniqueFilename,
}
// Write a file
function writeFile(filename, data, options) {
var d = Q.defer();
try {
fs.writeFileSync(filename, data, options)
} catch(err) {
d.reject(err);
}
d.resolve();
return d.promise;
}
// Write a stream to a file
function writeStream(filename, st) {
var d = Q.defer();
var wstream = fs.createWriteStream(filename);
wstream.on('finish', function () {
d.resolve();
});
wstream.on('error', function (err) {
d.reject(err);
});
st.pipe(wstream);
return d.promise;
}
// Find a filename available
function getUniqueFilename(base, filename) {
if (!filename) {
filename = base;
base = "/";
}
filename = path.resolve(base, filename);
var ext = path.extname(filename);
filename = path.join(path.dirname(filename), path.basename(filename, ext));
var _filename = filename+ext;
var i = 0;
while (1) {
if (!fs.existsSync(filename)) break;
_filename = filename+"_"+i+ext;
i = i + 1;
}
return path.relative(base, _filename);
}
// List files in a directory
function listFiles(root, options) {
options = _.defaults(options || {}, {
ignoreFiles: [],
ignoreRules: []
});
var d = Q.defer();
// Our list of files
var files = [];
var ig = Ignore({
path: root,
ignoreFiles: options.ignoreFiles
});
// Add extra rules to ignore common folders
ig.addIgnoreRules(options.ignoreRules, '__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;
}
// Clean a folder without removing .git and .svn
// Creates it if non existant
function cleanFolder(root) {
if (!fs.existsSync(root)) return fsUtils.mkdirp(root);
return listFiles(root, {
ignoreFiles: [],
ignoreRules: [
// Skip Git and SVN stuff
'.git/',
'.svn/'
]
})
.then(function(files) {
var d = Q.defer();
_.reduce(files, function(prev, file, i) {
return prev.then(function() {
var _file = path.join(root, file);
d.notify({
i: i+1,
count: files.length,
file: _file
});
return fsUtils.remove(_file);
});
}, Q())
.then(function() {
d.resolve();
}, function(err) {
d.reject(err);
});
return d.promise;
});
}
module.exports = fsUtils;
-112
View File
@@ -1,112 +0,0 @@
var Q = require("q");
var _ = require("lodash");
var url = require("url");
var tmp = require("tmp");
var path = require("path");
var crc = require("crc");
var exec = Q.denodeify(require("child_process").exec);
var URI = require("URIjs");
var fs = require("./fs");
var GIT_PREFIX = "git+";
var GIT_TMP = null;
// Check if an url is a git dependency url
function checkGitUrl(giturl) {
return (giturl.indexOf(GIT_PREFIX) === 0);
}
// Validates a SHA in hexadecimal
function validateSha(str) {
return (/[0-9a-f]{40}/).test(str);
}
// Parse and extract infos
function parseGitUrl(giturl) {
var ref, uri, fileParts, filepath;
if (!checkGitUrl(giturl)) return null;
giturl = giturl.slice(GIT_PREFIX.length);
uri = new URI(giturl);
ref = uri.fragment() || "master";
uri.fragment(null);
// Extract file inside the repo (after the .git)
fileParts =uri.path().split(".git");
filepath = fileParts.length > 1? fileParts.slice(1).join(".git") : "";
if (filepath[0] == "/") filepath = filepath.slice(1);
// Recreate pathname without the real filename
uri.path(_.first(fileParts)+".git");
return {
host: uri.toString(),
ref: ref || "master",
filepath: filepath
};
}
// Clone a git repo from a specific ref
function cloneGitRepo(host, ref) {
var isBranch = false;
ref = ref || "master";
if (!validateSha(ref)) isBranch = true;
return Q()
// Create temporary folder to store git repos
.then(function() {
if (GIT_TMP) return;
return fs.tmp.dir()
.then(function(_tmp) {
GIT_TMP = _tmp;
});
})
// Return or clone the git repo
.then(function() {
// Unique ID for repo/ref combinaison
var repoId = crc.crc32(host+"#"+ref).toString(16);
// Absolute path to the folder
var repoPath = path.resolve(GIT_TMP, repoId);
return fs.exists(repoPath)
.then(function(doExists) {
if (doExists) return;
// Clone repo
return exec("git clone "+host+" "+repoPath)
.then(function() {
return exec("git checkout "+ref, { cwd: repoPath });
})
})
.thenResolve(repoPath);
});
}
// Get file from a git repo
function resolveFileFromGit(giturl) {
if (_.isString(giturl)) giturl = parseGitUrl(giturl);
if (!giturl) return Q(null);
// Clone or get from cache
return cloneGitRepo(giturl.host, giturl.ref)
.then(function(repo) {
// Resolve relative path
return path.resolve(repo, giturl.filepath);
});
};
module.exports = {
checkUrl: checkGitUrl,
parseUrl: parseGitUrl,
resolveFile: resolveFileFromGit
};
-80
View File
@@ -1,80 +0,0 @@
var _ = require("lodash");
var path = require("path");
var fs = require("fs");
var i18n = require("i18n");
var I18N_PATH = path.resolve(__dirname, "../../theme/i18n/")
var DEFAULT_LANGUAGE = "en";
var LOCALES = _.map(fs.readdirSync(I18N_PATH), function(lang) {
return path.basename(lang, ".json");
});
i18n.configure({
locales: LOCALES,
directory: I18N_PATH,
defaultLocale: DEFAULT_LANGUAGE,
updateFiles: false
});
var compareLocales = function(lang, locale) {
var langMain = _.first(lang.split("-"));
var langSecond = _.last(lang.split("-"));
var localeMain = _.first(locale.split("-"));
var localeSecond = _.last(locale.split("-"));
if (locale == lang) return 100;
if (localeMain == langMain) return 50;
if (localeSecond == langSecond) return 20;
return 0;
};
var normalizeLanguage = _.memoize(function(lang) {
var language = _.chain(LOCALES)
.values()
.map(function(locale) {
return {
locale: locale,
score: compareLocales(lang, locale)
}
})
.filter(function(lang) {
return lang.score > 0;
})
.sortBy("score")
.pluck("locale")
.last()
.value();
return language || lang;
});
var translate = function(locale, phrase) {
var args = Array.prototype.slice.call(arguments, 2);
return i18n.__.apply({}, [{
locale: locale,
phrase: phrase
}].concat(args));
};
var getCatalog = function(locale) {
locale = normalizeLanguage(locale);
return i18n.getCatalog(locale);
};
var getLocales = function() {
return LOCALES;
};
var hasLocale = function(locale) {
return _.contains(LOCALES, locale);
}
module.exports = {
__: translate,
normalizeLanguage: normalizeLanguage,
getCatalog: getCatalog,
getLocales: getLocales,
hasLocale: hasLocale
};
-39
View File
@@ -1,39 +0,0 @@
var _ = require("lodash");
var Q = require("q");
var fs = require("./fs");
var spawn = require('spawn-cmd').spawn;
var links = require("./links");
// Convert a svg file
var convertSVG = function(source, dest, options) {
if (!fs.existsSync(source)) return Q.reject(new Error("File doesn't exist: "+source));
var d = Q.defer();
options = _.defaults(options || {}, {
});
//var command = shellescape(['svgexport', source, dest]);
var child = spawn('svgexport', [source, dest]);
child.on("error", function(error) {
if (error.code == "ENOENT") error = new Error("Need to install 'svgexport' using 'npm install svgexport -g'");
return d.reject(error);
});
child.on("close", function(code) {
if (code == 0 && fs.existsSync(dest)) {
d.resolve();
} else {
d.reject(new Error("Error converting "+source+" into "+dest));
}
});
return d.promise;
};
module.exports = {
convertSVG: convertSVG,
INVALID: [".svg"]
};
-82
View File
@@ -1,82 +0,0 @@
var url = require('url');
var path = require('path');
// Is the link an external link
var isExternal = function(href) {
try {
return Boolean(url.parse(href).protocol);
} catch(err) { }
return false;
};
// Return true if the link is relative
var isRelative = function(href) {
try {
var parsed = url.parse(href);
return !!(!parsed.protocol && parsed.path);
} catch(err) {}
return true;
};
// Return true if the link is an achor
var isAnchor = function(href) {
try {
var parsed = url.parse(href);
return !!(!parsed.protocol && !parsed.path && parsed.hash);
} catch(err) {}
return false;
};
// Normalize a path to be a link
var normalizeLink = function(s) {
return s.replace(/\\/g, '/');
};
// Relative to absolute path
// dir: directory parent of the file currently in rendering process
// outdir: directory parent from the html output
var toAbsolute = function(_href, dir, outdir) {
if (isExternal(_href)) return _href;
// Path '_href' inside the base folder
var hrefInRoot = path.normalize(path.join(dir, _href));
if (_href[0] == "/") hrefInRoot = path.normalize(_href.slice(1));
// Make it relative to output
_href = path.relative(outdir, hrefInRoot);
// Normalize windows paths
_href = normalizeLink(_href);
return _href;
};
// Join links
var join = function() {
var _href = path.join.apply(path, arguments);
return normalizeLink(_href);
};
// Change extension
var changeExtension = function(filename, newext) {
return path.join(
path.dirname(filename),
path.basename(filename, path.extname(filename))+newext
);
};
module.exports = {
isAnchor: isAnchor,
isRelative: isRelative,
isExternal: isExternal,
toAbsolute: toAbsolute,
join: join,
changeExtension: changeExtension,
normalize: normalizeLink
};
-102
View File
@@ -1,102 +0,0 @@
var _ = require('lodash');
var util = require('util');
var color = require('bash-color');
var LEVELS = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
DISABLED: 10
};
var COLORS = {
DEBUG: color.purple,
INFO: color.cyan,
WARN: color.yellow,
ERROR: color.red
};
module.exports = function(_write, logLevel) {
var logger = {};
var lastChar = '\n';
if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
// Write a simple message
logger.write = function(msg) {
msg = msg.toString();
lastChar = _.last(msg);
return _write(msg);
};
// Format a message
logger.format = function() {
return util.format.apply(util, arguments);
};
// Write a line
logger.writeLn = function(msg) {
return this.write((msg || "")+"\n");
};
// Write a message with a certain level
logger.log = function(level) {
if (level < logLevel) return;
var levelKey = _.findKey(LEVELS, function(v) { return v == level; });
var args = Array.prototype.slice.apply(arguments, [1]);
var msg = logger.format.apply(logger, args);
if (lastChar == '\n') {
msg = COLORS[levelKey](levelKey.toLowerCase()+":")+" "+msg;
}
return logger.write(msg);
};
logger.logLn = function() {
if (lastChar != '\n') logger.write("\n");
var args = Array.prototype.slice.apply(arguments);
args.push("\n");
logger.log.apply(logger, args);
};
// Write a OK
logger.ok = function(level) {
var args = Array.prototype.slice.apply(arguments, [1]);
var msg = logger.format.apply(logger, args);
if (arguments.length > 1) {
logger.logLn(level, color.green('>> ') + msg.trim().replace(/\n/g, color.green('\n>> ')));
} else {
logger.log(level, color.green("OK"), "\n");
}
};
// Write an "FAIL"
logger.fail = function(level) {
return logger.log(level, color.red("ERROR")+"\n");
};
_.each(_.omit(LEVELS, 'DISABLED'), function(level, levelKey) {
levelKey = levelKey.toLowerCase();
logger[levelKey] = _.partial(logger.log, level);
logger[levelKey].ln = _.partial(logger.logLn, level);
logger[levelKey].ok = _.partial(logger.ok, level);
logger[levelKey].fail = _.partial(logger.fail, level);
logger[levelKey].promise = function(p) {
return p.
then(function(st) {
logger[levelKey].ok();
return st;
}, function(err) {
logger[levelKey].fail();
throw err;
});
}
});
return logger;
};
module.exports.LEVELS = LEVELS;
module.exports.COLORS = COLORS;
-82
View File
@@ -1,82 +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(flattenChapters(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);
// List of all navNodes
// Flatten chapters, then add in default README node if needed etc ...
var navNodes = flattenChapters(summary.chapters);
// Mapping of prev/next for a give path
var mapping = _.chain(navNodes)
.map(function(current, i) {
var prev = null, next = null;
// Skip if no path
if(!current.exists) return null;
// Find prev
prev = _.chain(navNodes)
.slice(0, i)
.reverse()
.find(function(node) {
return node.exists && !node.external;
})
.value();
// Find next
next = _.chain(navNodes)
.slice(i+1)
.find(function(node) {
return node.exists && !node.external;
})
.value();
return [current.path, {
index: i,
title: current.title,
introduction: current.introduction,
prev: prev,
next: next,
level: current.level,
}];
})
.compact()
.object()
.value();
// Filter for only files we want
if(files) {
return _.pick(mapping, files);
}
return mapping;
}
// Exports
module.exports = navigation;
-372
View File
@@ -1,372 +0,0 @@
var Q = require('q');
var _ = require('lodash');
var url = require('url');
var path = require('path');
var cheerio = require('cheerio');
var domSerializer = require('dom-serializer');
var request = require('request');
var crc = require("crc");
var links = require('./links');
var imgUtils = require('./images');
var fs = require('./fs');
var batch = require('./batch');
var code = require('./code');
// Render a cheerio dom as html
var renderDom = function($, dom, options) {
if (!dom && $._root && $._root.children) {
dom = $._root.children;
}
options = options|| dom.options || $._options;
return domSerializer(dom, options);
};
// Map of images that have been converted
var imgConversionCache = {};
function replaceText($, el, search, replace, text_only ) {
return $(el).each(function(){
var node = this.firstChild,
val,
new_val,
// Elements to be removed at the end.
remove = [];
// Only continue if firstChild exists.
if ( node ) {
// Loop over all childNodes.
do {
// Only process text nodes.
if ( node.nodeType === 3 ) {
// The original node value.
val = node.nodeValue;
// The new value.
new_val = val.replace( search, replace );
// Only replace text if the new value is actually different!
if ( new_val !== val ) {
if ( !text_only && /</.test( new_val ) ) {
// The new value contains HTML, set it in a slower but far more
// robust way.
$(node).before( new_val );
// Don't remove the node yet, or the loop will lose its place.
remove.push( node );
} else {
// The new value contains no HTML, so it can be set in this
// very fast, simple way.
node.nodeValue = new_val;
}
}
}
} while ( node = node.nextSibling );
}
// Time to remove those elements!
remove.length && $(remove).remove();
});
};
function pregQuote( str ) {
return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1");
};
// Adapt an html snippet to be relative to a base folder
function normalizeHtml(src, options) {
var $ = cheerio.load(src, {
// We should parse html without trying to normalize too much
xmlMode: false,
// SVG need some attributes to use uppercases
lowerCaseAttributeNames: false,
lowerCaseTags: false
});
var toConvert = [];
var svgContent = {};
var outputRoot = options.book.options.output;
imgConversionCache[outputRoot] = imgConversionCache[outputRoot] || {};
// Find svg images to extract and process
if (options.convertImages) {
$("svg").each(function() {
var content = renderDom($, $(this));
var svgId = _.uniqueId("svg");
var dest = svgId+".svg";
// Generate filename
dest = "/"+fs.getUniqueFilename(outputRoot, dest);
svgContent[dest] = '<?xml version="1.0" encoding="UTF-8"?>'+content;
$(this).replaceWith($("<img>").attr("src", dest));
});
}
// Find images to normalize
$("img").each(function() {
var origin = undefined;
var src = $(this).attr("src");
if (!src) return;
var isExternal = links.isExternal(src);
// Transform as relative to the bases
if (links.isRelative(src)) {
src = links.toAbsolute(src, options.base, options.output);
}
// Convert if needed
if (options.convertImages) {
// If image is external and ebook, then downlaod the images
if (isExternal) {
origin = src;
src = "/"+crc.crc32(origin).toString(16)+path.extname(origin);
src = links.toAbsolute(src, options.base, options.output);
isExternal = false;
}
var ext = path.extname(src);
var srcAbs = links.join("/", options.base, src);
// Test image extension
if (_.contains(imgUtils.INVALID, ext)) {
if (imgConversionCache[outputRoot][srcAbs]) {
// Already converted
src = imgConversionCache[outputRoot][srcAbs];
} else {
// Not converted yet
var dest = "";
// Replace extension
dest = links.join(path.dirname(srcAbs), path.basename(srcAbs, ext)+".png");
dest = dest[0] == "/"? dest.slice(1) : dest;
// Get a name that doesn't exists
dest = fs.getUniqueFilename(outputRoot, dest);
options.book.log.debug.ln("detect invalid image (will be converted to png):", srcAbs);
// Add to cache
imgConversionCache[outputRoot][srcAbs] = "/"+dest;
// Push to convert
toConvert.push({
origin: origin,
content: svgContent[srcAbs],
source: isExternal? srcAbs : path.join("./", srcAbs),
dest: path.join("./", dest)
});
src = links.join("/", dest);
}
// Reset as relative to output
src = links.toAbsolute(src, options.base, options.output);
}
else if (origin) {
// Need to downlaod image
toConvert.push({
origin: origin,
source: path.join("./", srcAbs)
});
}
}
$(this).attr("src", src);
});
// Normalize links
$("a").each(function() {
var href = $(this).attr("href");
if (!href) return;
if (links.isAnchor(href)) {
// Keep it as it is
} else if (links.isRelative(href)) {
var parts = url.parse(href);
var absolutePath = links.join(options.base, parts.pathname);
var anchor = parts.hash || "";
// If is in navigation relative: transform as content
if (options.navigation[absolutePath]) {
absolutePath = options.book.contentLink(absolutePath);
}
// Transform as absolute
href = links.toAbsolute("/"+absolutePath, options.base, options.output)+anchor;
} else {
// External links
$(this).attr("target", "_blank");
}
// Transform extension
$(this).attr("href", href);
});
// Highlight code blocks
$("code").each(function() {
// Extract language
var lang = _.chain(
($(this).attr("class") || "").split(" ")
)
.map(function(cl) {
if (cl.search("lang-") === 0) return cl.slice("lang-".length);
return null;
})
.compact()
.first()
.value();
if (lang) {
var html = code.highlight(
lang,
$(this).text()
);
$(this).html(html);
}
});
// Replace glossary terms
_.each(options.glossary, function(term) {
var r = new RegExp( "\\b(" + pregQuote(term.name.toLowerCase()) + ")\\b" , 'gi' );
var includedInFiles = false;
$("*").each(function() {
// Ignore codeblocks
if (_.contains(["code", "pre"], this.name.toLowerCase())) return;
replaceText($, this, r, function(match) {
// Add to files index in glossary
if (!includedInFiles) {
includedInFiles = true;
term.files = term.files || [];
term.files.push(options.navigation[options.input]);
}
return "<a href='"+links.toAbsolute("/GLOSSARY.html", options.base, options.output)+"#"+term.id+"' class='glossary-term' title='"+_.escape(term.description)+"'>"+match+"</a>";
});
});
});
return {
html: renderDom($),
images: toConvert
};
};
// Convert svg images to png
function convertImages(images, options) {
if (!options.convertImages) return Q();
var downloaded = [];
options.book.log.debug.ln("convert ", images.length, "images to png");
return batch.execEach(images, {
max: 100,
fn: function(image) {
var imgin = path.resolve(options.book.options.output, image.source);
return Q()
// Write image if need to be download
.then(function() {
if (!image.origin && !_.contains(downloaded, image.origin)) return;
options.book.log.debug("download image", image.origin, "...");
downloaded.push(image.origin);
return options.book.log.debug.promise(fs.writeStream(imgin, request(image.origin)));
})
// Write svg if content
.then(function() {
if (!image.content) return;
return fs.writeFile(imgin, image.content);
})
// Convert
.then(function() {
if (!image.dest) return;
var imgout = path.resolve(options.book.options.output, image.dest);
options.book.log.debug("convert image", image.source, "to", image.dest, "...");
return options.book.log.debug.promise(imgUtils.convertSVG(imgin, imgout));
});
}
})
.then(function() {
options.book.log.debug.ok(images.length+" images converted with success");
});
};
// Adapt page content to be relative to a base folder
function normalizePage(sections, options) {
options = _.defaults(options || {}, {
// Current book
book: null,
// Do we need to convert svg?
convertImages: false,
// Current file path
input: ".",
// Navigation to use to transform path
navigation: {},
// Directory parent of the file currently in rendering process
base: "./",
// Directory parent from the html output
output: "./",
// Glossary terms
glossary: []
});
// List of images to convert
var toConvert = [];
sections = _.map(sections, function(section) {
if (section.type != "normal") return section;
var out = normalizeHtml(section.content, options);;
toConvert = toConvert.concat(out.images);
section.content = out.html;
return section;
});
return Q()
.then(function() {
toConvert = _.uniq(toConvert, 'source');
return convertImages(toConvert, options);
})
.thenResolve(sections);
};
// Extract text from sections
function extractText(sections) {
return _.reduce(sections, function(prev, section) {
if (section.type != "normal") return prev;
var $ = cheerio.load(section.content);
$("*").each(function() {
prev = prev+" "+$(this).text();
});
return prev;
}, "");
};
module.exports = {
normalize: normalizePage,
extractText: extractText
};
-55
View File
@@ -1,55 +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)
// Transform as array
.map(function(nav, path) {
nav.path = path;
return nav;
})
// Sort entries
.sortBy(function(nav) {
return nav.index;
})
.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;
-96
View File
@@ -1,96 +0,0 @@
var Q = require('q');
var _ = require('lodash');
var events = require('events');
var http = require('http');
var send = require('send');
var util = require('util');
var url = require('url');
var Server = function() {
this.running = null;
this.dir = null;
this.port = 0;
this.sockets = [];
};
util.inherits(Server, events.EventEmitter);
// Return true if the server is running
Server.prototype.isRunning = function() {
return this.running != null;
};
// Stop the server
Server.prototype.stop = function() {
var that = this;
if (!this.isRunning()) return Q();
var d = Q.defer();
this.running.close(function(err) {
that.running = null;
that.emit("state", false);
if (err) d.reject(err);
else d.resolve();
});
for (var i = 0; i < this.sockets.length; i++) {
this.sockets[i].destroy();
}
return d.promise;
};
Server.prototype.start = function(dir, port) {
var that = this, pre = Q();
port = port || 8004;
if (that.isRunning()) pre = this.stop();
return pre
.then(function() {
var d = Q.defer();
that.running = 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);
});
that.running.on('connection', function (socket) {
that.sockets.push(socket);
socket.setTimeout(4000);
socket.on('close', function () {
that.sockets.splice(that.sockets.indexOf(socket), 1);
});
});
that.running.listen(port, function(err) {
if (err) return d.reject(err);
that.port = port;
that.dir = dir;
that.emit("state", true);
d.resolve();
});
return d.promise;
});
}
module.exports = Server;
-27
View File
@@ -1,27 +0,0 @@
var _ = require("lodash");
function escapeShellArg(arg) {
var ret = '';
ret = arg.replace(/"/g, '\\"');
return "\"" + ret + "\"";
}
function optionsToShellArgs(options) {
return _.chain(options)
.map(function(value, key) {
if (value == null || value === false) return null;
if (value === true) return key;
return key+"="+escapeShellArg(value);
})
.compact()
.value()
.join(" ");
}
module.exports = {
escapeShellArg: escapeShellArg,
optionsToShellArgs: optionsToShellArgs,
toLowerCase: String.prototype.toLowerCase.call.bind(String.prototype.toLowerCase)
};
-39
View File
@@ -1,39 +0,0 @@
var Q = require('q');
var _ = require('lodash');
var path = require('path');
var chokidar = require('chokidar');
var parsers = require('gitbook-parsers')
function watch(dir) {
var d = Q.defer();
dir = path.resolve(dir);
var toWatch = [
"book.json", "book.js"
];
_.each(parsers.extensions, function(ext) {
toWatch.push("**/*"+ext);
});
var watcher = chokidar.watch(toWatch, {
cwd: dir,
ignoreInitial: true
});
watcher.once("all", function(e, filepath) {
watcher.close();
d.resolve(filepath);
});
watcher.once("error", function(err) {
watcher.close();
d.reject(err);
});
return d.promise;
}
module.exports = watch;
+23 -71
View File
@@ -1,78 +1,30 @@
{
"name": "gitbook",
"version": "2.0.3",
"homepage": "https://www.gitbook.com",
"description": "Library and cmd utility to generate GitBooks",
"main": "lib/index.js",
"dependencies": {
"q": "1.0.1",
"lunr": "0.5.7",
"lodash": "3.5.0",
"graceful-fs": "3.0.5",
"resolve": "0.6.3",
"fs-extra": "0.16.5",
"fstream-ignore": "1.0.2",
"gitbook-parsers": "0.7.5",
"nunjucks": "1.3.0",
"nunjucks-autoescape": "0.1.1",
"nunjucks-filter": "0.1.0",
"i18n": "0.5.0",
"semver": "2.2.1",
"npmi": "0.1.1",
"cheerio": "0.19.0",
"gitbook-plugin-livereload": "0.0.1",
"chokidar": "~1.0.1",
"send": "0.2.0",
"tiny-lr": "0.1.5",
"tmp": "0.0.24",
"crc": "3.2.1",
"bash-color": "0.0.3",
"URIjs": "1.14.1",
"request": "2.51.0",
"npm": "2.4.1",
"dom-serializer": "0.1.0",
"spawn-cmd": "0.0.2",
"highlight.js": "8.4.0",
"escape-string-regexp": "1.0.3"
},
"devDependencies": {
"mocha": "2.2.1",
"should": "5.2.0",
"grunt": "~0.4.2",
"grunt-cli": "0.1.11",
"grunt-contrib-copy": "0.5.0",
"grunt-contrib-less": "~0.5.0",
"grunt-contrib-requirejs": "0.4.1",
"grunt-bower-install-simple": "0.9.2"
},
"version": "0.1.0",
"private": true,
"packageManager": "bun@1.1.18",
"scripts": {
"test": "node_modules/.bin/mocha --reporter spec --timeout 15000"
"dev": "turbo run dev",
"build": "turbo run build",
"build:cloudflare": "turbo run build:cloudflare",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"format": "prettier ./ --ignore-unknown --write",
"format:check": "prettier ./ --ignore-unknown --list-different",
"unit": "turbo run unit",
"e2e": "turbo run e2e",
"changeset": "changeset",
"release": "turbo run build && changeset publish"
},
"repository": {
"type": "git",
"url": "https://github.com/GitbookIO/gitbook.git"
},
"bin": {
"gitbook": "./bin/gitbook.js"
},
"keywords": [
"git",
"book",
"gitbook"
"workspaces": [
"packages/*"
],
"author": "FriendCode Inc. <contact@gitbook.com>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/GitbookIO/gitbook/issues"
"devDependencies": {
"@changesets/cli": "^2.27.7",
"prettier": "^3.0.3",
"turbo": "^2.0.9"
},
"contributors": [
{
"name": "Aaron O'Mullan",
"email": "aaron@gitbook.com"
},
{
"name": "Samy Pessé",
"email": "samy@gitbook.com"
}
]
"patchedDependencies": {
"@vercel/next@4.0.15": "patches/@vercel%2Fnext@4.0.15.patch"
}
}
+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"
}
}
+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"]
}
}
+35
View File
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# 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/
+47
View File
@@ -0,0 +1,47 @@
# gitbook
## 0.3.0
### Minor Changes
- 24b785c: Update shiki for code block syntax highlighting, with support for more languages and fixes for diffs. It also patches the deployment on Cloudflare to support edge functions larger than 4MB.
### Patch Changes
- acc3f2f: Fix error with the "Try it" of OpenAPI block because of the Scalar proxy failing on Cloudflare with the `cache` option
- Updated dependencies [709f1a1]
- Updated dependencies [ede2335]
- Updated dependencies [0426312]
- @gitbook/react-openapi@0.6.0
## 0.2.2
### Patch Changes
- Updated dependencies [3445db4]
- @gitbook/react-contentkit@0.5.0
- @gitbook/react-openapi@0.5.0
- @gitbook/react-math@0.5.0
## 0.2.1
### Patch Changes
- Updated dependencies [24cd72e]
- @gitbook/react-contentkit@0.4.0
- @gitbook/react-math@0.4.0
- @gitbook/react-openapi@0.4.0
## 0.2.0
### Minor Changes
- de747b7: Refactor the repository to be a proper monorepo and publish JS files on NPM instead of TypeScript files.
### Patch Changes
- Updated dependencies [de747b7]
- Updated dependencies [de747b7]
- @gitbook/react-contentkit@0.3.0
- @gitbook/react-openapi@0.3.0
- @gitbook/react-math@0.3.0
+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()}`;
}
+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,
},
);
+89
View File
@@ -0,0 +1,89 @@
{
"name": "gitbook",
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build:cloudflare": "next-on-pages",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"e2e": "playwright test",
"unit": "bun test {src,packages}",
"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"
},
"dependencies": {
"@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.56.0",
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@gitbook/react-contentkit": "workspace:*",
"@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.1.20",
"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.11.1",
"tailwind-merge": "^2.2.0",
"tailwind-shades": "^1.1.2",
"unified": "^11.0.4",
"url-join": "^5.0.0"
},
"devDependencies": {
"@argos-ci/playwright": "^2.0.0",
"@playwright/test": "^1.42.1",
"@cloudflare/next-on-pages": "^1.11.3",
"@cloudflare/workers-types": "^4.20231218.0",
"@types/js-cookie": "^3.0.6",
"@types/jsontoxml": "^1.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20",
"@types/object-hash": "^3.0.6",
"@types/parse-cache-control": "^1.0.4",
"@types/psi": "^4.1.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/rison": "^0.0.9",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "^14.2.5",
"eslint-plugin-import": "^2.29.1",
"jsonwebtoken": "^9.0.2",
"postcss": "^8",
"psi": "^4.1.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.5.3"
}
}
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: [
process.env.CI ? ['dot'] : ['list'],
['@argos-ci/playwright/reporter', { uploadToArgos: !!process.env.CI }],
],
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
});
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+27
View File
@@ -0,0 +1,27 @@
import {
BrowserClient,
makeFetchTransport,
defaultStackParser,
getCurrentScope,
} from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
// To tree shake default integrations that we don't use
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/#tree-shaking-default-integrations
const client = new BrowserClient({
debug: false,
dsn,
integrations: [],
sampleRate: 0.1,
enableTracing: false,
beforeSendTransaction: () => {
return null;
},
transport: makeFetchTransport,
stackParser: defaultStackParser,
});
getCurrentScope().setClient(client);
client.init();
}
+20
View File
@@ -0,0 +1,20 @@
import { init } from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
init({
debug: false,
dsn,
sampleRate: 0.1,
// Disable tracing as it creates additional requests in an env where subrequests are limited.
enableTracing: false,
// Disable transactions as it creates additional requests in an env where subrequests are limited.
// https://docs.sentry.io/platforms/node/configuration/filtering/#using--3
beforeSendTransaction: () => {
return null;
},
});
}
+20
View File
@@ -0,0 +1,20 @@
import { init } from '@sentry/nextjs';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
init({
debug: false,
dsn,
sampleRate: 0.1,
// Disable tracing as it creates additional requests in an env where subrequests are limited.
enableTracing: false,
// Disable transactions as it creates additional requests in an env where subrequests are limited.
// https://docs.sentry.io/platforms/node/configuration/filtering/#using--3
beforeSendTransaction: () => {
return null;
},
});
}
@@ -0,0 +1,93 @@
import { NextRequest } from 'next/server';
import {
verifyImageSignature,
resizeImage,
CloudflareImageOptions,
checkIsSizableImageURL,
} from '@/lib/images';
import { parseImageAPIURL } from '@/lib/urls';
export const runtime = 'edge';
/**
* Only on Cloudflare Workers:
*
* Fetch and resize an image.
*/
export async function GET(request: NextRequest) {
let urlParam = request.nextUrl.searchParams.get('url');
const signature = request.nextUrl.searchParams.get('sign');
// The current signature algorithm sets version as 1, but we need to support the older version as well
// for previously generated content. In this case, we default to version 0.
const signatureVersion = (request.nextUrl.searchParams.get('sv') as '1') || '0';
if (!urlParam || !signature) {
return new Response('Missing url/sign parameters', { status: 400 });
}
const url = parseImageAPIURL(urlParam);
// Prevent infinite loops
if (url.includes('/~gitbook/image')) {
return new Response('Invalid url parameter', { status: 400 });
}
// Check again if the image can be sized, even though we checked when rendering the Image component
// Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain
if (!checkIsSizableImageURL(url)) {
return new Response('Invalid url parameter', { status: 400 });
}
// Verify the signature
const verified = await verifyImageSignature(url, { signature, version: signatureVersion });
if (!verified) {
return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 });
}
// Cloudflare-specific options are in the cf object.
const options: CloudflareImageOptions = {
fit: 'scale-down',
format: 'jpeg',
quality: 100,
};
const width = request.nextUrl.searchParams.get('width');
if (width) {
options.width = Number(width);
}
const height = request.nextUrl.searchParams.get('height');
if (height) {
options.height = Number(height);
}
const dpr = request.nextUrl.searchParams.get('dpr');
if (dpr) {
options.dpr = Number(dpr);
}
const quality = request.nextUrl.searchParams.get('quality');
if (quality) {
options.quality = Number(quality);
}
// Check the Accept header to handle content negotiation
const accept = request.headers.get('accept');
if (accept && /image\/avif/.test(accept)) {
options.format = 'avif';
} else if (accept && /image\/webp/.test(accept)) {
options.format = 'webp';
}
try {
const response = await resizeImage(url, options);
if (!response.ok) {
throw new Error('Failed to resize image');
}
return response;
} catch (error) {
// Redirect to the original image if resizing fails
return Response.redirect(url, 302);
}
}
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTags } from '@/lib/cache';
export const runtime = 'edge';
interface JsonBody {
tags: string[];
purge?: boolean;
}
/**
* Revalidate cached data based on tags.
* The body should be a JSON with { tags: string[] }
*/
export async function POST(req: NextRequest) {
const json = (await req.json()) as JsonBody;
if (!json.tags || !Array.isArray(json.tags)) {
return NextResponse.json(
{
error: 'tags must be an array',
},
{ status: 400 },
);
}
const result = await revalidateTags(json.tags);
return NextResponse.json({
success: true,
keys: result.keys,
stats: result.stats,
});
}
@@ -0,0 +1,28 @@
'use client';
import { CustomizationThemeMode } from '@gitbook/api';
import { ThemeProvider } from 'next-themes';
import React from 'react';
export function ClientContexts(props: {
nonce: string;
forcedTheme: CustomizationThemeMode | undefined;
children: React.ReactNode;
}) {
const { children, forcedTheme } = props;
/**
* A bug in ThemeProvider is causing the nonce to be included incorrectly
* on the client-side. Original issue: https://github.com/pacocoursey/next-themes/issues/218
*
* This is a workaround for it, until next-themes fixes it in their library. There is already
* a PR: https://github.com/pacocoursey/next-themes/pull/223
*/
const nonce = typeof window === 'undefined' ? props.nonce : '';
return (
<ThemeProvider nonce={nonce} attribute="class" enableSystem forcedTheme={forcedTheme}>
{children}
</ThemeProvider>
);
}
@@ -0,0 +1,30 @@
/**
* Insert inline JS to detect Cloudflare Rocket Loader and notify the user that the content will not load correctly.
* It needs to be inserted as a script tag so that we can pass `data-cfasync="false"` to avoid being processed by Rocket Loader.
*/
export function RocketLoaderDetector(props: { nonce: string }) {
const { nonce } = props;
return (
<script
nonce={nonce}
suppressHydrationWarning
data-cfasync="false"
dangerouslySetInnerHTML={{
__html: `
document.addEventListener("DOMContentLoaded", () => {
if (Array.from(document.scripts).find(script => script.src.includes('rocket-loader.min.js'))) {
const alert = document.createElement('div');
alert.className = 'p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400 mt-8 mx-8';
alert.innerHTML = \`
<strong>Error in site configuration:</strong>
It looks like \${window.location.hostname} has been incorrectly configured in Cloudflare. This may lead to unexpected behavior or issues with the page loading. If you are the owner of this site, please refer to <a href="https://docs.gitbook.com/published-documentation/custom-domain/configure-dns#are-you-using-cloudflare" class="underline">GitBook's documentation</a> for steps to fix the problem.
\`;
document.body.prepend(alert);
}
});`,
}}
/>
);
}
@@ -0,0 +1,40 @@
'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React from 'react';
import { useScrollToHash } from '@/components/hooks';
/**
* Client component to initialize interactivity for a page.
*/
export function PageClientLayout(props: {}) {
// We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to the hash.
useScrollToHash();
useStripFallbackQueryParam();
return null;
}
/**
* Strip the fallback query parameter from current URL.
*
* When the user switches variants using the space dropdown, we pass a fallback=true parameter.
* This parameter indicates that we should redirect to the root page if the path from the
* previous variant doesn't exist in the new variant. If the path does exist, no redirect occurs,
* so we need to remove the fallback parameter.
*/
function useStripFallbackQueryParam() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
React.useEffect(() => {
if (searchParams.has('fallback')) {
const params = new URLSearchParams(searchParams.toString());
params.delete('fallback');
router.push(`${pathname}?${params.toString()}${window.location.hash ?? ''}`);
}
}, [router, pathname, searchParams]);
}
@@ -0,0 +1,28 @@
import { SkeletonHeading, SkeletonParagraph } from '@/components/primitives';
import { tcls } from '@/lib/tailwind';
/**
* Placeholder when loading a page.
*/
export default function PageSkeleton() {
return (
<div
className={tcls(
'flex',
'flex-row',
'flex-1',
'relative',
'py-8',
'lg:px-16',
'xl:mr-56',
'items-center',
'lg:items-start',
)}
>
<div className={tcls('flex-1', 'max-w-3xl', 'mx-auto')}>
<SkeletonHeading style={tcls('mb-8')} />
<SkeletonParagraph style={tcls('mb-4')} />
</div>
</div>
);
}
@@ -0,0 +1,27 @@
import { getSpaceLanguage, t } from '@/intl/server';
import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
import { tcls } from '@/lib/tailwind';
import { getContentPointer } from '../../fetch';
export default async function NotFound() {
const pointer = getContentPointer();
const { customization } = await ('siteId' in pointer
? getCurrentSiteLayoutData(pointer)
: getSpaceLayoutData(pointer.spaceId));
const language = getSpaceLanguage(customization);
return (
<div
className={tcls('flex-1', 'flex', 'flex-row', 'items-center', 'justify-center', 'py-9')}
>
<div className={tcls('max-w-80')}>
<h2 className={tcls('text-2xl', 'font-semibold', 'mb-2')}>
{t(language, 'notfound_title')}
</h2>
<p className={tcls('text-base', 'mb-4')}>{t(language, 'notfound')}</p>
</div>
</div>
);
}
@@ -0,0 +1,186 @@
import { CustomizationHeaderPreset, CustomizationThemeMode } from '@gitbook/api';
import { Metadata, Viewport } from 'next';
import { notFound, redirect } from 'next/navigation';
import React from 'react';
import { PageAside } from '@/components/PageAside';
import { PageBody, PageCover } from '@/components/PageBody';
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
import { ContentRefContext } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
import { PageClientLayout } from './PageClientLayout';
import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } from '../../fetch';
export const runtime = 'edge';
/**
* Fetch and render a page.
*/
export default async function Page(props: {
params: PagePathParams;
searchParams: { fallback?: string };
}) {
const { params, searchParams } = props;
const {
content: contentPointer,
contentTarget,
space,
parent,
customization,
pages,
page,
document,
} = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
redirectOnFallback: true,
});
const linksContext: PageHrefContext = {};
const rawPathname = getPathnameParam(params);
if (!page) {
const pathname = normalizePathname(rawPathname);
if (pathname !== rawPathname) {
// If the pathname was not normalized, redirect to the normalized version
// before trying to resolve the page again
redirect(absoluteHref(pathname));
} else {
notFound();
}
} else if (getPagePath(pages, page) !== rawPathname) {
redirect(pageHref(pages, page, linksContext));
}
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
const withFullPageCover = !!(
page.cover &&
page.layout.cover &&
page.layout.coverSize === 'full'
);
const withPageFeedback = customization.feedback.enabled;
const contentRefContext: ContentRefContext = {
space,
revisionId: contentTarget.revisionId,
pages,
page,
};
return (
<>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={contentRefContext} />
) : null}
<div className={tcls('flex', 'flex-row')}>
<PageBody
space={space}
contentPointer={contentPointer}
contentTarget={contentTarget}
customization={customization}
context={contentRefContext}
page={page}
document={document}
withPageFeedback={
// Display the page feedback in the page footer if the aside is not visible
withPageFeedback && !page.layout.outline
}
/>
{page.layout.outline ? (
<PageAside
space={space}
site={parent?.object === 'site' ? parent : undefined}
customization={customization}
page={page}
document={document}
withHeaderOffset={withTopHeader}
withFullPageCover={withFullPageCover}
withPageFeedback={withPageFeedback}
context={contentRefContext}
/>
) : null}
</div>
<React.Suspense fallback={null}>
<PageClientLayout />
</React.Suspense>
</>
);
}
export async function generateViewport({ params }: { params: PagePathParams }): Promise<Viewport> {
const { customization } = await fetchPageData(params);
return {
colorScheme: customization.themes.toggeable
? customization.themes.default === CustomizationThemeMode.Dark
? 'dark light'
: 'light dark'
: customization.themes.default,
};
}
export async function generateMetadata({
params,
searchParams,
}: {
params: PagePathParams;
searchParams: { fallback?: string };
}): Promise<Metadata> {
const { space, pages, page, customization, parent } = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
});
if (!page) {
notFound();
}
return {
title: [page.title, getContentTitle(space, customization, parent)]
.filter(Boolean)
.join(' | '),
description: page.description ?? '',
alternates: {
canonical: absoluteHref(getPagePath(pages, page), true),
},
openGraph: {
images: [
customization.socialPreview.url ??
absoluteHref(`~gitbook/ogimage/${page.id}`, true),
],
},
};
}
/**
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
*/
async function getPageDataWithFallback(args: {
pagePathParams: PagePathParams;
searchParams: { fallback?: string };
redirectOnFallback?: boolean;
}) {
const { pagePathParams, searchParams, redirectOnFallback = false } = args;
const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams);
let page = targetPage;
const canFallback = !!searchParams.fallback;
if (!page && canFallback) {
const rootPage = resolveFirstDocument(pages, []);
if (redirectOnFallback && rootPage?.page) {
redirect(pageHref(pages, rootPage?.page));
}
page = rootPage?.page;
}
return {
...otherPageData,
pages,
page,
};
}
@@ -0,0 +1,153 @@
import { CustomizationThemeMode } from '@gitbook/api';
import { Metadata, Viewport } from 'next';
import { headers } from 'next/headers';
import React from 'react';
import * as ReactDOM from 'react-dom';
import { AdminToolbar } from '@/components/AdminToolbar';
import { CookiesToast } from '@/components/Cookies';
import { LoadIntegrations } from '@/components/Integrations';
import { SpaceLayout } from '@/components/SpaceLayout';
import { api } from '@/lib/api';
import { assetsDomain } from '@/lib/assets';
import { buildVersion } from '@/lib/build';
import { getContentSecurityPolicyNonce } from '@/lib/csp';
import { absoluteHref, baseUrl } from '@/lib/links';
import { shouldIndexSpace } from '@/lib/seo';
import { getContentTitle } from '@/lib/utils';
import { ClientContexts } from './ClientContexts';
import { RocketLoaderDetector } from './RocketLoaderDetector';
import { fetchSpaceData } from '../fetch';
export const runtime = 'edge';
/**
* Layout when rendering the content.
*/
export default async function ContentLayout(props: { children: React.ReactNode }) {
const { children } = props;
const nonce = getContentSecurityPolicyNonce();
const {
content,
space,
contentTarget,
customization,
pages,
parent,
spaces,
ancestors,
scripts,
} = await fetchSpaceData();
ReactDOM.preconnect(api().endpoint);
if (assetsDomain) {
ReactDOM.preconnect(assetsDomain);
}
scripts.forEach(({ script }) => {
ReactDOM.preload(script, {
as: 'script',
nonce,
});
});
return (
<ClientContexts
nonce={nonce}
forcedTheme={
getQueryStringTheme() ??
(customization.themes.toggeable ? undefined : customization.themes.default)
}
>
<SpaceLayout
space={space}
contentTarget={contentTarget}
parent={parent}
spaces={spaces}
customization={customization}
pages={pages}
ancestors={ancestors}
content={content}
>
{children}
</SpaceLayout>
{scripts.length > 0 ? (
<>
<LoadIntegrations />
{scripts.map(({ script }) => (
<script key={script} async src={script} nonce={nonce} />
))}
</>
) : null}
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
<React.Suspense fallback={null}>
<CookiesToast privacyPolicy={customization.privacyPolicy.url} />
</React.Suspense>
) : null}
<RocketLoaderDetector nonce={nonce} />
<AdminToolbar space={space} content={content} />
</ClientContexts>
);
}
export async function generateViewport(): Promise<Viewport> {
const { customization } = await fetchSpaceData();
return {
colorScheme: customization.themes.toggeable
? customization.themes.default === CustomizationThemeMode.Dark
? 'dark light'
: 'light dark'
: customization.themes.default,
};
}
export async function generateMetadata(): Promise<Metadata> {
const { space, parent, customization } = await fetchSpaceData();
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
return {
title: getContentTitle(space, customization, parent),
generator: `GitBook (${buildVersion()})`,
metadataBase: new URL(baseUrl()),
icons: {
icon: [
{
url:
customIcon?.light ??
absoluteHref('~gitbook/icon?size=small&theme=light', true),
type: 'image/png',
media: '(prefers-color-scheme: light)',
},
{
url:
customIcon?.dark ??
absoluteHref('~gitbook/icon?size=small&theme=dark', true),
type: 'image/png',
media: '(prefers-color-scheme: dark)',
},
],
},
robots: shouldIndexSpace({ space, parent }) ? 'index, follow' : 'noindex, nofollow',
};
}
/**
* For preview, the theme can be set via query string (?theme=light).
*/
function getQueryStringTheme() {
const headersList = headers();
const queryStringTheme = headersList.get('x-gitbook-theme');
if (!queryStringTheme) {
return null;
}
return queryStringTheme === 'light'
? CustomizationThemeMode.Light
: CustomizationThemeMode.Dark;
}
@@ -0,0 +1,39 @@
import { ContentVisibility } from '@gitbook/api';
import { NextRequest } from 'next/server';
import { getCollection, getSite, getSpace } from '@/lib/api';
import { absoluteHref } from '@/lib/links';
import { shouldIndexSpace } from '@/lib/seo';
import { getContentPointer } from '../../fetch';
export const runtime = 'edge';
/**
* Generate a robots.txt for the current space.
*/
export async function GET(req: NextRequest) {
const pointer = getContentPointer();
const space = await getSpace(pointer.spaceId);
const parent =
'siteId' in pointer
? await getSite(pointer.organizationId, pointer.siteId)
: space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
const lines = [
`User-agent: *`,
'Disallow: /~gitbook/',
...(shouldIndexSpace({ space, parent })
? [`Allow: /`, `Sitemap: ${absoluteHref(`/sitemap.xml`, true)}`]
: [`Disallow: /`]),
];
const content = lines.join('\n');
return new Response(content, {
headers: {
'Content-Type': 'text/plain',
},
});
}
@@ -0,0 +1,86 @@
import { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
import jsontoxml from 'jsontoxml';
import { NextRequest } from 'next/server';
import { getSpaceContentData } from '@/lib/api';
import { absoluteHref } from '@/lib/links';
import { getPagePath } from '@/lib/pages';
import { getContentPointer } from '../../fetch';
export const runtime = 'edge';
/**
* Generate a sitemap.xml for the current space.
*/
export async function GET(req: NextRequest) {
const { pages: rootPages } = await getSpaceContentData(getContentPointer());
const pages = flattenPages(rootPages, (page) => !page.hidden);
const urls = pages.map(({ page, depth }) => {
// Decay priority with depth
const priority = Math.pow(2, -0.25 * depth);
// Normalize to keep 2 decimals
const normalizedPriority = Math.floor(100 * priority) / 100;
const lastModified = page.updatedAt || page.createdAt;
return {
url: {
loc: absoluteHref(getPagePath(rootPages, page), true),
priority: normalizedPriority,
...(lastModified
? {
// lastmod format is YYYY-MM-DD
lastmod: new Date(lastModified).toISOString().split('T')[0],
}
: {}),
},
};
});
const xml = jsontoxml(
[
{
name: 'urlset',
children: urls,
attrs: {
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9',
'xsi:schemaLocation':
'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
},
},
],
{
xmlHeader: true,
prettyPrint: true,
},
);
return new Response(xml, {
headers: {
'Content-Type': 'application/xml',
},
});
}
type FlatPageEntry = { page: RevisionPageDocument; depth: number };
function flattenPages(
rootPags: RevisionPage[],
filter: (page: RevisionPageDocument) => boolean,
): FlatPageEntry[] {
const flattenPage = (
page: RevisionPageDocument | RevisionPageGroup,
depth: number,
): FlatPageEntry[] => {
return [
...(page.type === 'document' && filter(page) ? [{ page, depth }] : []),
...page.pages.flatMap((child) =>
child.type === 'link' ? [] : flattenPage(child, depth + 1),
),
];
};
return rootPags.flatMap((page) => (page.type === 'link' ? [] : flattenPage(page, 0)));
}
@@ -0,0 +1,111 @@
import { ContentVisibility } from '@gitbook/api';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { getContentPointer } from '@/app/(space)/fetch';
import {
getCollection,
getCurrentSiteCustomization,
getSite,
getSpace,
getSpaceCustomization,
} from '@/lib/api';
import { getEmojiForCode } from '@/lib/emojis';
import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils';
export const runtime = 'edge';
const SIZES = {
/** Size for a favicon */
small: {
width: 48,
height: 48,
textSize: 'text-[32px]',
boxStyle: 'rounded-[8px]',
},
/** Size for display as an app icon or in the header */
medium: {
width: 256,
height: 256,
textSize: 'text-[164px]',
boxStyle: 'rounded-[32px]',
},
};
/**
* Render an icon for the space.
*/
export async function GET(req: NextRequest) {
const options = getOptions(req.url);
const size = SIZES[options.size];
const pointer = getContentPointer();
const spaceId = pointer.spaceId;
const [space, customization] = await Promise.all([
getSpace(spaceId),
'siteId' in pointer ? getCurrentSiteCustomization(pointer) : getSpaceCustomization(spaceId),
]);
const parent =
'siteId' in pointer
? await getSite(pointer.organizationId, pointer.siteId)
: space.visibility === ContentVisibility.InCollection && space.parent
? await getCollection(space.parent)
: null;
const contentTitle = getContentTitle(space, customization, parent);
return new ImageResponse(
(
<div
tw={tcls(options.theme === 'light' ? 'bg-white' : 'bg-gray-900', size.boxStyle)}
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<h2
tw={tcls(
size.textSize,
'font-bold',
'tracking-tight',
options.theme === 'light' ? 'text-gray-900' : 'text-white',
)}
>
{'emoji' in customization.favicon
? getEmojiForCode(customization.favicon.emoji)
: contentTitle.slice(0, 1).toUpperCase()}
</h2>
</div>
),
{
width: size.width,
height: size.height,
},
);
}
function getOptions(inputUrl: string): {
size: keyof typeof SIZES;
theme: 'light' | 'dark';
} {
const url = new URL(inputUrl);
const sizeParam = (url.searchParams.get('size') ?? 'small') as keyof typeof SIZES;
const themeParam = url.searchParams.get('theme') ?? 'light';
if (!SIZES[sizeParam] || !['light', 'dark'].includes(themeParam)) {
notFound();
}
return {
// @ts-ignore
size: sizeParam,
// @ts-ignore
theme: themeParam,
};
}
@@ -0,0 +1,53 @@
import { redirect } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { getContentTitle } from '@/lib/utils';
import { PageIdParams, fetchPageData } from '../../../../fetch';
export const runtime = 'edge';
/**
* Render the OpenGraph image for a space.
*/
export async function GET(req: NextRequest, { params }: { params: PageIdParams }) {
const { space, page, customization, parent } = await fetchPageData(params);
const url = new URL(space.urls.published ?? space.urls.app);
if (customization.socialPreview.url) {
// If user configured a custom social preview, we redirect to it.
redirect(customization.socialPreview.url);
}
return new ImageResponse(
(
<div
tw="bg-gray-50 py-16 px-14"
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<h2 tw="text-7xl font-bold tracking-tight text-left">
{getContentTitle(space, customization, parent)}
</h2>
<div tw="flex flex-1">
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>
</div>
<div tw="flex">
<p tw="text-4xl">
{url.hostname + (url.pathname.length > 1 ? url.pathname : '')}
</p>
</div>
</div>
),
{
width: 1200,
height: 630,
},
);
}
@@ -0,0 +1,148 @@
'use client';
import { AlertTriangle } from '@geist-ui/icons';
import React from 'react';
import { useScrollActiveId } from '@/components/hooks';
import { Button } from '@/components/primitives';
import { t, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { getPDFUrl } from '@/lib/urls';
const limitExtend = 50;
/**
* Dynamic controls to show active page and to let the user select between modes.
*/
export function PageControlButtons(props: {
pdfHref: string;
singlePageMode: boolean;
/** Array of the [pageId, divId] */
pageIds: Array<[string, string]>;
/** Current limit */
limit: number;
/** Total number of pages targetted by the generation */
total: number;
/** Trademark to display */
trademark?: React.ReactNode;
}) {
const { pdfHref, singlePageMode, pageIds, limit, total, trademark } = props;
const language = useLanguage();
const divIds = React.useMemo(() => {
return pageIds.map((entry) => entry[1]);
}, [pageIds]);
const activeDivId = useScrollActiveId(divIds, {
threshold: 0,
});
const activeIndex = (activeDivId ? divIds.indexOf(activeDivId) : 0) + 1;
const activePageId = pageIds[activeIndex - 1]?.[0];
return (
<>
<div
className={tcls(
'fixed',
'left-12',
'bottom-12',
'flex',
'flex-col',
'gap-2',
'print:hidden',
'z-50',
)}
>
{singlePageMode ? null : (
<Button
href={getPDFUrl(new URL(pdfHref), {
page: activePageId,
only: true,
}).toString()}
variant="secondary"
>
{t(language, 'pdf_mode_only_page')}
</Button>
)}
<Button
href={getPDFUrl(new URL(pdfHref), { page: undefined, only: false }).toString()}
variant="secondary"
>
{t(language, 'pdf_mode_all')}
</Button>
{trademark ? <div className={tcls('mt-5')}>{trademark}</div> : null}
</div>
<div
className={tcls(
'fixed',
'right-12',
'bottom-12',
'flex',
'flex-col',
'items-end',
'gap-2',
'print:hidden',
'z-50',
)}
>
{total !== pageIds.length ? (
<div
role="banner"
className={tcls(
'flex',
'flex-row',
'items-start',
'mb-5',
'bg-yellow-100',
'border-yellow-400',
'text-yellow-800',
'shadow-sm',
'border',
'rounded-md',
'p-4',
'max-w-sm',
)}
>
<AlertTriangle className={tcls('size-6', 'mr-3', 'mt-1')} />{' '}
<div>
<div>{t(language, 'pdf_limit_reached', total, pageIds.length)}</div>
<div>
<a
href={getPDFUrl(new URL(pdfHref), {
page: undefined,
only: false,
limit: limit + limitExtend,
}).toString()}
className={tcls('underline')}
>
{t(language, 'pdf_limit_reached_continue', limitExtend)}
</a>
</div>
</div>
</div>
) : null}
<div
className={tcls(
'flex',
'flex-row',
'items-center',
'justify-center',
'text-lg',
'text-dark/6',
'px-6',
'py-2',
'bg-slate-100',
'rounded-full',
'shadow-sm',
'border-slate-300',
'border',
)}
>
{t(language, 'pdf_page_of', activeIndex, pageIds.length)}
</div>
</div>
</>
);
}
@@ -0,0 +1,19 @@
'use client';
import * as React from 'react';
import { PolymorphicComponentProp } from '@/components/utils/types';
export function PrintButton(props: PolymorphicComponentProp<'button'>) {
const { className, children, ...rest } = props;
const onClick = React.useCallback(() => {
window.print();
}, []);
return (
<button {...rest} onClick={onClick} className={className}>
{children}
</button>
);
}
@@ -0,0 +1,364 @@
import { ArrowLeft, Printer } from '@geist-ui/icons';
import {
CustomizationSettings,
Revision,
RevisionPageDocument,
RevisionPageGroup,
RevisionPageLink,
SiteCustomizationSettings,
Space,
} from '@gitbook/api';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import * as React from 'react';
import { getContentPointer } from '@/app/(space)/fetch';
import { DocumentView, createHighlightingContext } from '@/components/DocumentView';
import { TrademarkLink } from '@/components/TableOfContents/Trademark';
import { PolymorphicComponentProp } from '@/components/utils/types';
import { getSpaceLanguage } from '@/intl/server';
import { tString } from '@/intl/translate';
import {
getDocument,
getSpace,
getSpaceCustomization,
getSpaceContentData,
getCurrentSiteCustomization,
} from '@/lib/api';
import { pagePDFContainerId, PageHrefContext, absoluteHref } from '@/lib/links';
import { resolvePageId } from '@/lib/pages';
import { ContentRefContext, resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { PDFSearchParams, getPDFSearchParams } from '@/lib/urls';
import './pdf.css';
import { PageControlButtons } from './PageControlButtons';
import { PrintButton } from './PrintButton';
const DEFAULT_LIMIT = 100;
export const runtime = 'edge';
export async function generateMetadata(): Promise<Metadata> {
const contentPointer = getContentPointer();
const [space, customization] = await Promise.all([
getSpace(contentPointer.spaceId),
'siteId' in contentPointer
? getCurrentSiteCustomization(contentPointer)
: getSpaceCustomization(contentPointer.spaceId),
]);
return {
title: customization.title ?? space.title,
robots: 'noindex, nofollow',
};
}
/**
* Render a space as a standalone HTML page that can be printed as a PDF.
*/
export default async function PDFHTMLOutput(props: { searchParams: { [key: string]: string } }) {
const contentPointer = getContentPointer();
const searchParams = new URLSearchParams(props.searchParams);
const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams));
// Build current PDF URL and preserve all search params
let currentPDFUrl = absoluteHref('~gitbook/pdf', true);
currentPDFUrl += '?' + searchParams.toString();
// Load the content,
const [customization, { space, contentTarget, pages: rootPages }] = await Promise.all([
'siteId' in contentPointer
? getCurrentSiteCustomization(contentPointer)
: getSpaceCustomization(contentPointer.spaceId),
getSpaceContentData(contentPointer),
]);
const language = getSpaceLanguage(customization);
// Compute the pages to render
const { pages, total } = selectPages(rootPages, pdfParams);
const pageIds = pages.map(
({ page }) => [page.id, pagePDFContainerId(page)] as [string, string],
);
const linksContext: PageHrefContext = {
pdf: pages.map(({ page }) => page.id),
};
return (
<div className="print-mode">
{pdfParams.back !== 'false' ? (
<div className={tcls('fixed', 'left-12', 'top-12', 'print:hidden', 'z-50')}>
<a
title={tString(language, 'pdf_goback')}
href={pdfParams.back ?? absoluteHref('')}
className={tcls(
'flex',
'flex-row',
'items-center',
'justify-center',
'text-sm',
'text-dark/6',
'hover:text-primary',
'p-4',
'dark:text-light/5',
'rounded-full',
'bg-white',
'shadow-sm',
'hover:shadow-md',
'border-slate-300',
'border',
)}
>
<ArrowLeft className={tcls('size-6')} />
</a>
</div>
) : null}
<div className={tcls('fixed', 'right-12', 'top-12', 'print:hidden', 'z-50')}>
<PrintButton
title={tString(language, 'pdf_print')}
className={tcls(
'flex',
'flex-row',
'items-center',
'justify-center',
'text-sm',
'text-dark/6',
'hover:text-primary',
'p-4',
'dark:text-light/5',
'rounded-full',
'bg-white',
'shadow-sm',
'hover:shadow-md',
'border-slate-300',
'border',
)}
>
<Printer className={tcls('size-6')} />
</PrintButton>
</div>
<PageControlButtons
pageIds={pageIds}
pdfHref={currentPDFUrl}
singlePageMode={!!pdfParams.only}
total={total}
limit={pdfParams.limit ?? DEFAULT_LIMIT}
trademark={
customization.trademark.enabled ? (
<TrademarkLink space={space} customization={customization} />
) : null
}
/>
{pdfParams.only ? null : <PDFSpaceIntro space={space} customization={customization} />}
{pages.map(({ page }) =>
page.type === 'group' ? (
<PDFPageGroup key={page.id} space={space} page={page} />
) : (
<React.Suspense
key={page.id}
fallback={
<PrintPage id={pagePDFContainerId(page)}>
<p>Loading...</p>
</PrintPage>
}
>
<PDFPageDocument
space={space}
page={page}
refContext={{
space,
revisionId: contentTarget.revisionId,
pages: rootPages,
page,
...linksContext,
}}
/>
</React.Suspense>
),
)}
</div>
);
}
async function PDFSpaceIntro(props: {
space: Space;
customization: CustomizationSettings | SiteCustomizationSettings;
}) {
const { space, customization } = props;
return (
<PrintPage isFirst>
<div className={tcls('flex', 'items-center', 'justify-center', 'py-12')}>
<h1 className={tcls('text-6xl', 'font-bold')}>
{customization.title ?? space.title}
</h1>
</div>
</PrintPage>
);
}
async function PDFPageGroup(props: { space: Space; page: RevisionPageGroup }) {
const { page } = props;
return (
<PrintPage id={pagePDFContainerId(page)}>
<div
className={tcls(
'break-before-page',
'mt-10',
'print:mt-0',
'flex',
'items-center',
'justify-center',
'py-12',
)}
>
<h1 className={tcls('text-5xl', 'font-bold')}>{page.title}</h1>
</div>
</PrintPage>
);
}
async function PDFPageDocument(props: {
space: Space;
page: RevisionPageDocument;
refContext: ContentRefContext;
}) {
const { space, page, refContext } = props;
const document = page.documentId ? await getDocument(space.id, page.documentId) : null;
const shouldHighlightCode = createHighlightingContext();
return (
<PrintPage id={pagePDFContainerId(page)}>
<h1 className={tcls('text-4xl', 'font-bold')}>{page.title}</h1>
{page.description ? (
<p className={tcls('decoration-primary/6', 'mt-2', 'mb-3')}>{page.description}</p>
) : null}
{document ? (
<DocumentView
document={document}
style={'mt-6 space-y-6'}
blockStyle={['max-w-full']}
context={{
mode: 'print',
content: {
spaceId: space.id,
revisionId: refContext.revisionId,
},
contentRefContext: refContext,
resolveContentRef: (ref) => resolveContentRef(ref, refContext),
getId: (id) => pagePDFContainerId(page, id),
shouldHighlightCode,
}}
/>
) : null}
</PrintPage>
);
}
function PrintPage(
props: PolymorphicComponentProp<
'div',
{
isFirst?: boolean;
}
>,
) {
const { children, isFirst, className, ...rest } = props;
return (
<div
{...rest}
className={tcls(
className,
'my-11',
'print:my-0',
'mx-auto',
'max-w-4xl',
'w-full',
'p-12',
'print:p-0',
'shadow-xl',
'print:shadow-none',
'rounded-sm',
'bg-white',
'min-h-[29.7cm]',
'print:min-h-0',
isFirst ? null : 'break-before-page',
'break-anywhere',
)}
>
{children}
</div>
);
}
type FlatPageEntry = { page: RevisionPageDocument | RevisionPageGroup; depth: number };
/**
* Compute the ordered flat set of pages to render.
*/
function selectPages(
rootPages: Revision['pages'],
params: PDFSearchParams,
): { pages: FlatPageEntry[]; total: number } {
const flattenPage = (
page: RevisionPageDocument | RevisionPageGroup,
depth: number,
): FlatPageEntry[] => {
return [
{ page, depth },
...page.pages.flatMap((child) => {
if (child.type === 'link') {
return [];
}
if (child.hidden) {
return [];
}
return flattenPage(child, depth + 1);
}),
];
};
const limitTo = (entries: FlatPageEntry[]) => {
return {
// Apply a soft-limit, the limit can be controlled by the URL to allow testing
pages: entries.slice(0, params.limit ?? DEFAULT_LIMIT),
total: entries.length,
};
};
if (params.page) {
const found = resolvePageId(rootPages, params.page);
if (!found) {
notFound();
}
if (!params.only) {
return limitTo([{ page: found.page, depth: 0 }]);
}
return limitTo(flattenPage(found.page, 0));
}
const allPages = rootPages.flatMap((page) => {
if (page.type === 'link') {
return [];
}
if (page.hidden) {
return [];
}
return flattenPage(page, 0);
});
return limitTo(allPages);
}
@@ -0,0 +1,8 @@
@page {
size: A4;
margin: 60pt 60pt;
@bottom-right {
content: counter(page);
}
}
@@ -0,0 +1,20 @@
'use client';
import React from 'react';
import { RecoilRoot } from 'recoil';
import { TranslateContext } from '@/intl/client';
import { TranslationLanguage } from '@/intl/translations';
export function ClientContexts(props: {
language: TranslationLanguage;
children: React.ReactNode;
}) {
const { children, language } = props;
return (
<RecoilRoot>
<TranslateContext.Provider value={language}>{children}</TranslateContext.Provider>
</RecoilRoot>
);
}
@@ -0,0 +1,52 @@
'use client';
import { captureException } from '@sentry/nextjs';
import React from 'react';
import { Button } from '@/components/primitives/Button';
import { t, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
export default function ErrorPage(props: {
error: Error & { digest?: string };
reset: () => void;
}) {
const { error, reset } = props;
const language = useLanguage();
React.useEffect(() => {
captureException(error);
}, [error]);
return (
<div
className={tcls(
'fixed',
'w-full',
'h-full',
'flex',
'items-center',
'justify-center',
'p-7',
)}
>
<div>
<h2 className={tcls('text-2xl', 'font-semibold', 'mb-2')}>
{t(language, 'unexpected_error_title')}
</h2>
<p className={tcls('text-base', 'mb-4')}>{t(language, 'unexpected_error')}</p>
<div>
<Button
onClick={() => {
reset();
}}
variant="secondary"
size="small"
>
{t(language, 'unexpected_error_retry')}
</Button>
</div>
</div>
</div>
);
}
+227
View File
@@ -0,0 +1,227 @@
import { ContentVisibility, RevisionPage, Space } from '@gitbook/api';
import { headers } from 'next/headers';
import {
getCollectionSpaces,
getCollection,
ContentPointer,
getRevisionPageByPath,
getDocument,
getSpaceData,
ContentTarget,
SiteContentPointer,
getCurrentSiteData,
getSite,
getSiteSpaces,
} from '@/lib/api';
import { resolvePagePath, resolvePageId } from '@/lib/pages';
export interface PagePathParams {
pathname?: string[];
}
export interface PageIdParams {
pageId: string;
}
/**
* Get the current content pointer from the params.
*/
export function getContentPointer(): ContentPointer | SiteContentPointer {
const headerSet = headers();
const spaceId = headerSet.get('x-gitbook-content-space');
if (!spaceId) {
throw new Error(
'getContentPointer is called outside the scope of a request processed by the middleware',
);
}
const siteId = headerSet.get('x-gitbook-content-site');
if (siteId) {
const organizationId = headerSet.get('x-gitbook-content-organization');
const siteSpaceId = headerSet.get('x-gitbook-content-site-space');
const siteShareKey = headerSet.get('x-gitbook-content-site-share-key');
if (!organizationId) {
throw new Error('Missing site content headers');
}
const siteContent: SiteContentPointer = {
siteId,
spaceId,
siteSpaceId: siteSpaceId ?? undefined,
siteShareKey: siteShareKey ?? undefined,
organizationId,
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
};
return siteContent;
} else {
const content: ContentPointer = {
spaceId,
revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined,
changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined,
};
return content;
}
}
/**
* Fetch all the data needed to render the space layout.
*/
export async function fetchSpaceData() {
const content = getContentPointer();
const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all(
'siteId' in content
? [
getCurrentSiteData(content),
fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
}),
]
: [getSpaceData(content)],
);
const parent = await (parentSite ?? fetchParentCollection(space));
return {
content,
contentTarget,
space,
pages,
customization,
scripts,
ancestors: [],
...parent,
};
}
/**
* Fetch all the data needed to render the content.
* Optimized to fetch in parallel as much as possible.
*/
export async function fetchPageData(params: PagePathParams | PageIdParams) {
const content = getContentPointer();
const { space, contentTarget, pages, customization, scripts } = await ('siteId' in content
? getCurrentSiteData(content)
: getSpaceData(content));
const page = await resolvePage(contentTarget, pages, params);
const [parent, document] = await Promise.all([
'siteId' in content
? fetchParentSite({
organizationId: content.organizationId,
siteId: content.siteId,
siteShareKey: content.siteShareKey,
})
: fetchParentCollection(space),
page?.page.documentId ? getDocument(space.id, page.page.documentId) : null,
]);
return {
content,
contentTarget,
space,
pages,
customization,
scripts,
ancestors: [],
...page,
...parent,
document,
};
}
/**
* Resolve a page from the params.
* If the path can't be found, we try to resolve it from the API to handle redirects.
*/
async function resolvePage(
contentTarget: ContentTarget,
pages: RevisionPage[],
params: PagePathParams | PageIdParams,
) {
if ('pageId' in params) {
return resolvePageId(pages, params.pageId);
}
const rawPathname = getPathnameParam(params);
const pathname = normalizePathname(rawPathname);
// When resolving a page, we use the lowercased pathname
const page = resolvePagePath(pages, pathname);
if (page) {
return page;
}
// If page can't be found, we try with the API, in case we have a redirect
// We use the raw pathname to handle special/malformed redirects setup by users in the GitSync.
// The page rendering will take care of redirecting to a normalized pathname.
//
// We don't test path that are too long as GitBook doesn't support them and will return a 404 anyway.
if (rawPathname.length <= 512) {
const resolved = await getRevisionPageByPath(
contentTarget.spaceId,
contentTarget.revisionId,
rawPathname,
);
if (resolved) {
return resolvePageId(pages, resolved.id);
}
}
return undefined;
}
async function fetchParentCollection(space: Space) {
const parentCollectionId =
space.visibility === ContentVisibility.InCollection ? space.parent : undefined;
const [collection, spaces] = await Promise.all([
parentCollectionId ? getCollection(parentCollectionId) : null,
parentCollectionId ? getCollectionSpaces(parentCollectionId) : ([] as Space[]),
]);
return { parent: collection, spaces };
}
async function fetchParentSite(args: {
organizationId: string;
siteId: string;
siteShareKey: string | undefined;
}) {
const { organizationId, siteId, siteShareKey } = args;
const [site, siteSpaces] = await Promise.all([
getSite(organizationId, siteId),
getSiteSpaces({ organizationId, siteId, siteShareKey }),
]);
const spaces: Record<string, Space> = {};
siteSpaces.forEach((siteSpace) => {
spaces[siteSpace.space.id] = {
...siteSpace.space,
urls: {
...siteSpace.space.urls,
published: siteSpace.urls.published,
},
};
});
return { parent: site, spaces: Object.values(spaces) };
}
/**
* Get the page path from the params.
*/
export function getPathnameParam(params: PagePathParams): string {
const { pathname } = params;
return pathname ? pathname.map((part) => decodeURIComponent(part)).join('/') : '';
}
/**
* Normalize the URL pathname into the format used in the revision page path.
*/
export function normalizePathname(pathname: string) {
return pathname.toLowerCase();
}
@@ -0,0 +1,235 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--scrollbar-width: calc(100vw - 100%);
--dark-base: 20 20 20;
--light-base: 251 252 252;
--light-1: color-mix(in srgb, rgb(var(--primary-base-600)), rgb(var(--light-base)) 99%);
--light-DEFAULT: color-mix(
in srgb,
rgb(var(--primary-base-700)),
rgb(var(--light-base)) 96%
);
--light-2: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 92%);
--light-3: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 88%);
--light-4: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 72%);
--dark-1: color-mix(in srgb, rgb(var(--primary-base-100)), rgb(1 1 1) 96%);
--dark-DEFAULT: color-mix(
in srgb,
rgb(var(--primary-base-300)),
rgb(var(--dark-base)) 94.5%
);
--dark-2: color-mix(in srgb, rgb(var(--primary-base-200)), rgb(var(--dark-base)) 92%);
--dark-3: color-mix(in srgb, rgb(var(--primary-base-200)), rgb(25 25 25) 91%);
--dark-4: color-mix(in srgb, rgb(var(--primary-base-200)), rgb(var(--dark-base)) 64%);
@apply leading-relaxed;
}
body {
@apply text-dark dark:text-light;
}
html {
@apply gutter-stable;
}
html:is(.plain-background) {
--dark-base: 22 22 22;
--light-base: 255 255 255;
/* reset primaries as greys */
--primary-base-50: 239 239 239;
--primary-base-100: 222 222 222;
--primary-base-200: 200 200 200;
--primary-base-300: 180 180 180;
--primary-base-400: 150 150 150;
--primary-base-500: 120 120 120;
--primary-base-600: 90 90 90;
--primary-base-700: 70 70 70;
--primary-base-800: 30 30 30;
--primary-base-900: 19 19 19;
--light-DEFAULT: color-mix(
in srgb,
rgb(var(--primary-base-700)),
rgb(var(--light-base)) 100%
);
--light-2: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 96%);
--light-3: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 90%);
--light-4: color-mix(in srgb, rgb(var(--primary-base-800)), rgb(var(--light-base)) 76%);
}
h1 {
@apply tracking-[-0.025em] text-dark dark:text-light text-balance;
}
h2,
h3,
h4,
h5,
h6 {
@apply tracking-[-0.0125em] text-dark dark:text-light;
}
a,
button,
input,
textarea {
@apply outline-2 outline-primary/6;
}
/* Light mode */
::-webkit-scrollbar {
@apply bg-dark/1;
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
@apply bg-dark/4;
border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-dark/5;
}
/* Dark mode */
.dark ::-webkit-scrollbar {
@apply bg-light/1;
}
.dark ::-webkit-scrollbar-thumb {
@apply bg-light/3;
}
.dark ::-webkit-scrollbar-thumb:hover {
@apply bg-light/4;
}
/* Inverted theme light mode */
.inverted-theme-scroll::-webkit-scrollbar {
@apply bg-transparent;
}
.inverted-theme-scroll::-webkit-scrollbar-thumb {
@apply bg-dark/5;
}
.inverted-theme-scroll::-webkit-scrollbar-thumb:hover {
@apply bg-dark/6;
}
.flip-heading-hash {
@apply [&:is(h1,h2,h3,h4)]:content-start;
@apply [&:is(h1,h2,h3,h4)]:[grid-auto-columns:auto_1fr];
@apply [&:is(h1,h2,h3,h4)>div:first-child]:[grid-area:1/2];
@apply [&:is(h1,h2,h3,h4)>div:first-child]:ml-1;
}
/* bullet icons */
.bullet-dash {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjEiIHk9IjIuNSIgd2lkdGg9IjgiIGhlaWdodD0iMS41IiByeD0iMC42NiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==")];
}
.bullet-circleFilled {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIiIHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHJ4PSIzIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K")];
}
.bullet-circle {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIuNzUiIHk9IjAuNzUiIHdpZHRoPSI1LjUiIGhlaWdodD0iNS41IiByeD0iMi43NSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPC9zdmc+Cg==")];
}
.bullet-squareFilled {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIiIHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHJ4PSIxIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K")];
}
.bullet-square {
@apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxtYXNrIGlkPSJwYXRoLTEtaW5zaWRlLTFfOV80OCIgZmlsbD0id2hpdGUiPgo8cmVjdCB4PSIyIiB3aWR0aD0iNyIgaGVpZ2h0PSI3IiByeD0iMSIvPgo8L21hc2s+CjxyZWN0IHg9IjIiIHdpZHRoPSI3IiBoZWlnaHQ9IjciIHJ4PSIxIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjMiIG1hc2s9InVybCgjcGF0aC0xLWluc2lkZS0xXzlfNDgpIi8+Cjwvc3ZnPgo=")];
}
}
@layer utilities {
/* hide scroll bar but still scroll */
.hide-scroll::-webkit-scrollbar {
display: none;
}
.hide-scroll {
scrollbar-width: none;
-ms-overflow-style: none;
}
.linear-mask-gradient {
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 96px, rgba(0, 0, 0, 0));
}
.linear-mask-util {
mask-image: linear-gradient(to bottom, white, white);
}
.grid-area-1-1 {
grid-area: 1/1;
}
.gutter-stable {
scrollbar-gutter: stable;
}
.triangle {
position: relative;
background-color: orange;
text-align: left;
transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
}
.triangle:before,
.triangle:after {
content: '';
position: absolute;
background-color: inherit;
}
.triangle,
.triangle:before,
.triangle:after {
width: inherit;
height: inherit;
border-top-right-radius: 30%;
}
.triangle:before {
transform: rotate(-135deg) skewX(-45deg) scale(1.414, 0.707) translate(0, -50%);
}
.triangle:after {
transform: rotate(135deg) skewY(-45deg) scale(0.707, 1.414) translate(50%);
}
.break-anywhere {
word-break: break-word;
}
@supports (overflow-wrap: anywhere) {
.break-anywhere {
word-break: break-word;
overflow-wrap: anywhere;
}
}
/**
* Utility to prevent horizontal jumps when the vertical scrollbar appears
* It should be used on main containers of the layout, while preserving horizontal delimiters.
* It basically adds a margin-left with the width of the scrollbar, and a width that compensates for it; when a scrollbar is displayed.
* Inspired by https://aykevl.nl/2014/09/fix-jumping-scrollbar, but adapted to work on child elements of "html"
*/
.scroll-nojump {
width: 100%;
}
@media screen and (min-width: 1536px) {
.scroll-nojump {
margin-left: var(--scrollbar-width);
margin-right: 0;
width: calc(100% - var(--scrollbar-width));
}
}
}
html {
color-scheme: light;
/** Ensure PDF export and print correctly displays the background colors */
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
html.dark {
color-scheme: dark light;
}
+190
View File
@@ -0,0 +1,190 @@
import {
CustomizationBackground,
CustomizationCorners,
CustomizationHeaderPreset,
CustomizationSettings,
SiteCustomizationSettings,
} from '@gitbook/api';
import assertNever from 'assert-never';
import colors from 'tailwindcss/colors';
import { emojiFontClassName } from '@/components/primitives';
import { fonts, ibmPlexMono } from '@/fonts';
import { getSpaceLanguage } from '@/intl/server';
import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
import { hexToRgb, shadesOfColor } from '@/lib/colors';
import { tcls } from '@/lib/tailwind';
import { ClientContexts } from './ClientContexts';
import './globals.css';
import { getContentPointer } from './fetch';
/**
* Layout shared between the content and the PDF renderer.
* It takes care of setting the theme and the language.
*/
export default async function SpaceRootLayout(props: { children: React.ReactNode }) {
const { children } = props;
const pointer = getContentPointer();
const { customization } = await ('siteId' in pointer
? getCurrentSiteLayoutData(pointer)
: getSpaceLayoutData(pointer.spaceId));
const headerTheme = generateHeaderTheme(customization);
const language = getSpaceLanguage(customization);
return (
<html
suppressHydrationWarning
lang={customization.internationalization.locale}
className={
tcls(
customization.header.preset === CustomizationHeaderPreset.None
? null
: [
// Take the sticky header in consideration for the scrolling
`scroll-pt-[76px]`,
],
) +
(customization.styling.corners === CustomizationCorners.Straight
? ' straight-corners'
: '') +
(customization.styling.background === CustomizationBackground.Plain
? ' plain-background'
: '')
}
>
<head>
{customization.privacyPolicy.url ? (
<link rel="privacy-policy" href={customization.privacyPolicy.url} />
) : null}
<style
nonce={
//Since I can't get the nonce to work for inline styles, we need to allow unsafe-inline
undefined
}
>{`
:root {
${generateColorVariable(
'primary-color',
customization.styling.primaryColor.light,
)}
${generateColorVariable(
'primary-base',
customization.styling.primaryColor.light,
)}
${generateColorVariable(
'header-background',
headerTheme.backgroundColor.light,
)}
${generateColorVariable('header-link', headerTheme.linkColor.light)}
}
.dark {
${generateColorVariable(
'primary-color',
customization.styling.primaryColor.dark,
)}
${generateColorVariable(
'primary-base',
customization.styling.primaryColor.dark,
)}
${generateColorVariable(
'header-background',
headerTheme.backgroundColor.dark,
)}
${generateColorVariable('header-link', headerTheme.linkColor.dark)}
}
`}</style>
</head>
<body
className={tcls(
emojiFontClassName,
`${fonts[customization.styling.font].className}`,
`${ibmPlexMono.variable}`,
'bg-light',
'dark:bg-dark',
)}
>
<ClientContexts language={language}>{children}</ClientContexts>
</body>
</html>
);
}
type ColorInput = string | Record<string, string>;
function generateColorVariable(name: string, color: ColorInput) {
const shades: Record<string, string> = typeof color === 'string' ? shadesOfColor(color) : color;
return Object.entries(shades)
.map(([key, value]) => {
// Check the original hex value
const rgbValue = hexToRgb(value);
return `--${name}-${key}: ${rgbValue};`;
})
.join('\n');
}
function generateHeaderTheme(customization: CustomizationSettings | SiteCustomizationSettings): {
backgroundColor: { light: ColorInput; dark: ColorInput };
linkColor: { light: ColorInput; dark: ColorInput };
} {
switch (customization.header.preset) {
case CustomizationHeaderPreset.None:
case CustomizationHeaderPreset.Default: {
return {
backgroundColor: {
light: colors.white,
dark: colors.black,
},
linkColor: {
light: customization.styling.primaryColor.light,
dark: customization.styling.primaryColor.dark,
},
};
}
case CustomizationHeaderPreset.Bold: {
return {
backgroundColor: {
light: customization.styling.primaryColor.light,
dark: customization.styling.primaryColor.dark,
},
linkColor: {
// TODO: should depend on the color of the background
light: colors.white,
dark: colors.black,
},
};
}
case CustomizationHeaderPreset.Contrast: {
return {
backgroundColor: {
light: colors.black,
dark: colors.white,
},
linkColor: {
light: colors.white,
dark: colors.black,
},
};
}
case CustomizationHeaderPreset.Custom: {
return {
backgroundColor: {
light: customization.header.backgroundColor?.light ?? colors.white,
dark: customization.header.backgroundColor?.dark ?? colors.black,
},
linkColor: {
light:
customization.header.linkColor?.light ??
customization.styling.primaryColor.light,
dark:
customization.header.linkColor?.dark ??
customization.styling.primaryColor.dark,
},
};
}
default: {
assertNever(customization.header.preset);
}
}
}
+19
View File
@@ -0,0 +1,19 @@
'use client';
import { captureException } from '@sentry/nextjs';
import Error from 'next/error';
import { useEffect } from 'react';
export default function GlobalError({ error }: { error: Error }) {
useEffect(() => {
captureException(error);
}, [error]);
return (
<html>
<body>
<Error statusCode={undefined as any} />
</body>
</html>
);
}
@@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from 'next/server';
type ProxyRequest = {
url: string;
method: string;
headers: Record<string, string>;
data: Record<string, any>;
};
export const runtime = 'edge';
/**
* Taken from https://github.com/scalar/scalar/tree/main/packages/api-client-proxy
*/
export async function POST(req: NextRequest) {
const requestBody: ProxyRequest = await req.json();
const isGetOrHeadRequest = ['get', 'head'].includes(requestBody.method.trim().toLowerCase());
const body = isGetOrHeadRequest
? null
: requestBody.data
? JSON.stringify(requestBody.data)
: null;
// Default options are marked with *
try {
const response = await fetch(requestBody.url.trim(), {
// *GET, POST, PUT, DELETE, etc.
method: requestBody.method.trim(),
// no-cors, *cors, same-origin
// mode: 'cors', // Not supported on Cloudflare Workers
// *default, no-cache, reload, force-cache, only-if-cached
// cache: 'no-cache', // Not supported on Cloudflare Workers
// include, *same-origin, omit
// credentials: 'include', // Not supported on Cloudflare Workers
headers: requestBody.headers,
// manual, *follow, error
redirect: 'follow',
// no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
// referrerPolicy: 'no-referrer', // Not supported on Cloudflare Workers
// body data type must match "Content-Type" header
body,
});
const headers: Record<string, string> = {};
const proxyHeaders = [...response.headers];
proxyHeaders.forEach(([key, value]) => {
if (['access-control-allow-origin'].includes(key.toLowerCase())) {
return;
}
headers[key] = value;
});
const text = await response.text();
return NextResponse.json({
statusCode: response.status,
// TODO: Do we need body?
// body: …
data: text,
headers: {
...headers,
'X-API-Client-Content-Length': text.length,
},
// TODO: transform cookie data
cookies: response.headers.get('cookies'),
});
} catch (error) {
console.error(
'Scalar API Client Proxy Error',
(error as Error).stack ?? (error as Error).message ?? error,
);
return NextResponse.json({
data: 'Scalar API Client Proxy Error',
});
}
}
@@ -0,0 +1,113 @@
import { Space } from '@gitbook/api';
import React from 'react';
import { ContentPointer, getChangeRequest, getRevision } from '@/lib/api';
import { tcls } from '@/lib/tailwind';
interface AdminToolbarProps {
content: ContentPointer;
space: Space;
}
/**
* Toolbar with information for the content admin when previewing a revision or change-request.
*/
export function AdminToolbar(props: AdminToolbarProps) {
const { content } = props;
const toolbar = (() => {
if (content.changeRequestId) {
return (
<ChangeRequestToolbar
spaceId={content.spaceId}
changeRequestId={content.changeRequestId}
/>
);
}
if (content.revisionId) {
return <RevisionToolbar spaceId={content.spaceId} revisionId={content.revisionId} />;
}
return null;
})();
if (!toolbar) {
return null;
}
return (
<div
className={tcls(
'fixed',
'bottom-5',
'left-1/2',
'z-50',
'transform',
'-translate-x-1/2',
'rounded-full',
'bg-dark-1/9',
'shadow-lg',
'min-h-10',
'min-w-40',
'p-2',
'max-w-md',
'border-dark-1',
'backdrop-blur-sm',
)}
>
<React.Suspense fallback={null}>{toolbar}</React.Suspense>
</div>
);
}
async function ChangeRequestToolbar(props: { spaceId: string; changeRequestId: string }) {
const { spaceId, changeRequestId } = props;
const changeRequest = await getChangeRequest(spaceId, changeRequestId);
return (
<ToolbarButton href={changeRequest.urls.app}>
Change request #{changeRequest.number}: {changeRequest.subject ?? 'No subject'}
</ToolbarButton>
);
}
async function RevisionToolbar(props: { spaceId: string; revisionId: string }) {
const { spaceId, revisionId } = props;
const revision = await getRevision(spaceId, revisionId, {
metadata: false,
});
return (
<ToolbarButton href={revision.urls.app}>
Revision created on {new Date(revision.createdAt).toLocaleDateString()}
</ToolbarButton>
);
}
function ToolbarButton(props: { href: string; children: React.ReactNode }) {
const { href, children } = props;
return (
<a
href={href}
className={tcls(
'block',
'text-sm',
'px-4',
'py-1',
'rounded-full',
'hover:bg-dark-1',
'hover:text-white',
'truncate',
'text-light',
'dark:text-light',
'dark:hover:bg-dark-2',
)}
>
{children}
</a>
);
}
@@ -0,0 +1 @@
export * from './AdminToolbar';
+200
View File
@@ -0,0 +1,200 @@
'use client';
import IconHeart from '@geist-ui/icons/heart';
import * as React from 'react';
import { ClassValue, tcls } from '@/lib/tailwind';
import { AdClassicRendering } from './AdClassicRendering';
import { AdCoverRendering } from './AdCoverRendering';
import { AdItem, AdsResponse } from './types';
/**
* Fetch and render the Ad placement.
* https://docs.buysellads.com/ad-serving-api
*/
export function Ad({
zoneId,
spaceId,
placement,
ignore,
style,
mode = 'auto',
}: {
zoneId: string;
spaceId: string;
placement: string;
ignore: boolean;
style?: ClassValue;
mode?: 'classic' | 'auto' | 'cover';
}) {
const containerRef = React.useRef<HTMLDivElement>(null);
const [visible, setVisible] = React.useState(false);
const [failed, setFailed] = React.useState(false);
const [ad, setAd] = React.useState<AdItem | undefined>(undefined);
// Observe the container visibility
React.useEffect(() => {
if (!containerRef.current) {
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
}
},
{
root: null,
rootMargin: '0px',
threshold: 0.1,
},
);
observer.observe(containerRef.current);
return () => {
observer.disconnect();
};
}, []);
// When the container is visible,
// track an impression on the ad and fetch it
React.useEffect(() => {
if (!visible) {
return;
}
let cancelled = false;
(async () => {
const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`);
url.searchParams.set('segment', `placement:${placement}`);
url.searchParams.set('v', 'true');
if (ignore) {
url.searchParams.set('ignore', 'true');
}
try {
const res = await fetch(url);
const json: AdsResponse = await res.json();
if (cancelled) {
return;
}
const first = json.ads[0];
if (first && 'active' in first) {
setAd(first);
}
} catch (error) {
console.error(
'Failed to fetch ad, it might have been blocked by a ad-blocker',
error,
);
setFailed(true);
}
})();
return () => {
cancelled = true;
};
}, [visible, zoneId, ignore, placement]);
const viaUrl = new URL('https://www.gitbook.com');
viaUrl.searchParams.set('utm_source', 'content');
viaUrl.searchParams.set('utm_medium', 'ads');
viaUrl.searchParams.set('utm_campaign', spaceId);
return (
<div ref={containerRef} className={tcls(style)}>
{ad && ad.description && ad.statlink ? (
<>
{mode === 'classic' || !('callToAction' in ad) ? (
<AdClassicRendering ad={ad} />
) : (
<AdCoverRendering ad={ad} />
)}
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
<p
className={tcls(
'mt-2',
'mr-2',
'text-xs',
'text-right',
'text-dark/5',
'dark:text-light/5',
)}
>
<a
target="_blank"
href={viaUrl.toString()}
className={tcls('hover:underline')}
>
Ads via GitBook
</a>
</p>
</>
) : failed ? (
<AdBlockerPlaceholder />
) : null}
</div>
);
}
/**
* Render attribution or verification pixels.
* https://docs.buysellads.com/ad-serving-api#pixels
*/
function AdPixels({ rawPixel }: { rawPixel: string }) {
const pixels = rawPixel.split('||');
const time = String(Math.round(Date.now() / 1e4) | 0);
return (
<div className={tcls('hidden')}>
{pixels.map((pixel, index) => {
return (
<img
key={index}
src={pixel.replace('[timestamp]', time)}
width="1"
height="1"
style={{ display: 'none' }}
alt="Ads tracking pixel"
/>
);
})}
</div>
);
}
/**
* Placeholder when visitor has an ad-blocker.
*/
function AdBlockerPlaceholder() {
return (
<div
className={tcls(
'flex',
'flex-col',
'gap-3',
'bg-light-2',
'text-dark/7',
'dark:bg-dark-2',
'dark:text-light/7',
'rounded-lg',
'p-4',
)}
>
<div className={tcls('flex', 'flex-row', 'gap-2', 'items-center')}>
<IconHeart className={tcls('w-4', 'h-4', 'text-primary-500')} />
<p className={tcls('text-xs', 'font-semibold')}>Ad disabled</p>
</div>
<p className={tcls('text-xs')}>
{`It looks like you're using an adblocker. Whitelist this site to help support this
project.`}
</p>
</div>
);
}
@@ -0,0 +1,47 @@
import * as React from 'react';
import { tcls } from '@/lib/tailwind';
import { AdItem } from './types';
/**
* Classic rendering for an ad.
*/
export function AdClassicRendering({ ad }: { ad: AdItem }) {
return (
<a
className={tcls(
'flex',
'flex-col',
'gap-4',
'bg-light-2',
'text-dark/7',
'dark:bg-dark-2',
'dark:text-light/7',
'hover:text-dark/9',
'dark:hover:text-light/9',
'rounded-lg',
'p-4',
)}
href={ad.statlink}
rel="sponsored noopener"
target="_blank"
>
{'smallImage' in ad ? (
<div>
<img alt="Ads logo" className={tcls('rounded-md')} src={ad.smallImage} />
</div>
) : (
<div
className={tcls('px-6', 'py-4', 'rounded-md')}
style={{ backgroundColor: ad.backgroundColor }}
>
<img alt="Ads logo" src={ad.logo} />
</div>
)}
<div className={tcls('flex', 'flex-col')}>
<div className={tcls('text-xs')}>{ad.description}</div>
</div>
</a>
);
}
@@ -0,0 +1,108 @@
import * as React from 'react';
import { hexToRgba } from '@/lib/colors';
import { tcls } from '@/lib/tailwind';
import { AdCover } from './types';
/**
* Cover rendering for an ad.
*/
export function AdCoverRendering({ ad }: { ad: AdCover }) {
return (
<a
className={tcls(
'group/ad',
'relative',
'flex',
'flex-col',
'gap-4',
'bg-light-2',
'text-dark/7',
'dark:bg-dark-2',
'dark:text-light/7',
'hover:text-dark/9',
'dark:hover:text-light/9',
'rounded-lg',
'p-4',
'overflow-hidden',
'shadow-sm',
)}
style={{ backgroundColor: ad.backgroundColor, color: ad.textColor ?? '#ffffff' }}
href={ad.statlink}
rel="sponsored noopener"
target="_blank"
>
<div
className={tcls(
'absolute',
'inset-0',
'bg-center',
'bg-cover',
'bg-no-repeat',
'z-0',
)}
style={{
backgroundImage: `url(${ad.largeImage})`,
}}
/>
<div className={tcls('z-[2]')}>
<img
alt="Large image"
src={ad.largeImage}
className={tcls(
'rounded-md',
'shadow-md',
'max-h-32',
'group-hover/ad:max-h-16',
'transition-all',
)}
/>
</div>
<div className={tcls('z-[2]')}>
<img alt={ad.company} src={ad.logo} className={tcls('max-w-36', 'max-h-12')} />
</div>
<div className={tcls('flex', 'flex-col', 'z-[2]')}>
<div className={tcls('text-sm', 'font-semibold', 'mb-2')}>{ad.companyTagline}</div>
<div
className={tcls(
'text-xs',
'h-0',
'opacity-0',
'group-hover/ad:h-16',
'group-hover/ad:opacity-10',
'transition-all',
)}
>
{ad.description}
</div>
</div>
<div className={tcls('z-[2]')}>
<span
className={tcls(
'text-sm',
'font-semibold',
'shadow-lg',
'rounded-md',
'bg-white',
'py-2',
'px-4',
)}
style={{
backgroundColor: ad.ctaBackgroundColor,
color: ad.ctaTextColor ?? ad.backgroundColor,
}}
>
{ad.callToAction}
</span>
</div>
<div
className={tcls('absolute', 'inset-0', 'backdrop-blur', 'z-[1]')}
style={{
backgroundColor: hexToRgba(ad.backgroundColor, 0.8),
}}
/>
</a>
);
}
@@ -0,0 +1 @@
export * from './Ad';
@@ -0,0 +1,51 @@
export interface AdGeneric {
active: string;
ad_via_link: string;
bannerid: string;
creativeid: string;
evenodd: string;
external_id: string;
height: string;
i: string;
identifier: string;
longimp: string;
longlink: string;
num_slots: string;
statimp: string;
statlink: string;
timestamp: string;
width: string;
zoneid: string;
zonekey: string;
rendering: 'carbon';
pixel?: string;
}
export interface AdClassic extends AdGeneric {
description: string;
smallImage: string;
}
export interface AdCover extends AdGeneric {
backgroundColor: string;
backgroundHoverColor?: string;
textColor?: string;
textColorHover?: string;
callToAction: string;
company: string;
companyTagline: string;
description: string;
largeImage: string;
image?: string;
logo: string;
ctaBackgroundColor?: string;
ctaBackgroundHoverColor?: string;
ctaTextColor?: string;
ctaTextColorHover?: string;
}
export type AdItem = AdClassic | AdCover;
export interface AdsResponse {
ads: Array<AdItem | {}>;
}
@@ -0,0 +1,121 @@
'use client';
import IconX from '@geist-ui/icons/x';
import * as React from 'react';
import { Button } from '@/components/primitives';
import { useLanguage } from '@/intl/client';
import { t, tString } from '@/intl/translate';
import { isCookiesTrackingDisabled, setCookiesTracking } from '@/lib/analytics';
import { tcls } from '@/lib/tailwind';
/**
* Toast to accept or reject the use of cookies.
*/
export function CookiesToast(props: { privacyPolicy?: string }) {
const { privacyPolicy = 'https://policies.gitbook.com/privacy/cookies' } = props;
const [show, setShow] = React.useState(false);
const language = useLanguage();
React.useEffect(() => {
setShow(isCookiesTrackingDisabled() === undefined);
}, []);
if (!show) {
return null;
}
const onUpdateState = (enabled: boolean) => {
setCookiesTracking(enabled);
// Reload the page to take the change in consideration
window.location.reload();
};
const describedById = 'cookies-description';
return (
<div
role="dialog"
data-testid="cookies-dialog"
aria-modal="true"
aria-label={tString(language, 'cookies_title')}
aria-describedby={describedById}
className={tcls(
'fixed',
'z-10',
'bg-white',
'rounded',
'straight-corners:rounded-none',
'ring-1',
'ring-dark/2',
'shadow-1xs',
'p-4',
'pr-8',
'bottom-4',
'right-4',
'left-16',
'max-w-md',
'text-balance',
'sm:left-auto',
'dark:ring-light/2',
'dark:bg-dark',
)}
>
<p id={describedById} className={tcls('text-sm')}>
{t(
language,
'cookies_prompt',
<a
href={privacyPolicy}
className={tcls('text-primary-500', 'hover:text-primary-600', 'underline')}
>
{t(language, 'cookies_prompt_privacy')}
</a>,
)}
</p>
<button
onClick={() => setShow(false)}
aria-label={tString(language, 'cookies_close')}
className={tcls(
'absolute',
'top-3',
'right-3',
'w-6',
'h-6',
'flex',
'justify-center',
'items-center',
'rounded-sm',
'hover:bg-dark/2',
'dark:hover:bg-light/1',
)}
title={tString(language, 'cookies_close')}
>
<IconX className={tcls('w-4')} />
</button>
<div className={tcls('mt-3', 'flex', 'flex-row', 'gap-2')}>
<Button
variant="primary"
size="small"
aria-label={tString(language, 'cookies_accept')}
onClick={() => {
onUpdateState(true);
}}
>
{t(language, 'cookies_accept')}
</Button>
<Button
variant="secondary"
size="small"
aria-label={tString(language, 'cookies_reject')}
onClick={() => {
onUpdateState(false);
}}
>
{t(language, 'cookies_reject')}
</Button>
</div>
</div>
);
}
@@ -0,0 +1 @@
export * from './CookiesToast';
@@ -0,0 +1,36 @@
import { DocumentInlineAnnotation } from '@gitbook/api';
import { getNodeFragmentByType } from '@/lib/document';
import { AnnotationPopover } from './AnnotationPopover';
import { Blocks } from '../Blocks';
import { InlineProps } from '../Inline';
import { Inlines } from '../Inlines';
export async function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
const { inline, context, document, children } = props;
const fragment = getNodeFragmentByType(inline, 'annotation-body');
const content = children ?? (
<Inlines document={document} context={context} nodes={inline.nodes} ancestorInlines={[]} />
);
if (!fragment) {
return <>{content}</>;
}
return (
<AnnotationPopover
body={
<Blocks
document={document}
ancestorBlocks={[]}
context={context}
nodes={fragment.nodes}
/>
}
>
{content}
</AnnotationPopover>
);
}

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