mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-06 21:33:25 +00:00
Export subtree PDFs in batches of 20 with previous/next navigation
This commit is contained in:
@@ -3,6 +3,13 @@ import { describe, expect, it } from 'bun:test';
|
||||
import { RevisionPageType } from '@gitbook/api';
|
||||
|
||||
import { createPDFLinker } from './linker';
|
||||
import { selectPages } from './selectPages';
|
||||
import {
|
||||
PDF_DEFAULT_LIMIT,
|
||||
PDF_SUBTREE_DEFAULT_LIMIT,
|
||||
getPDFSearchParams,
|
||||
getPDFURLSearchParams,
|
||||
} from './urls';
|
||||
import { createLinker, linkerWithAbsoluteURLs } from '@/lib/links';
|
||||
import { getLinkerForSiteSpace } from '@/lib/sites';
|
||||
|
||||
@@ -15,6 +22,10 @@ function createDocumentPage(id: string, path: string) {
|
||||
} as any;
|
||||
}
|
||||
|
||||
function createDocumentPageWithChildren(id: string, children: any[]) {
|
||||
return { ...createDocumentPage(id, id), pages: children };
|
||||
}
|
||||
|
||||
function createGroupPage(id: string, path: string) {
|
||||
return {
|
||||
id,
|
||||
@@ -208,3 +219,183 @@ describe('createPDFLinker', () => {
|
||||
).toBe('https://open-2v.gitbook.com/~space/HJ1ltuWFvsArFWtevnRn~gitbook/pdf/respond');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPDFLinker with batches', () => {
|
||||
const baseLinker = createLinker({
|
||||
host: 'open-2v.gitbook.com',
|
||||
siteBasePath: '/~space/HJ1ltuWFvsArFWtevnRn',
|
||||
spaceBasePath: '/~space/HJ1ltuWFvsArFWtevnRn',
|
||||
});
|
||||
const getBatchURL = (page: { id: string }) =>
|
||||
page.id === 'other-batch'
|
||||
? 'https://open-2v.gitbook.com/~gitbook/pdf?offset=20'
|
||||
: undefined;
|
||||
|
||||
it('prefers the published URL for documents exported in another batch', () => {
|
||||
const linker = createPDFLinker(
|
||||
baseLinker,
|
||||
[{ page: createDocumentPage('included', '') }],
|
||||
createPublishedLinker(),
|
||||
{ getBatchURL }
|
||||
);
|
||||
|
||||
expect(
|
||||
linker.toPathForPage({
|
||||
pages: [
|
||||
createDocumentPage('included', ''),
|
||||
createDocumentPage('other-batch', 'respond'),
|
||||
],
|
||||
page: createDocumentPage('other-batch', 'respond'),
|
||||
})
|
||||
).toBe('https://docs.vectra.ai/deployment/respond');
|
||||
});
|
||||
|
||||
it('links to the batch containing a page when there is no published URL', () => {
|
||||
const linker = createPDFLinker(
|
||||
baseLinker,
|
||||
[{ page: createDocumentPage('included', '') }],
|
||||
undefined,
|
||||
{ getBatchURL }
|
||||
);
|
||||
|
||||
expect(
|
||||
linker.toPathForPage({
|
||||
pages: [createDocumentPage('other-batch', 'respond')],
|
||||
page: createDocumentPage('other-batch', 'respond'),
|
||||
anchor: 'faq',
|
||||
})
|
||||
).toBe('https://open-2v.gitbook.com/~gitbook/pdf?offset=20#page-other-batch-faq');
|
||||
});
|
||||
|
||||
it('links group pages to the batch containing them', () => {
|
||||
const linker = createPDFLinker(
|
||||
baseLinker,
|
||||
[{ page: createDocumentPage('included', '') }],
|
||||
createPublishedLinker(),
|
||||
{ getBatchURL }
|
||||
);
|
||||
|
||||
expect(
|
||||
linker.toPathForPage({
|
||||
pages: [createGroupPage('other-batch', 'group')],
|
||||
page: createGroupPage('other-batch', 'group'),
|
||||
})
|
||||
).toBe('https://open-2v.gitbook.com/~gitbook/pdf?offset=20#page-other-batch');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPDFSearchParams', () => {
|
||||
it('defaults to a small batch for subtree exports', () => {
|
||||
expect(getPDFSearchParams(new URLSearchParams('page=abc&only=yes'))).toEqual({
|
||||
page: 'abc',
|
||||
only: true,
|
||||
limit: PDF_SUBTREE_DEFAULT_LIMIT,
|
||||
offset: 0,
|
||||
});
|
||||
expect(PDF_SUBTREE_DEFAULT_LIMIT).toBe(20);
|
||||
});
|
||||
|
||||
it('keeps the larger default for whole-space and single-page exports', () => {
|
||||
expect(getPDFSearchParams(new URLSearchParams('')).limit).toBe(PDF_DEFAULT_LIMIT);
|
||||
expect(getPDFSearchParams(new URLSearchParams('page=abc')).limit).toBe(PDF_DEFAULT_LIMIT);
|
||||
expect(PDF_DEFAULT_LIMIT).toBe(100);
|
||||
});
|
||||
|
||||
it('parses an explicit limit and offset', () => {
|
||||
const params = getPDFSearchParams(
|
||||
new URLSearchParams('page=abc&only=yes&limit=5&offset=10')
|
||||
);
|
||||
expect(params.limit).toBe(5);
|
||||
expect(params.offset).toBe(10);
|
||||
});
|
||||
|
||||
it('ignores invalid limit and offset values', () => {
|
||||
const params = getPDFSearchParams(
|
||||
new URLSearchParams('page=abc&only=yes&limit=nope&offset=-3')
|
||||
);
|
||||
expect(params.limit).toBe(PDF_SUBTREE_DEFAULT_LIMIT);
|
||||
expect(params.offset).toBe(0);
|
||||
expect(getPDFSearchParams(new URLSearchParams('limit=0&offset=1.5'))).toMatchObject({
|
||||
limit: PDF_DEFAULT_LIMIT,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('round-trips through getPDFURLSearchParams', () => {
|
||||
const params = { page: 'abc', only: true, limit: 20, offset: 40, back: 'false' };
|
||||
expect(getPDFSearchParams(getPDFURLSearchParams(params))).toEqual(params);
|
||||
});
|
||||
|
||||
it('omits offset and limit when not set', () => {
|
||||
expect(getPDFURLSearchParams({ page: 'abc', only: true }).toString()).toBe(
|
||||
'page=abc&only=yes'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectPages', () => {
|
||||
const children = Array.from({ length: 45 }, (_, index) =>
|
||||
createDocumentPage(`child-${index}`, `child-${index}`)
|
||||
);
|
||||
const rootPages = [createDocumentPageWithChildren('root', children)];
|
||||
const ids = (pages: { page: { id: string } }[] | undefined) =>
|
||||
(pages ?? []).map(({ page }) => page.id);
|
||||
const childIds = (from: number, to: number) => children.slice(from, to).map((page) => page.id);
|
||||
|
||||
it('renders the first batch of a subtree by default', () => {
|
||||
const selection = selectPages(
|
||||
rootPages,
|
||||
getPDFSearchParams(new URLSearchParams('page=root&only=yes'))
|
||||
);
|
||||
|
||||
expect(selection?.total).toBe(46);
|
||||
expect(selection?.offset).toBe(0);
|
||||
expect(ids(selection?.pages)).toEqual(['root', ...childIds(0, 19)]);
|
||||
expect(selection?.allPages).toHaveLength(46);
|
||||
});
|
||||
|
||||
it('slices the batch from offset to offset + limit', () => {
|
||||
const selection = selectPages(rootPages, {
|
||||
page: 'root',
|
||||
only: true,
|
||||
limit: 20,
|
||||
offset: 20,
|
||||
});
|
||||
|
||||
expect(selection?.offset).toBe(20);
|
||||
expect(ids(selection?.pages)).toEqual(childIds(19, 39));
|
||||
});
|
||||
|
||||
it('returns a shorter last batch', () => {
|
||||
const selection = selectPages(rootPages, {
|
||||
page: 'root',
|
||||
only: true,
|
||||
limit: 20,
|
||||
offset: 40,
|
||||
});
|
||||
|
||||
expect(ids(selection?.pages)).toEqual(childIds(39, 45));
|
||||
});
|
||||
|
||||
it('falls back to the last batch when offset is past the end', () => {
|
||||
const selection = selectPages(rootPages, {
|
||||
page: 'root',
|
||||
only: true,
|
||||
limit: 20,
|
||||
offset: 100,
|
||||
});
|
||||
|
||||
expect(selection?.offset).toBe(26);
|
||||
expect(selection?.pages).toHaveLength(20);
|
||||
});
|
||||
|
||||
it('only selects the page itself without only', () => {
|
||||
const selection = selectPages(rootPages, { page: 'root', limit: 100, offset: 0 });
|
||||
expect(ids(selection?.pages)).toEqual(['root']);
|
||||
expect(selection?.total).toBe(1);
|
||||
});
|
||||
|
||||
it('returns null for an unknown page', () => {
|
||||
expect(selectPages(rootPages, { page: 'missing', limit: 100, offset: 0 })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { notFound } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
type Revision,
|
||||
type RevisionPageDocument,
|
||||
type RevisionPageGroup,
|
||||
type SiteCustomizationSettings,
|
||||
@@ -16,7 +15,8 @@ import { ImagesLoadingStatus } from './ImagesLoadingStatus';
|
||||
import { createPDFLinker, getPagePDFContainerId } from './linker';
|
||||
import { PageControlButtons } from './PageControlButtons';
|
||||
import { PDFPrintControls } from './PDFPrintControls';
|
||||
import { type PDFSearchParams, getPDFSearchParams } from './urls';
|
||||
import { selectPages } from './selectPages';
|
||||
import { getPDFSearchParams, getPDFURLSearchParams } from './urls';
|
||||
import { DocumentView } from '@/components/DocumentView';
|
||||
import { Trademark } from '@/components/TableOfContents/Trademark';
|
||||
import type { PolymorphicComponentProp } from '@/components/utils/types';
|
||||
@@ -27,13 +27,10 @@ import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
|
||||
import { getPageDocument } from '@/lib/data';
|
||||
import { type GitBookLinker, createLinker, linkerWithAbsoluteURLs } from '@/lib/links';
|
||||
import './pdf.css';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { getLinkerForSiteSpace } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { defaultCustomization } from '@/lib/utils';
|
||||
|
||||
const DEFAULT_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Generate the metadata for the PDF page.
|
||||
*/
|
||||
@@ -62,13 +59,37 @@ export async function PDFPage(props: {
|
||||
const language = await getSpaceLanguage(baseContext);
|
||||
|
||||
// Compute the pages to render
|
||||
const { pages, total } = selectPages(baseContext.revision.pages, pdfParams);
|
||||
const selection = selectPages(baseContext.revision.pages, pdfParams);
|
||||
if (!selection) {
|
||||
notFound();
|
||||
}
|
||||
const { pages, allPages, offset, total } = selection;
|
||||
const batchParams = { ...pdfParams, offset };
|
||||
const pageIds = pages.map(
|
||||
({ page }) => [page.id, getPagePDFContainerId(page)] as [string, string]
|
||||
);
|
||||
|
||||
// Build a linker that create anchor links for the pages rendered in the PDF page.
|
||||
const linker = createPDFLinker(baseContext.linker, pages, getPublishedLinker(baseContext));
|
||||
const pageIndexes = new Map(allPages.map(({ page }, index) => [page.id, index]));
|
||||
const linker = createPDFLinker(baseContext.linker, pages, getPublishedLinker(baseContext), {
|
||||
getBatchURL: (page) => {
|
||||
const index = pageIndexes.get(page.id);
|
||||
if (index === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Align on the current batch so these links match the previous/next controls.
|
||||
const batchOffset = Math.max(
|
||||
0,
|
||||
offset + Math.floor((index - offset) / pdfParams.limit) * pdfParams.limit
|
||||
);
|
||||
return baseContext.linker.toAbsoluteURL(
|
||||
baseContext.linker.toPathInSpace(
|
||||
`~gitbook/pdf?${getPDFURLSearchParams({ ...pdfParams, offset: batchOffset }).toString()}`
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const context: GitBookSpaceContext = {
|
||||
...baseContext,
|
||||
@@ -113,7 +134,7 @@ export async function PDFPage(props: {
|
||||
</div>
|
||||
|
||||
<PageControlButtons
|
||||
params={pdfParams}
|
||||
params={batchParams}
|
||||
pageIds={pageIds}
|
||||
total={total}
|
||||
trademark={
|
||||
@@ -126,7 +147,7 @@ export async function PDFPage(props: {
|
||||
}
|
||||
/>
|
||||
|
||||
{pdfParams.only ? null : (
|
||||
{pdfParams.only || offset > 0 ? null : (
|
||||
<PDFSpaceIntro space={context.space} customization={customization} />
|
||||
)}
|
||||
{pages.map(({ page }) =>
|
||||
@@ -273,67 +294,3 @@ function PrintPage(
|
||||
</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 !== 'document') {
|
||||
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 !== 'document' && page.type !== 'group') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (page.hidden) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flattenPage(page, 0);
|
||||
});
|
||||
return limitTo(allPages);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import { Button } from '@/components/primitives';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
const limitExtend = 50;
|
||||
|
||||
/**
|
||||
* Dynamic controls to show active page and to let the user select between modes.
|
||||
*/
|
||||
@@ -19,7 +17,7 @@ export function PageControlButtons(props: {
|
||||
params: PDFSearchParams;
|
||||
/** Array of the [pageId, divId] */
|
||||
pageIds: [string, string][];
|
||||
/** Total number of pages targetted by the generation */
|
||||
/** Total number of pages targetted by the generation, across all batches */
|
||||
total: number;
|
||||
/** Trademark to display */
|
||||
trademark?: React.ReactNode;
|
||||
@@ -38,6 +36,10 @@ export function PageControlButtons(props: {
|
||||
const activeIndex = (activeDivId ? divIds.indexOf(activeDivId) : 0) + 1;
|
||||
const activePageId = pageIds[activeIndex - 1]?.[0];
|
||||
|
||||
const batchEnd = params.offset + pageIds.length;
|
||||
const previousCount = Math.min(params.limit, params.offset);
|
||||
const nextCount = Math.min(params.limit, total - batchEnd);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -58,6 +60,8 @@ export function PageControlButtons(props: {
|
||||
...params,
|
||||
page: activePageId,
|
||||
only: true,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
}).toString()}`}
|
||||
variant="secondary"
|
||||
label={tString(language, 'pdf_mode_only_page')}
|
||||
@@ -68,6 +72,8 @@ export function PageControlButtons(props: {
|
||||
...params,
|
||||
page: undefined,
|
||||
only: false,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
}).toString()}`}
|
||||
variant="secondary"
|
||||
label={tString(language, 'pdf_mode_all')}
|
||||
@@ -89,7 +95,7 @@ export function PageControlButtons(props: {
|
||||
'z-50'
|
||||
)}
|
||||
>
|
||||
{total !== pageIds.length ? (
|
||||
{previousCount > 0 || nextCount > 0 ? (
|
||||
<div
|
||||
role="banner"
|
||||
className={tcls(
|
||||
@@ -112,19 +118,32 @@ export function PageControlButtons(props: {
|
||||
className={tcls('size-6', 'mr-3', 'mt-1')}
|
||||
/>{' '}
|
||||
<div>
|
||||
<div>{t(language, 'pdf_limit_reached', total, pageIds.length)}</div>
|
||||
<div>
|
||||
<a
|
||||
href={`?${getPDFURLSearchParams({
|
||||
...params,
|
||||
page: undefined,
|
||||
only: false,
|
||||
limit: params.limit + limitExtend,
|
||||
}).toString()}`}
|
||||
className={tcls('underline')}
|
||||
>
|
||||
{t(language, 'pdf_limit_reached_continue', limitExtend)}
|
||||
</a>
|
||||
{t(language, 'pdf_batch_range', params.offset + 1, batchEnd, total)}
|
||||
</div>
|
||||
<div className={tcls('flex', 'flex-row', 'flex-wrap', 'gap-x-3')}>
|
||||
{previousCount > 0 ? (
|
||||
<a
|
||||
href={`?${getPDFURLSearchParams({
|
||||
...params,
|
||||
offset: params.offset - previousCount,
|
||||
}).toString()}`}
|
||||
className={tcls('underline')}
|
||||
>
|
||||
{t(language, 'pdf_batch_previous', previousCount)}
|
||||
</a>
|
||||
) : null}
|
||||
{nextCount > 0 ? (
|
||||
<a
|
||||
href={`?${getPDFURLSearchParams({
|
||||
...params,
|
||||
offset: batchEnd,
|
||||
}).toString()}`}
|
||||
className={tcls('underline')}
|
||||
>
|
||||
{t(language, 'pdf_batch_next', nextCount)}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,7 +165,7 @@ export function PageControlButtons(props: {
|
||||
'border'
|
||||
)}
|
||||
>
|
||||
{t(language, 'pdf_page_of', activeIndex, pageIds.length)}
|
||||
{t(language, 'pdf_page_of', params.offset + activeIndex, total)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -20,36 +20,39 @@ export function getPagePDFContainerId(
|
||||
/**
|
||||
* Create a custom linker for PDF exports.
|
||||
*
|
||||
* This linker generates in-document anchor links for pages that are included
|
||||
* in the current PDF export, using `getPagePDFContainerId` to build the
|
||||
* target element ID. For pages that are not part of the exported PDF, it
|
||||
* falls back to URLs pointing to the published site if `publishedLinker` is
|
||||
* provided, preserving navigability for external content. Otherwise, it uses
|
||||
* absolute URLs from the base linker.
|
||||
*
|
||||
* @param baseLinker - The base GitBook linker used to resolve standard paths and URLs.
|
||||
* @param pages - The list of pages that are included in the current PDF export.
|
||||
* @param publishedLinker - Optional linker for the published space for external page links.
|
||||
* @returns A `GitBookLinker` configured to generate PDF-friendly links.
|
||||
* Pages rendered in the current batch get in-document anchors. Other pages link to the
|
||||
* published site when possible; pages exported in another batch otherwise link to that batch.
|
||||
*/
|
||||
export function createPDFLinker(
|
||||
baseLinker: GitBookLinker,
|
||||
pages: { page: Revision['pages'][number] }[],
|
||||
publishedLinker?: GitBookLinker
|
||||
publishedLinker?: GitBookLinker,
|
||||
options: {
|
||||
/** URL of the export batch rendering a page, for pages outside the current batch. */
|
||||
getBatchURL?: (page: RevisionPageDocument | RevisionPageGroup) => string | undefined;
|
||||
} = {}
|
||||
): GitBookLinker {
|
||||
const pageIds = new Set(pages.map((p) => p.page.id));
|
||||
|
||||
return {
|
||||
...baseLinker,
|
||||
toPathForPage(input) {
|
||||
if (pages.some((p) => p.page.id === input.page.id)) {
|
||||
if (pageIds.has(input.page.id)) {
|
||||
return `#${getPagePDFContainerId(input.page, input.anchor)}`;
|
||||
}
|
||||
if (input.page.type === RevisionPageType.Group) {
|
||||
return '#';
|
||||
|
||||
// A published page outlives the export URL, so prefer it for documents.
|
||||
if (input.page.type !== RevisionPageType.Group && publishedLinker) {
|
||||
return publishedLinker.toPathForPage(input);
|
||||
}
|
||||
|
||||
// For pages that are not embedded in this PDF export, keep links on the published site.
|
||||
if (publishedLinker) {
|
||||
return publishedLinker.toPathForPage(input);
|
||||
const batchURL = options.getBatchURL?.(input.page);
|
||||
if (batchURL) {
|
||||
return `${batchURL}#${getPagePDFContainerId(input.page, input.anchor)}`;
|
||||
}
|
||||
|
||||
if (input.page.type === RevisionPageType.Group) {
|
||||
return '#';
|
||||
}
|
||||
|
||||
return baseLinker.toAbsoluteURL(baseLinker.toPathForPage(input));
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { Revision, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
|
||||
|
||||
import type { PDFSearchParams } from './urls';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
|
||||
export type FlatPageEntry = { page: RevisionPageDocument | RevisionPageGroup; depth: number };
|
||||
|
||||
export interface PDFPageSelection {
|
||||
/** Pages rendered in the current batch */
|
||||
pages: FlatPageEntry[];
|
||||
/** Every page targeted by the export, across all batches */
|
||||
allPages: FlatPageEntry[];
|
||||
/** Index in `allPages` of the first page of the batch */
|
||||
offset: number;
|
||||
/** Total number of pages targeted by the export */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the ordered flat set of pages targeted by the export, and the batch to render.
|
||||
* Returns `null` if the requested page doesn't exist.
|
||||
*/
|
||||
export function selectPages(
|
||||
rootPages: Revision['pages'],
|
||||
params: Pick<PDFSearchParams, 'page' | 'only' | 'limit' | 'offset'>
|
||||
): PDFPageSelection | null {
|
||||
const allPages = selectAllPages(rootPages, params);
|
||||
if (!allPages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const total = allPages.length;
|
||||
// Fall back to the last batch rather than rendering an empty one.
|
||||
const offset = params.offset < total ? params.offset : Math.max(0, total - params.limit);
|
||||
|
||||
return {
|
||||
pages: allPages.slice(offset, offset + params.limit),
|
||||
allPages,
|
||||
offset,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
function selectAllPages(
|
||||
rootPages: Revision['pages'],
|
||||
params: Pick<PDFSearchParams, 'page' | 'only'>
|
||||
): FlatPageEntry[] | null {
|
||||
if (params.page) {
|
||||
const found = resolvePageId(rootPages, params.page);
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!params.only) {
|
||||
return [{ page: found.page, depth: 0 }];
|
||||
}
|
||||
|
||||
return flattenPage(found.page, 0);
|
||||
}
|
||||
|
||||
return rootPages.flatMap((page) => {
|
||||
if (page.type !== 'document' && page.type !== 'group') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (page.hidden) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flattenPage(page, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function flattenPage(
|
||||
page: RevisionPageDocument | RevisionPageGroup,
|
||||
depth: number
|
||||
): FlatPageEntry[] {
|
||||
return [
|
||||
{ page, depth },
|
||||
...page.pages.flatMap((child) => {
|
||||
if (child.type !== 'document') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (child.hidden) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flattenPage(child, depth + 1);
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -1,33 +1,61 @@
|
||||
const DEFAULT_LIMIT = 100;
|
||||
/** Default number of pages rendered per request when exporting the whole space. */
|
||||
export const PDF_DEFAULT_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Default number of pages rendered per request when exporting a page and its subtree.
|
||||
* Subtrees are often OpenAPI references, which are much heavier to render than prose
|
||||
* and get the request killed for CPU/memory at higher limits.
|
||||
*/
|
||||
export const PDF_SUBTREE_DEFAULT_LIMIT = 20;
|
||||
|
||||
export interface PDFSearchParams {
|
||||
/** Page to export. If none is passed, all pages are exported. */
|
||||
page?: string;
|
||||
/** If true, only the `page` is exported, and not its descendant */
|
||||
/** If true, the `page` is exported with its descendants */
|
||||
only?: boolean;
|
||||
/** Limit the number of pages */
|
||||
/** Maximum number of pages to render in this batch */
|
||||
limit: number;
|
||||
/** Index of the first page to render in this batch */
|
||||
offset: number;
|
||||
/** URL to redirect back to */
|
||||
back?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Params accepted when building a PDF export URL. `limit` and `offset` fall back to the defaults.
|
||||
*/
|
||||
export type PDFURLParams = Omit<PDFSearchParams, 'limit' | 'offset'> & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default batch size for a given PDF export.
|
||||
*/
|
||||
export function getDefaultPDFLimit(params: Pick<PDFSearchParams, 'page' | 'only'>): number {
|
||||
return params.page && params.only ? PDF_SUBTREE_DEFAULT_LIMIT : PDF_DEFAULT_LIMIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF export params from the URL serch params.
|
||||
*/
|
||||
export function getPDFSearchParams(searchParams: URLSearchParams): PDFSearchParams {
|
||||
const page = searchParams.has('page') ? (searchParams.get('page') ?? '') : undefined;
|
||||
const only = searchParams.has('only') ? true : undefined;
|
||||
|
||||
const params: PDFSearchParams = {
|
||||
limit: DEFAULT_LIMIT,
|
||||
limit:
|
||||
parseInteger(searchParams.get('limit'), { min: 1 }) ??
|
||||
getDefaultPDFLimit({ page, only }),
|
||||
offset: parseInteger(searchParams.get('offset'), { min: 0 }) ?? 0,
|
||||
};
|
||||
|
||||
if (searchParams.has('page')) {
|
||||
params.page = searchParams.get('page') ?? '';
|
||||
if (page !== undefined) {
|
||||
params.page = page;
|
||||
}
|
||||
if (searchParams.has('only')) {
|
||||
if (only) {
|
||||
params.only = true;
|
||||
}
|
||||
if (searchParams.has('limit')) {
|
||||
params.limit = Number(searchParams.get('limit'));
|
||||
}
|
||||
if (searchParams.has('back')) {
|
||||
params.back = searchParams.get('back') ?? '';
|
||||
}
|
||||
@@ -39,7 +67,7 @@ export function getPDFSearchParams(searchParams: URLSearchParams): PDFSearchPara
|
||||
* Get the URL search params to use for a PDF export.
|
||||
*/
|
||||
export function getPDFURLSearchParams(
|
||||
params: PDFSearchParams,
|
||||
params: PDFURLParams,
|
||||
searchParams = new URLSearchParams({})
|
||||
): URLSearchParams {
|
||||
if (params?.page) {
|
||||
@@ -53,9 +81,16 @@ export function getPDFURLSearchParams(
|
||||
searchParams.delete('only');
|
||||
}
|
||||
|
||||
// Persist limit and back
|
||||
// Persist limit, offset and back
|
||||
if (params?.limit) {
|
||||
searchParams.set('limit', String(params.limit));
|
||||
} else {
|
||||
searchParams.delete('limit');
|
||||
}
|
||||
if (params?.offset) {
|
||||
searchParams.set('offset', String(params.offset));
|
||||
} else {
|
||||
searchParams.delete('offset');
|
||||
}
|
||||
if (params?.back) {
|
||||
searchParams.set('back', String(params.back));
|
||||
@@ -63,3 +98,12 @@ export function getPDFURLSearchParams(
|
||||
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
function parseInteger(value: string | null, options: { min: number }): number | undefined {
|
||||
if (value === null || value.trim() === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed >= options.min ? parsed : undefined;
|
||||
}
|
||||
|
||||
@@ -533,7 +533,6 @@ function getPageActionsURLs({
|
||||
`~gitbook/pdf?${getPDFURLSearchParams({
|
||||
page: page.id,
|
||||
only: true,
|
||||
limit: 100,
|
||||
}).toString()}`
|
||||
)
|
||||
: undefined,
|
||||
|
||||
@@ -96,8 +96,9 @@ export const ar: TranslationLanguage = {
|
||||
pdf_page_of: '${1} من ${2}',
|
||||
pdf_mode_only_page: 'هذه الصفحة فقط',
|
||||
pdf_mode_all: 'كل الصفحات',
|
||||
pdf_limit_reached: 'تعذر إنشاء ملف PDF لـ ${1} صفحات، وتوقف الإنشاء عند ${2}.',
|
||||
pdf_limit_reached_continue: 'التوسيع بإضافة ${1} صفحات أخرى.',
|
||||
pdf_batch_range: 'عرض الصفحات ${1}–${2} من ${3}.',
|
||||
pdf_batch_previous: 'الصفحات الـ ${1} السابقة',
|
||||
pdf_batch_next: 'الصفحات الـ ${1} التالية',
|
||||
more: 'المزيد',
|
||||
link_tooltip_external_link: 'رابط خارجي إلى',
|
||||
link_tooltip_email: 'إرسال بريد إلكتروني إلى',
|
||||
|
||||
@@ -99,9 +99,9 @@ export const bg: TranslationLanguage = {
|
||||
pdf_page_of: '${1} от ${2}',
|
||||
pdf_mode_only_page: 'Само тази страница',
|
||||
pdf_mode_all: 'Всички страници',
|
||||
pdf_limit_reached:
|
||||
'PDF файлът за ${1} страници не можа да бъде генериран, генерирането спря на ${2}.',
|
||||
pdf_limit_reached_continue: 'Разширяване с още ${1} страници.',
|
||||
pdf_batch_range: 'Показани са страници ${1}–${2} от ${3}.',
|
||||
pdf_batch_previous: 'Предишни ${1} страници',
|
||||
pdf_batch_next: 'Следващи ${1} страници',
|
||||
more: 'Още',
|
||||
link_tooltip_external_link: 'Външна връзка към',
|
||||
link_tooltip_email: 'Изпращане на имейл до',
|
||||
|
||||
@@ -97,9 +97,9 @@ export const cs: TranslationLanguage = {
|
||||
pdf_page_of: '${1} z ${2}',
|
||||
pdf_mode_only_page: 'Pouze tato stránka',
|
||||
pdf_mode_all: 'Všechny stránky',
|
||||
pdf_limit_reached:
|
||||
'Nepodařilo se vygenerovat PDF pro ${1} stránek, generování se zastavilo na ${2}.',
|
||||
pdf_limit_reached_continue: 'Rozšířit o dalších ${1} stránek.',
|
||||
pdf_batch_range: 'Zobrazeny stránky ${1}–${2} z ${3}.',
|
||||
pdf_batch_previous: 'Předchozích ${1} stránek',
|
||||
pdf_batch_next: 'Dalších ${1} stránek',
|
||||
more: 'Více',
|
||||
link_tooltip_external_link: 'Externí odkaz na',
|
||||
link_tooltip_email: 'Odeslat e-mail na',
|
||||
|
||||
@@ -97,8 +97,9 @@ export const da: TranslationLanguage = {
|
||||
pdf_page_of: '${1} af ${2}',
|
||||
pdf_mode_only_page: 'Kun denne side',
|
||||
pdf_mode_all: 'Alle sider',
|
||||
pdf_limit_reached: 'Kunne ikke generere PDF for ${1} sider, genereringen stoppede ved ${2}.',
|
||||
pdf_limit_reached_continue: 'Udvid med ${1} flere sider.',
|
||||
pdf_batch_range: 'Viser sider ${1}–${2} af ${3}.',
|
||||
pdf_batch_previous: 'Forrige ${1} sider',
|
||||
pdf_batch_next: 'Næste ${1} sider',
|
||||
more: 'Mere',
|
||||
link_tooltip_external_link: 'Eksternt link til',
|
||||
link_tooltip_email: 'Send e-mail til',
|
||||
|
||||
@@ -102,9 +102,9 @@ export const de: TranslationLanguage = {
|
||||
pdf_page_of: '${1} von ${2}',
|
||||
pdf_mode_only_page: 'Nur diese Seite',
|
||||
pdf_mode_all: 'Alle Seiten',
|
||||
pdf_limit_reached:
|
||||
'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.',
|
||||
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
|
||||
pdf_batch_range: 'Seiten ${1}–${2} von ${3}.',
|
||||
pdf_batch_previous: 'Vorherige ${1} Seiten',
|
||||
pdf_batch_next: 'Nächste ${1} Seiten',
|
||||
more: 'Mehr',
|
||||
link_tooltip_external_link: 'Externer Link zu',
|
||||
link_tooltip_email: 'E-Mail senden an',
|
||||
|
||||
@@ -100,9 +100,9 @@ export const el: TranslationLanguage = {
|
||||
pdf_page_of: '${1} από ${2}',
|
||||
pdf_mode_only_page: 'Μόνο αυτή η σελίδα',
|
||||
pdf_mode_all: 'Όλες οι σελίδες',
|
||||
pdf_limit_reached:
|
||||
'Δεν ήταν δυνατή η δημιουργία PDF για ${1} σελίδες, η δημιουργία σταμάτησε στο ${2}.',
|
||||
pdf_limit_reached_continue: 'Επέκταση με ${1} ακόμη σελίδες.',
|
||||
pdf_batch_range: 'Εμφάνιση σελίδων ${1}–${2} από ${3}.',
|
||||
pdf_batch_previous: 'Προηγούμενες ${1} σελίδες',
|
||||
pdf_batch_next: 'Επόμενες ${1} σελίδες',
|
||||
more: 'Περισσότερα',
|
||||
link_tooltip_external_link: 'Εξωτερικός σύνδεσμος προς',
|
||||
link_tooltip_email: 'Αποστολή email σε',
|
||||
|
||||
@@ -95,8 +95,9 @@ export const en = {
|
||||
pdf_page_of: '${1} of ${2}',
|
||||
pdf_mode_only_page: 'Only this page',
|
||||
pdf_mode_all: 'All pages',
|
||||
pdf_limit_reached: "Couldn't generate the PDF for ${1} pages, generation stopped at ${2}.",
|
||||
pdf_limit_reached_continue: 'Extend with ${1} more pages.',
|
||||
pdf_batch_range: 'Showing pages ${1}–${2} of ${3}.',
|
||||
pdf_batch_previous: 'Previous ${1} pages',
|
||||
pdf_batch_next: 'Next ${1} pages',
|
||||
more: 'More',
|
||||
link_tooltip_external_link: 'External link to',
|
||||
link_tooltip_email: 'Send an email to',
|
||||
|
||||
@@ -101,9 +101,9 @@ export const es: TranslationLanguage = {
|
||||
pdf_page_of: '${1} de ${2}',
|
||||
pdf_mode_only_page: 'Solo esta página',
|
||||
pdf_mode_all: 'Todas las páginas',
|
||||
pdf_limit_reached:
|
||||
'No se pudo generar el PDF para ${1} páginas, la generación se detuvo en ${2}.',
|
||||
pdf_limit_reached_continue: 'Extender con ${1} páginas más.',
|
||||
pdf_batch_range: 'Mostrando las páginas ${1}–${2} de ${3}.',
|
||||
pdf_batch_previous: '${1} páginas anteriores',
|
||||
pdf_batch_next: '${1} páginas siguientes',
|
||||
more: 'Más',
|
||||
link_tooltip_external_link: 'Enlace externo a',
|
||||
link_tooltip_email: 'Enviar un correo electrónico a',
|
||||
|
||||
@@ -97,8 +97,9 @@ export const et: TranslationLanguage = {
|
||||
pdf_page_of: '${1}/${2}',
|
||||
pdf_mode_only_page: 'Ainult see leht',
|
||||
pdf_mode_all: 'Kõik lehed',
|
||||
pdf_limit_reached: 'PDF-i ei saanud luua ${1} lehe jaoks, loomine peatus kohas ${2}.',
|
||||
pdf_limit_reached_continue: 'Laienda veel ${1} lehe võrra.',
|
||||
pdf_batch_range: 'Kuvatakse lehti ${1}–${2}/${3}.',
|
||||
pdf_batch_previous: 'Eelmised ${1} lehte',
|
||||
pdf_batch_next: 'Järgmised ${1} lehte',
|
||||
more: 'Rohkem',
|
||||
link_tooltip_external_link: 'Väline link aadressile',
|
||||
link_tooltip_email: 'Saada e-kiri aadressile',
|
||||
|
||||
@@ -99,8 +99,9 @@ export const fi: TranslationLanguage = {
|
||||
pdf_page_of: '${1}/${2}',
|
||||
pdf_mode_only_page: 'Vain tämä sivu',
|
||||
pdf_mode_all: 'Kaikki sivut',
|
||||
pdf_limit_reached: 'PDF-tiedostoa ei voitu luoda ${1} sivulle, luonti pysähtyi kohtaan ${2}.',
|
||||
pdf_limit_reached_continue: 'Laajenna ${1} sivulla.',
|
||||
pdf_batch_range: 'Näytetään sivut ${1}–${2}/${3}.',
|
||||
pdf_batch_previous: 'Edelliset ${1} sivua',
|
||||
pdf_batch_next: 'Seuraavat ${1} sivua',
|
||||
more: 'Lisää',
|
||||
link_tooltip_external_link: 'Ulkoinen linkki kohteeseen',
|
||||
link_tooltip_email: 'Lähetä sähköpostia osoitteeseen',
|
||||
|
||||
@@ -98,8 +98,9 @@ export const fr: TranslationLanguage = {
|
||||
pdf_page_of: '${1} sur ${2}',
|
||||
pdf_mode_only_page: 'Cette page uniquement',
|
||||
pdf_mode_all: 'Toutes les pages',
|
||||
pdf_limit_reached: 'Impossible de générer le PDF pour ${1} pages, arrêté à ${2}.',
|
||||
pdf_limit_reached_continue: 'Ajouter ${1} pages supplémentaires',
|
||||
pdf_batch_range: 'Pages ${1} à ${2} sur ${3}.',
|
||||
pdf_batch_previous: '${1} pages précédentes',
|
||||
pdf_batch_next: '${1} pages suivantes',
|
||||
more: 'Plus',
|
||||
link_tooltip_external_link: 'Lien externe vers',
|
||||
link_tooltip_email: 'Envoyer un e-mail à',
|
||||
|
||||
@@ -96,8 +96,9 @@ export const he: TranslationLanguage = {
|
||||
pdf_page_of: '${1} מתוך ${2}',
|
||||
pdf_mode_only_page: 'רק הדף הזה',
|
||||
pdf_mode_all: 'כל הדפים',
|
||||
pdf_limit_reached: 'לא ניתן ליצור PDF עבור ${1} דפים, היצירה נעצרה ב-${2}.',
|
||||
pdf_limit_reached_continue: 'הרחבה עם עוד ${1} דפים.',
|
||||
pdf_batch_range: 'מוצגים דפים ${1}–${2} מתוך ${3}.',
|
||||
pdf_batch_previous: '${1} הדפים הקודמים',
|
||||
pdf_batch_next: '${1} הדפים הבאים',
|
||||
more: 'עוד',
|
||||
link_tooltip_external_link: 'קישור חיצוני אל',
|
||||
link_tooltip_email: 'שליחת אימייל אל',
|
||||
|
||||
@@ -96,8 +96,9 @@ export const hi: TranslationLanguage = {
|
||||
pdf_page_of: '${1} में से ${2}',
|
||||
pdf_mode_only_page: 'केवल यह पृष्ठ',
|
||||
pdf_mode_all: 'सभी पृष्ठ',
|
||||
pdf_limit_reached: '${1} पृष्ठों के लिए PDF नहीं बन सका, जनरेशन ${2} पर रुक गया।',
|
||||
pdf_limit_reached_continue: '${1} और पृष्ठों के साथ बढ़ाएं।',
|
||||
pdf_batch_range: '${3} में से पृष्ठ ${1}–${2} दिखाए जा रहे हैं।',
|
||||
pdf_batch_previous: 'पिछले ${1} पृष्ठ',
|
||||
pdf_batch_next: 'अगले ${1} पृष्ठ',
|
||||
more: 'अधिक',
|
||||
link_tooltip_external_link: 'बाहरी लिंक',
|
||||
link_tooltip_email: 'ईमेल भेजें',
|
||||
|
||||
@@ -97,9 +97,9 @@ export const hr: TranslationLanguage = {
|
||||
pdf_page_of: '${1} od ${2}',
|
||||
pdf_mode_only_page: 'Samo ova stranica',
|
||||
pdf_mode_all: 'Sve stranice',
|
||||
pdf_limit_reached:
|
||||
'Nije moguće generirati PDF za ${1} stranica, generiranje je zaustavljeno na ${2}.',
|
||||
pdf_limit_reached_continue: 'Proširi za još ${1} stranica.',
|
||||
pdf_batch_range: 'Prikazane stranice ${1}–${2} od ${3}.',
|
||||
pdf_batch_previous: 'Prethodnih ${1} stranica',
|
||||
pdf_batch_next: 'Sljedećih ${1} stranica',
|
||||
more: 'Više',
|
||||
link_tooltip_external_link: 'Vanjska poveznica na',
|
||||
link_tooltip_email: 'Pošalji e-poštu na',
|
||||
|
||||
@@ -97,9 +97,9 @@ export const hu: TranslationLanguage = {
|
||||
pdf_page_of: '${1} / ${2}',
|
||||
pdf_mode_only_page: 'Csak ez az oldal',
|
||||
pdf_mode_all: 'Minden oldal',
|
||||
pdf_limit_reached:
|
||||
'Nem sikerült PDF-et létrehozni ${1} oldalhoz, a létrehozás itt megállt: ${2}.',
|
||||
pdf_limit_reached_continue: 'Bővítés további ${1} oldallal.',
|
||||
pdf_batch_range: '${1}–${2}. oldal, összesen ${3}.',
|
||||
pdf_batch_previous: 'Előző ${1} oldal',
|
||||
pdf_batch_next: 'Következő ${1} oldal',
|
||||
more: 'Továbbiak',
|
||||
link_tooltip_external_link: 'Külső hivatkozás ide:',
|
||||
link_tooltip_email: 'E-mail küldése ide:',
|
||||
|
||||
@@ -97,8 +97,9 @@ export const id: TranslationLanguage = {
|
||||
pdf_page_of: '${1} dari ${2}',
|
||||
pdf_mode_only_page: 'Hanya halaman ini',
|
||||
pdf_mode_all: 'Semua halaman',
|
||||
pdf_limit_reached: 'Tidak dapat membuat PDF untuk ${1} halaman, pembuatan berhenti di ${2}.',
|
||||
pdf_limit_reached_continue: 'Perluas dengan ${1} halaman lagi.',
|
||||
pdf_batch_range: 'Menampilkan halaman ${1}–${2} dari ${3}.',
|
||||
pdf_batch_previous: '${1} halaman sebelumnya',
|
||||
pdf_batch_next: '${1} halaman berikutnya',
|
||||
more: 'Lainnya',
|
||||
link_tooltip_external_link: 'Tautan eksternal ke',
|
||||
link_tooltip_email: 'Kirim email ke',
|
||||
|
||||
@@ -100,9 +100,9 @@ export const it: TranslationLanguage = {
|
||||
pdf_page_of: '${1} di ${2}',
|
||||
pdf_mode_only_page: 'Solo questa pagina',
|
||||
pdf_mode_all: 'Tutte le pagine',
|
||||
pdf_limit_reached:
|
||||
'Impossibile generare il PDF per ${1} pagine, la generazione si è fermata a ${2}.',
|
||||
pdf_limit_reached_continue: 'Estendi con altre ${1} pagine.',
|
||||
pdf_batch_range: 'Pagine ${1}–${2} di ${3}.',
|
||||
pdf_batch_previous: '${1} pagine precedenti',
|
||||
pdf_batch_next: '${1} pagine successive',
|
||||
more: 'Altro',
|
||||
link_tooltip_external_link: 'Link esterno a',
|
||||
link_tooltip_email: "Invia un'email a",
|
||||
|
||||
@@ -98,8 +98,9 @@ export const ja: TranslationLanguage = {
|
||||
pdf_page_of: '${1} / ${2}',
|
||||
pdf_mode_only_page: 'このページのみ',
|
||||
pdf_mode_all: '全てのページ',
|
||||
pdf_limit_reached: '${1}ページのPDFを生成できませんでした、${2}で生成が停止しました。',
|
||||
pdf_limit_reached_continue: 'さらに${1}ページで拡張',
|
||||
pdf_batch_range: '${3}ページ中 ${1}–${2}ページを表示しています。',
|
||||
pdf_batch_previous: '前の${1}ページ',
|
||||
pdf_batch_next: '次の${1}ページ',
|
||||
more: 'もっと見る',
|
||||
link_tooltip_external_link: '外部リンクへ',
|
||||
link_tooltip_email: 'メールを送信',
|
||||
|
||||
@@ -97,8 +97,9 @@ export const ko: TranslationLanguage = {
|
||||
pdf_page_of: '${1} / ${2}',
|
||||
pdf_mode_only_page: '이 페이지만',
|
||||
pdf_mode_all: '모든 페이지',
|
||||
pdf_limit_reached: '${1}페이지에 대한 PDF를 생성할 수 없어 ${2}에서 생성이 중지되었습니다.',
|
||||
pdf_limit_reached_continue: '${1}페이지 더 확장하기.',
|
||||
pdf_batch_range: '전체 ${3}페이지 중 ${1}–${2}페이지를 표시하고 있습니다.',
|
||||
pdf_batch_previous: '이전 ${1}페이지',
|
||||
pdf_batch_next: '다음 ${1}페이지',
|
||||
more: '더 보기',
|
||||
link_tooltip_external_link: '외부 링크',
|
||||
link_tooltip_email: '이메일 보내기',
|
||||
|
||||
@@ -98,8 +98,9 @@ export const lt: TranslationLanguage = {
|
||||
pdf_page_of: '${1} iš ${2}',
|
||||
pdf_mode_only_page: 'Tik šis puslapis',
|
||||
pdf_mode_all: 'Visi puslapiai',
|
||||
pdf_limit_reached: 'Nepavyko sugeneruoti PDF ${1} puslapiams, generavimas sustojo ties ${2}.',
|
||||
pdf_limit_reached_continue: 'Išplėsti dar ${1} puslapiais.',
|
||||
pdf_batch_range: 'Rodomi puslapiai ${1}–${2} iš ${3}.',
|
||||
pdf_batch_previous: 'Ankstesni ${1} puslapiai',
|
||||
pdf_batch_next: 'Kiti ${1} puslapiai',
|
||||
more: 'Daugiau',
|
||||
link_tooltip_external_link: 'Išorinė nuoroda į',
|
||||
link_tooltip_email: 'Siųsti el. laišką į',
|
||||
|
||||
@@ -96,8 +96,9 @@ export const lv: TranslationLanguage = {
|
||||
pdf_page_of: '${1} no ${2}',
|
||||
pdf_mode_only_page: 'Tikai šī lapa',
|
||||
pdf_mode_all: 'Visas lapas',
|
||||
pdf_limit_reached: 'Neizdevās ģenerēt PDF ${1} lapām, ģenerēšana apstājās pie ${2}.',
|
||||
pdf_limit_reached_continue: 'Paplašināt ar vēl ${1} lapām.',
|
||||
pdf_batch_range: 'Rāda lapas ${1}–${2} no ${3}.',
|
||||
pdf_batch_previous: 'Iepriekšējās ${1} lapas',
|
||||
pdf_batch_next: 'Nākamās ${1} lapas',
|
||||
more: 'Vairāk',
|
||||
link_tooltip_external_link: 'Ārēja saite uz',
|
||||
link_tooltip_email: 'Sūtīt e-pastu uz',
|
||||
|
||||
@@ -97,8 +97,9 @@ export const ms: TranslationLanguage = {
|
||||
pdf_page_of: '${1} daripada ${2}',
|
||||
pdf_mode_only_page: 'Halaman ini sahaja',
|
||||
pdf_mode_all: 'Semua halaman',
|
||||
pdf_limit_reached: 'Tidak dapat menjana PDF untuk ${1} halaman, penjanaan berhenti pada ${2}.',
|
||||
pdf_limit_reached_continue: 'Lanjutkan dengan ${1} halaman lagi.',
|
||||
pdf_batch_range: 'Memaparkan halaman ${1}–${2} daripada ${3}.',
|
||||
pdf_batch_previous: '${1} halaman sebelumnya',
|
||||
pdf_batch_next: '${1} halaman seterusnya',
|
||||
more: 'Lagi',
|
||||
link_tooltip_external_link: 'Pautan luaran ke',
|
||||
link_tooltip_email: 'Hantar e-mel kepada',
|
||||
|
||||
@@ -100,8 +100,9 @@ export const nl: TranslationLanguage = {
|
||||
pdf_page_of: '${1} van ${2}',
|
||||
pdf_mode_only_page: 'Alleen deze pagina',
|
||||
pdf_mode_all: "Alle pagina's",
|
||||
pdf_limit_reached: "Kon de PDF niet genereren voor ${1} pagina's, generatie gestopt bij ${2}.",
|
||||
pdf_limit_reached_continue: "Verleng met ${1} extra pagina's.",
|
||||
pdf_batch_range: "Pagina's ${1}–${2} van ${3}.",
|
||||
pdf_batch_previous: "Vorige ${1} pagina's",
|
||||
pdf_batch_next: "Volgende ${1} pagina's",
|
||||
more: 'Meer',
|
||||
link_tooltip_external_link: 'Externe link naar',
|
||||
link_tooltip_email: 'E-mail versturen naar',
|
||||
|
||||
@@ -99,8 +99,9 @@ export const no: TranslationLanguage = {
|
||||
pdf_page_of: '${1} av ${2}',
|
||||
pdf_mode_only_page: 'Kun denne siden',
|
||||
pdf_mode_all: 'Alle sider',
|
||||
pdf_limit_reached: 'Kunne ikke generere PDF for ${1} sider, generering stoppet ved ${2}.',
|
||||
pdf_limit_reached_continue: 'Utvid med ${1} flere sider.',
|
||||
pdf_batch_range: 'Viser sider ${1}–${2} av ${3}.',
|
||||
pdf_batch_previous: 'Forrige ${1} sider',
|
||||
pdf_batch_next: 'Neste ${1} sider',
|
||||
more: 'Mer',
|
||||
link_tooltip_external_link: 'Ekstern lenke til',
|
||||
link_tooltip_email: 'Send e-post til',
|
||||
|
||||
@@ -97,9 +97,9 @@ export const pl: TranslationLanguage = {
|
||||
pdf_page_of: '${1} z ${2}',
|
||||
pdf_mode_only_page: 'Tylko ta strona',
|
||||
pdf_mode_all: 'Wszystkie strony',
|
||||
pdf_limit_reached:
|
||||
'Nie udało się wygenerować PDF dla ${1} stron, generowanie zatrzymało się na ${2}.',
|
||||
pdf_limit_reached_continue: 'Rozszerz o kolejne ${1} stron.',
|
||||
pdf_batch_range: 'Strony ${1}–${2} z ${3}.',
|
||||
pdf_batch_previous: 'Poprzednie ${1} stron',
|
||||
pdf_batch_next: 'Następne ${1} stron',
|
||||
more: 'Więcej',
|
||||
link_tooltip_external_link: 'Link zewnętrzny do',
|
||||
link_tooltip_email: 'Wyślij e-mail do',
|
||||
|
||||
@@ -101,9 +101,9 @@ export const pt_br: TranslationLanguage = {
|
||||
pdf_page_of: '${1} de ${2}',
|
||||
pdf_mode_only_page: 'Somente esta página',
|
||||
pdf_mode_all: 'Todas as páginas',
|
||||
pdf_limit_reached:
|
||||
'Não foi possível gerar o PDF para ${1} páginas, a geração foi interrompida em ${2}.',
|
||||
pdf_limit_reached_continue: 'Estender com mais ${1} páginas.',
|
||||
pdf_batch_range: 'Mostrando as páginas ${1}–${2} de ${3}.',
|
||||
pdf_batch_previous: '${1} páginas anteriores',
|
||||
pdf_batch_next: 'Próximas ${1} páginas',
|
||||
more: 'Mais',
|
||||
link_tooltip_external_link: 'Link externo para',
|
||||
link_tooltip_email: 'Enviar e-mail para',
|
||||
|
||||
@@ -99,8 +99,9 @@ export const pt: TranslationLanguage = {
|
||||
pdf_page_of: '${1} de ${2}',
|
||||
pdf_mode_only_page: 'Apenas esta página',
|
||||
pdf_mode_all: 'Todas as páginas',
|
||||
pdf_limit_reached: 'Não foi possível gerar o PDF para ${1} páginas; a geração parou em ${2}.',
|
||||
pdf_limit_reached_continue: 'Expandir com mais ${1} páginas.',
|
||||
pdf_batch_range: 'A mostrar as páginas ${1}–${2} de ${3}.',
|
||||
pdf_batch_previous: '${1} páginas anteriores',
|
||||
pdf_batch_next: '${1} páginas seguintes',
|
||||
more: 'Mais',
|
||||
link_tooltip_external_link: 'Ligação externa para',
|
||||
link_tooltip_email: 'Enviar e-mail para',
|
||||
|
||||
@@ -100,9 +100,9 @@ export const ro: TranslationLanguage = {
|
||||
pdf_page_of: '${1} din ${2}',
|
||||
pdf_mode_only_page: 'Doar această pagină',
|
||||
pdf_mode_all: 'Toate paginile',
|
||||
pdf_limit_reached:
|
||||
'Nu s-a putut genera PDF-ul pentru ${1} pagini, generarea s-a oprit la ${2}.',
|
||||
pdf_limit_reached_continue: 'Extinde cu încă ${1} pagini.',
|
||||
pdf_batch_range: 'Se afișează paginile ${1}–${2} din ${3}.',
|
||||
pdf_batch_previous: '${1} pagini anterioare',
|
||||
pdf_batch_next: 'Următoarele ${1} pagini',
|
||||
more: 'Mai multe',
|
||||
link_tooltip_external_link: 'Link extern către',
|
||||
link_tooltip_email: 'Trimite e-mail către',
|
||||
|
||||
@@ -100,9 +100,9 @@ export const ru: TranslationLanguage = {
|
||||
pdf_page_of: '${1} из ${2}',
|
||||
pdf_mode_only_page: 'Только эта страница',
|
||||
pdf_mode_all: 'Все страницы',
|
||||
pdf_limit_reached:
|
||||
'Не удалось создать PDF-файл для ${1} страниц, генерация остановлена на ${2}.',
|
||||
pdf_limit_reached_continue: 'Расширьте ещё на ${1} страниц.',
|
||||
pdf_batch_range: 'Страницы ${1}–${2} из ${3}.',
|
||||
pdf_batch_previous: 'Предыдущие ${1} страниц',
|
||||
pdf_batch_next: 'Следующие ${1} страниц',
|
||||
more: 'Ещё',
|
||||
link_tooltip_external_link: 'Внешняя ссылка на',
|
||||
link_tooltip_email: 'Отправить письмо на',
|
||||
|
||||
@@ -99,9 +99,9 @@ export const sk: TranslationLanguage = {
|
||||
pdf_page_of: '${1} z ${2}',
|
||||
pdf_mode_only_page: 'Iba táto stránka',
|
||||
pdf_mode_all: 'Všetky stránky',
|
||||
pdf_limit_reached:
|
||||
'Nepodarilo sa vygenerovať PDF pre ${1} stránok, generovanie sa zastavilo na ${2}.',
|
||||
pdf_limit_reached_continue: 'Rozšíriť o ďalších ${1} stránok.',
|
||||
pdf_batch_range: 'Zobrazené stránky ${1}–${2} z ${3}.',
|
||||
pdf_batch_previous: 'Predchádzajúcich ${1} stránok',
|
||||
pdf_batch_next: 'Ďalších ${1} stránok',
|
||||
more: 'Viac',
|
||||
link_tooltip_external_link: 'Externý odkaz na',
|
||||
link_tooltip_email: 'Odoslať e-mail na',
|
||||
|
||||
@@ -97,9 +97,9 @@ export const sl: TranslationLanguage = {
|
||||
pdf_page_of: '${1} od ${2}',
|
||||
pdf_mode_only_page: 'Samo ta stran',
|
||||
pdf_mode_all: 'Vse strani',
|
||||
pdf_limit_reached:
|
||||
'PDF-ja ni bilo mogoče ustvariti za ${1} strani, ustvarjanje se je ustavilo pri ${2}.',
|
||||
pdf_limit_reached_continue: 'Razširi še za ${1} strani.',
|
||||
pdf_batch_range: 'Prikazane strani ${1}–${2} od ${3}.',
|
||||
pdf_batch_previous: 'Prejšnjih ${1} strani',
|
||||
pdf_batch_next: 'Naslednjih ${1} strani',
|
||||
more: 'Več',
|
||||
link_tooltip_external_link: 'Zunanja povezava do',
|
||||
link_tooltip_email: 'Pošlji e-pošto na',
|
||||
|
||||
@@ -97,9 +97,9 @@ export const sv: TranslationLanguage = {
|
||||
pdf_page_of: '${1} av ${2}',
|
||||
pdf_mode_only_page: 'Endast denna sida',
|
||||
pdf_mode_all: 'Alla sidor',
|
||||
pdf_limit_reached:
|
||||
'Det gick inte att generera PDF för ${1} sidor, genereringen stoppades vid ${2}.',
|
||||
pdf_limit_reached_continue: 'Utöka med ytterligare ${1} sidor.',
|
||||
pdf_batch_range: 'Visar sidorna ${1}–${2} av ${3}.',
|
||||
pdf_batch_previous: 'Föregående ${1} sidor',
|
||||
pdf_batch_next: 'Nästa ${1} sidor',
|
||||
more: 'Mer',
|
||||
link_tooltip_external_link: 'Extern länk till',
|
||||
link_tooltip_email: 'Skicka e-post till',
|
||||
|
||||
@@ -95,8 +95,9 @@ export const th: TranslationLanguage = {
|
||||
pdf_page_of: '${1} จาก ${2}',
|
||||
pdf_mode_only_page: 'เฉพาะหน้านี้',
|
||||
pdf_mode_all: 'ทุกหน้า',
|
||||
pdf_limit_reached: 'ไม่สามารถสร้าง PDF สำหรับ ${1} หน้าได้ การสร้างหยุดที่ ${2}',
|
||||
pdf_limit_reached_continue: 'ขยายอีก ${1} หน้า',
|
||||
pdf_batch_range: 'แสดงหน้า ${1}–${2} จาก ${3}',
|
||||
pdf_batch_previous: '${1} หน้าก่อนหน้า',
|
||||
pdf_batch_next: '${1} หน้าถัดไป',
|
||||
more: 'เพิ่มเติม',
|
||||
link_tooltip_external_link: 'ลิงก์ภายนอกไปยัง',
|
||||
link_tooltip_email: 'ส่งอีเมลไปยัง',
|
||||
|
||||
@@ -96,8 +96,9 @@ export const tr: TranslationLanguage = {
|
||||
pdf_page_of: '${1} / ${2}',
|
||||
pdf_mode_only_page: 'Yalnızca bu sayfa',
|
||||
pdf_mode_all: 'Tüm sayfalar',
|
||||
pdf_limit_reached: '${1} sayfa için PDF oluşturulamadı, oluşturma ${2} konumunda durdu.',
|
||||
pdf_limit_reached_continue: '${1} sayfa daha ekleyerek genişlet.',
|
||||
pdf_batch_range: '${3} sayfadan ${1}–${2} arası gösteriliyor.',
|
||||
pdf_batch_previous: 'Önceki ${1} sayfa',
|
||||
pdf_batch_next: 'Sonraki ${1} sayfa',
|
||||
more: 'Daha fazla',
|
||||
link_tooltip_external_link: 'Harici bağlantı',
|
||||
link_tooltip_email: 'E-posta gönder',
|
||||
|
||||
@@ -96,8 +96,9 @@ export const uk: TranslationLanguage = {
|
||||
pdf_page_of: '${1} з ${2}',
|
||||
pdf_mode_only_page: 'Лише ця сторінка',
|
||||
pdf_mode_all: 'Усі сторінки',
|
||||
pdf_limit_reached: 'Не вдалося створити PDF для ${1} сторінок, створення зупинилося на ${2}.',
|
||||
pdf_limit_reached_continue: 'Розширити ще на ${1} сторінок.',
|
||||
pdf_batch_range: 'Сторінки ${1}–${2} з ${3}.',
|
||||
pdf_batch_previous: 'Попередні ${1} сторінок',
|
||||
pdf_batch_next: 'Наступні ${1} сторінок',
|
||||
more: 'Більше',
|
||||
link_tooltip_external_link: 'Зовнішнє посилання на',
|
||||
link_tooltip_email: 'Надіслати електронний лист на',
|
||||
|
||||
@@ -96,8 +96,9 @@ export const vi: TranslationLanguage = {
|
||||
pdf_page_of: '${1} trên ${2}',
|
||||
pdf_mode_only_page: 'Chỉ trang này',
|
||||
pdf_mode_all: 'Tất cả trang',
|
||||
pdf_limit_reached: 'Không thể tạo PDF cho ${1} trang, quá trình tạo đã dừng tại ${2}.',
|
||||
pdf_limit_reached_continue: 'Mở rộng thêm ${1} trang.',
|
||||
pdf_batch_range: 'Đang hiển thị trang ${1}–${2} trên ${3}.',
|
||||
pdf_batch_previous: '${1} trang trước',
|
||||
pdf_batch_next: '${1} trang tiếp theo',
|
||||
more: 'Thêm',
|
||||
link_tooltip_external_link: 'Liên kết ngoài tới',
|
||||
link_tooltip_email: 'Gửi email tới',
|
||||
|
||||
@@ -94,8 +94,9 @@ export const yue: TranslationLanguage = {
|
||||
pdf_page_of: '${1} / ${2}',
|
||||
pdf_mode_only_page: '只限呢頁',
|
||||
pdf_mode_all: '所有頁面',
|
||||
pdf_limit_reached: '無法為 ${1} 頁產生 PDF,已喺 ${2} 停止。',
|
||||
pdf_limit_reached_continue: '再加 ${1} 頁。',
|
||||
pdf_batch_range: '而家顯示緊第 ${1}–${2} 頁,總共 ${3} 頁。',
|
||||
pdf_batch_previous: '上 ${1} 頁',
|
||||
pdf_batch_next: '下 ${1} 頁',
|
||||
more: '更多',
|
||||
link_tooltip_external_link: '外部連結到',
|
||||
link_tooltip_email: '傳送電郵到',
|
||||
|
||||
@@ -94,8 +94,9 @@ export const zh_tw: TranslationLanguage = {
|
||||
pdf_page_of: '${1} / ${2}',
|
||||
pdf_mode_only_page: '僅此頁面',
|
||||
pdf_mode_all: '所有頁面',
|
||||
pdf_limit_reached: '無法為 ${1} 頁產生 PDF,已在 ${2} 停止產生。',
|
||||
pdf_limit_reached_continue: '再擴展 ${1} 頁。',
|
||||
pdf_batch_range: '正在顯示第 ${1}–${2} 頁,共 ${3} 頁。',
|
||||
pdf_batch_previous: '上 ${1} 頁',
|
||||
pdf_batch_next: '下 ${1} 頁',
|
||||
more: '更多',
|
||||
link_tooltip_external_link: '外部連結至',
|
||||
link_tooltip_email: '傳送電子郵件至',
|
||||
|
||||
@@ -95,8 +95,9 @@ export const zh: TranslationLanguage = {
|
||||
pdf_page_of: '${1} / ${2}',
|
||||
pdf_mode_only_page: '仅本页',
|
||||
pdf_mode_all: '所有页面',
|
||||
pdf_limit_reached: '无法为 ${1} 页生成 PDF,生成在 ${2} 页时停止。',
|
||||
pdf_limit_reached_continue: '再扩展 ${1} 页。',
|
||||
pdf_batch_range: '正在显示第 ${1}–${2} 页,共 ${3} 页。',
|
||||
pdf_batch_previous: '上 ${1} 页',
|
||||
pdf_batch_next: '下 ${1} 页',
|
||||
more: '更多',
|
||||
link_tooltip_external_link: '外部链接到',
|
||||
link_tooltip_email: '发送邮件到',
|
||||
|
||||
Reference in New Issue
Block a user