├── .prettierignore ├── test ├── manual │ ├── in1.docx │ ├── in2.docx │ ├── in3.docx │ ├── google.pdf │ ├── document.docx │ ├── google_via_got.pdf │ ├── html_with_bg.pdf │ ├── test_ping.js │ ├── test_issue_14.ts │ ├── test_issue_25.ts │ ├── test_issue_27.ts │ ├── test_issue_11.ts │ ├── test_issue_5.ts │ ├── test.ts │ ├── test.js │ ├── test_6.1_new_features.ts │ ├── test_6.2_new_features.ts │ ├── test_issue_15.ts │ ├── test_ping.ts │ ├── test_issue_33.ts │ ├── test_issue_47.ts │ ├── test_issue_51.ts │ ├── test_issue_13.ts │ ├── test_issue_32.ts │ └── statement.html ├── add.spec.ts ├── convert.spec.ts ├── ping.spec.ts ├── office.spec.ts ├── internal │ ├── path.spec.ts │ ├── headers.spec.ts │ ├── fields.spec.ts │ ├── type.spec.ts │ ├── source-converters.spec.ts │ └── source-checkers.spec.ts ├── html.spec.ts ├── markdown.spec.ts ├── add-helpers.spec.ts ├── set.spec.ts ├── set-helpers.spec.ts ├── adjust.spec.ts ├── merge.spec.ts ├── url.spec.ts ├── gotenberg.spec.ts ├── to.spec.ts ├── client │ └── node.spec.ts ├── to-helpers.spec.ts └── please.spec.ts ├── .gitignore ├── tslint.json ├── src ├── convert.ts ├── tools │ ├── fn.ts │ └── pipe.ts ├── internal │ ├── path.ts │ ├── headers.ts │ ├── fields.ts │ ├── type.ts │ ├── source-checkers.ts │ └── source-converters.ts ├── ping.ts ├── office.ts ├── add.ts ├── page.ts ├── merge.ts ├── html.ts ├── markdown.ts ├── set.ts ├── index.ts ├── adjust.ts ├── add-helpers.ts ├── url.ts ├── gotenberg.ts ├── to-helpers.ts ├── to.ts ├── set-helpers.ts ├── client │ └── node.ts ├── please.ts └── _types.ts ├── jest.config.json ├── prettier.config.js ├── tsconfig.json ├── .github └── workflows │ └── build.yml ├── LICENSE ├── .yaspeller.json ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /test/manual/in1.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumauri/gotenberg-js-client/HEAD/test/manual/in1.docx -------------------------------------------------------------------------------- /test/manual/in2.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumauri/gotenberg-js-client/HEAD/test/manual/in2.docx -------------------------------------------------------------------------------- /test/manual/in3.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumauri/gotenberg-js-client/HEAD/test/manual/in3.docx -------------------------------------------------------------------------------- /test/manual/google.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumauri/gotenberg-js-client/HEAD/test/manual/google.pdf -------------------------------------------------------------------------------- /test/manual/document.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumauri/gotenberg-js-client/HEAD/test/manual/document.docx -------------------------------------------------------------------------------- /test/manual/google_via_got.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumauri/gotenberg-js-client/HEAD/test/manual/google_via_got.pdf -------------------------------------------------------------------------------- /test/manual/html_with_bg.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yumauri/gotenberg-js-client/HEAD/test/manual/html_with_bg.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /coverage/ 3 | /lib/ 4 | /pkg/ 5 | yarn-error.log 6 | .DS_Store 7 | .project 8 | .vscode 9 | .idea 10 | *.log 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard-plus", 4 | "tslint-config-security", 5 | "tslint-config-prettier" 6 | ], 7 | "rules": { 8 | "no-any": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/convert.ts: -------------------------------------------------------------------------------- 1 | import { path } from './internal/path' 2 | 3 | /** 4 | * Adjust Request url, by adding `/convert` to it 5 | * @return new HtmlRequest, doesn't modify original Request 6 | */ 7 | export const convert = path('/convert') 8 | -------------------------------------------------------------------------------- /src/tools/fn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Some short useful functions 3 | */ 4 | 5 | export const setProperty = 6 | (...fields: string[]) => 7 | (...values: any[]) => 8 | (object: any) => { 9 | for (let i = 0; i < fields.length; i++) { 10 | object[fields[i]] = values[i] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "transform": { 3 | "^.+\\.ts$": "ts-jest" 4 | }, 5 | "collectCoverage": true, 6 | "coverageReporters": ["text", "lcov"], 7 | "collectCoverageFrom": ["src/**/*.ts"], 8 | "testRegex": ".+\\.spec\\.ts$", 9 | "maxConcurrency": 3, 10 | "testEnvironment": "node" 11 | } 12 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: true, 9 | trailingComma: 'es5', 10 | bracketSpacing: true, 11 | jsxBracketSameLine: false, 12 | arrowParens: 'always', 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/path.ts: -------------------------------------------------------------------------------- 1 | import { Request } from '../_types' 2 | 3 | /** 4 | * Adjust Request fields, by adding given `path` to it 5 | * @return new Request, doesn't modify original Request 6 | */ 7 | export const path = 8 | (path: string) => 9 | (request: Request): Request => ({ 10 | ...request, 11 | url: (request.url || '') + path, 12 | }) 13 | -------------------------------------------------------------------------------- /test/manual/test_ping.js: -------------------------------------------------------------------------------- 1 | const { gotenberg, pipe, ping, please } = require('../../pkg') 2 | 3 | // need to run Gotenberg like this 4 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 5 | 6 | pipe(gotenberg('http://localhost:3500'), ping, please)() 7 | .then(() => console.log('Gotenberg is up')) 8 | .catch((error) => console.error('Gotenberg is down:', error)) 9 | -------------------------------------------------------------------------------- /test/manual/test_issue_14.ts: -------------------------------------------------------------------------------- 1 | import { convert, gotenberg, office, pipe, please, url } from '../../src' 2 | 3 | // following should throw an exception about double conversion 4 | // >>> Cannot set "Office" conversion, already set to "Url" 5 | 6 | const toPDF = pipe(gotenberg('http://localhost:8008'), convert, url, office, please) 7 | toPDF('http://any.url.com').then(console.log).catch(console.error) 8 | -------------------------------------------------------------------------------- /src/ping.ts: -------------------------------------------------------------------------------- 1 | import { PingRequest, Request, RequestType } from './_types' 2 | import { pipe } from './tools/pipe' 3 | import { path } from './internal/path' 4 | import { type } from './internal/type' 5 | 6 | /** 7 | * Adjust Request url, by adding `/ping` to it 8 | * @return new PingRequest, doesn't modify original Request 9 | */ 10 | export const ping: { 11 | (request: Request): PingRequest 12 | } = pipe(path('/ping'), type(RequestType.Ping)) 13 | -------------------------------------------------------------------------------- /src/internal/headers.ts: -------------------------------------------------------------------------------- 1 | import { HttpHeaders, Request } from '../_types' 2 | 3 | /** 4 | * Adjust Request headers, by extending `headers` 5 | * @return new Request, doesn't modify original Request 6 | */ 7 | export const headers: { 8 | (headers: HttpHeaders): (request: RequestEx) => RequestEx 9 | } = (headers) => (request) => ({ 10 | ...request, 11 | headers: { 12 | ...request.headers, 13 | ...headers, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/office.ts: -------------------------------------------------------------------------------- 1 | import { OfficeRequest, Request, RequestType } from './_types' 2 | import { pipe } from './tools/pipe' 3 | import { path } from './internal/path' 4 | import { type } from './internal/type' 5 | 6 | /** 7 | * Adjust Request url, by adding `/office` to it 8 | * @return new OfficeRequest, doesn't modify original Request 9 | */ 10 | export const office: { 11 | (request: Request): OfficeRequest 12 | } = pipe(path('/office'), type(RequestType.Office)) 13 | -------------------------------------------------------------------------------- /src/internal/fields.ts: -------------------------------------------------------------------------------- 1 | import { Request, RequestFields } from '../_types' 2 | 3 | /** 4 | * Adjust Request fields, by extending `fields` 5 | * @return new Request, doesn't modify original Request 6 | */ 7 | export const fields: { 8 | (fields: Partial): (request: RequestEx) => RequestEx 9 | } = (fields) => (request) => ({ 10 | ...request, 11 | fields: { 12 | ...request.fields, 13 | ...fields, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /test/manual/test_issue_25.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { convert, gotenberg, landscape, pipe, please, to, url } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 6 | 7 | pipe( 8 | gotenberg('http://localhost:3500'), 9 | convert, 10 | url, 11 | to(landscape), 12 | please 13 | )('https://google.com') 14 | .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/google.pdf`))) 15 | .catch(console.error) 16 | -------------------------------------------------------------------------------- /test/manual/test_issue_27.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { convert, gotenberg, html, pipe, please } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 6 | ;(async () => { 7 | const toPDF = pipe(gotenberg('http://localhost:3500'), convert, html, please) 8 | const pdf = await toPDF('
Make my day') 9 | pdf.pipe(createWriteStream(`${__dirname}/test_issue_27.pdf`)) 10 | })() 11 | -------------------------------------------------------------------------------- /test/add.spec.ts: -------------------------------------------------------------------------------- 1 | import { add, headers, Request, RequestType } from '../src' 2 | 3 | // dumb object to test purity 4 | const dumb: Request = { 5 | type: RequestType.Undefined, 6 | url: 'test', 7 | fields: {}, 8 | client: { 9 | post: () => { 10 | throw new Error('not implemented') 11 | }, 12 | }, 13 | } 14 | 15 | test('Should set fields using modifiers', () => { 16 | expect(add(headers({ Test: 'Foo' }))(dumb)).toEqual({ 17 | ...dumb, 18 | headers: { 'Gotenberg-Remoteurl-Test': 'Foo' }, 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/manual/test_issue_11.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream, readFileSync } from 'fs' 2 | import { convert, gotenberg, office, pipe, please } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 6 | 7 | const buffer = readFileSync('document.docx') 8 | 9 | pipe( 10 | gotenberg('http://localhost:3500'), 11 | convert, 12 | office, 13 | please 14 | )(['aaaa', buffer]) 15 | .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/document.pdf`))) 16 | .catch(console.error) 17 | -------------------------------------------------------------------------------- /test/manual/test_issue_5.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream, readFileSync } from 'fs' 2 | import { convert, gotenberg, office, pipe, please } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 6 | 7 | const buffer = readFileSync('document.docx') 8 | 9 | pipe( 10 | gotenberg('http://localhost:3500'), 11 | convert, 12 | office, 13 | please 14 | )(['doc.docx', buffer]) 15 | .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/document.pdf`))) 16 | .catch(console.error) 17 | -------------------------------------------------------------------------------- /test/convert.spec.ts: -------------------------------------------------------------------------------- 1 | import { convert, Request } from '../src' 2 | 3 | // dumb object to test purity 4 | const dumb = { url: 'test', fields: {} } 5 | 6 | test('Should add `/convert` path', () => { 7 | expect(convert(dumb as Request)).toEqual({ ...dumb, url: 'test/convert' }) 8 | }) 9 | 10 | test('Should not change original request', () => { 11 | expect(convert(dumb as Request)).toEqual({ ...dumb, url: 'test/convert' }) 12 | }) 13 | 14 | test('Should add property, if absent', () => { 15 | expect(convert({} as Request)).toEqual({ url: '/convert' }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/add.ts: -------------------------------------------------------------------------------- 1 | import { HeadersModifier, HttpHeaders, Request } from './_types' 2 | import { headers } from './internal/headers' 3 | 4 | /** 5 | * Adjust Request headers, for any request 6 | * @return new typed Request, doesn't modify original Request 7 | */ 8 | export const add: { 9 | (...opts: HeadersModifier[]): (request: RequestEx) => RequestEx 10 | } = (...opts) => { 11 | const httpHeaders: HttpHeaders = {} 12 | 13 | for (let i = opts.length; i--; ) { 14 | const op = opts[i] 15 | op(httpHeaders) 16 | } 17 | 18 | return headers(httpHeaders) 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "esnext", 5 | "lib": [], // there is problem with URL, when empty :( https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34960 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "removeComments": true, 13 | "declaration": true, 14 | "baseUrl": "./src" 15 | }, 16 | "include": ["src"], 17 | "exclude": ["**/node_modules", "**/.*/", "**/*.spec.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /test/manual/test.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { a4, convert, gotenberg, html, pipe, please, to } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 6 | 7 | pipe( 8 | gotenberg('http://localhost:3500'), 9 | convert, 10 | html, 11 | to(a4, { 12 | top: 0, 13 | right: 0.2, // ~5mm 14 | bottom: 0, 15 | left: 0.2, // ~5mm 16 | }), 17 | please 18 | )(`file://${__dirname}/statement.html`) 19 | .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/statement.pdf`))) 20 | .catch(console.error) 21 | -------------------------------------------------------------------------------- /test/manual/test.js: -------------------------------------------------------------------------------- 1 | const { createWriteStream } = require('fs') 2 | const { a4, convert, gotenberg, html, pipe, please, to } = require('../../pkg') 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 6 | 7 | pipe( 8 | gotenberg('http://localhost:3500'), 9 | convert, 10 | html, 11 | to(a4, { 12 | top: 0, 13 | right: 0.2, // ~5mm 14 | bottom: 0, 15 | left: 0.2, // ~5mm 16 | }), 17 | please 18 | )(`file://${__dirname}/statement.html`) 19 | .then(pdf => pdf.pipe(createWriteStream(`${__dirname}/statement.pdf`))) 20 | .catch(console.error) 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: ['push', 'pull_request'] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node: ['12', '14'] 12 | 13 | name: Node ${{ matrix.node }} 14 | 15 | steps: 16 | - uses: actions/checkout@master 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node }} 20 | - run: yarn 21 | - run: yarn lint 22 | - run: yarn test 23 | - run: yarn build 24 | - uses: coverallsapp/github-action@master 25 | with: 26 | github-token: ${{ secrets.github_token }} 27 | -------------------------------------------------------------------------------- /src/page.ts: -------------------------------------------------------------------------------- 1 | import { MarginOptions, PaperOptions } from './_types' 2 | 3 | // maybe add more sizes 4 | // https://papersizes.io/ 5 | 6 | export const A3: PaperOptions = [11.7, 16.5] 7 | export const A4: PaperOptions = [8.27, 11.7] 8 | export const A5: PaperOptions = [5.8, 8.3] 9 | export const A6: PaperOptions = [4.1, 5.8] 10 | export const LETTER: PaperOptions = [8.5, 11] 11 | export const LEGAL: PaperOptions = [8.5, 14] 12 | export const TABLOID: PaperOptions = [11, 17] 13 | 14 | export const NO_MARGINS: MarginOptions = [0, 0, 0, 0] 15 | export const NORMAL_MARGINS: MarginOptions = [1, 1, 1, 1] 16 | export const LARGE_MARGINS: MarginOptions = [2, 2, 2, 2] 17 | -------------------------------------------------------------------------------- /test/manual/test_6.1_new_features.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { add, convert, gotenberg, headers, pipe, please, range, set, url } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 6 | 7 | pipe( 8 | gotenberg('http://localhost:3500'), 9 | convert, 10 | url, 11 | add( 12 | headers({ 13 | 'Test-Header': 'Foo', 14 | 'Test-Header-2': 'Bar', 15 | }) 16 | ), 17 | set(range('1-1')), 18 | please 19 | )(`https://request.urih.com/`) 20 | .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/headers.pdf`))) 21 | .catch(console.error) 22 | -------------------------------------------------------------------------------- /test/manual/test_6.2_new_features.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { a4, convert, gotenberg, html, pipe, please, scale, set, to } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 6 | 7 | pipe( 8 | gotenberg('http://localhost:3500'), 9 | convert, 10 | html, 11 | to(a4, { 12 | top: 0, 13 | right: 0.2, // ~5mm 14 | bottom: 0, 15 | left: 0.2, // ~5mm 16 | }), 17 | set(scale(0.5)), 18 | please 19 | )(`file://${__dirname}/statement.html`) 20 | .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/scale.pdf`))) 21 | .catch(console.error) 22 | -------------------------------------------------------------------------------- /src/merge.ts: -------------------------------------------------------------------------------- 1 | import { MergeRequest, Request, RequestType } from './_types' 2 | import { isIterable, isObject } from './internal/source-checkers' 3 | import { pipe } from './tools/pipe' 4 | import { path } from './internal/path' 5 | import { type } from './internal/type' 6 | 7 | /** 8 | * Adjust Request url, by adding `/merge` to it 9 | * @return new MergeRequest, doesn't modify original Request 10 | */ 11 | export const merge: { 12 | (request: Request): MergeRequest 13 | } = (request) => { 14 | if (!isIterable(request.source) && !isObject(request.source)) { 15 | throw new Error('Invalid source, should be iterable or object') 16 | } 17 | 18 | return pipe(path('/merge'), type(RequestType.Merge))(request) 19 | } 20 | -------------------------------------------------------------------------------- /test/ping.spec.ts: -------------------------------------------------------------------------------- 1 | import { ping, Request, RequestType } from '../src' 2 | 3 | // dumb object to test purity 4 | const dumb = { type: RequestType.Undefined, url: 'test', fields: {} } 5 | 6 | test('Should add `/ping` path and change type', () => { 7 | expect(ping(dumb as Request)).toEqual({ 8 | ...dumb, 9 | type: RequestType.Ping, 10 | url: 'test/ping', 11 | }) 12 | }) 13 | 14 | test('Should not change original request', () => { 15 | expect(ping(dumb as Request)).toEqual({ 16 | ...dumb, 17 | type: RequestType.Ping, 18 | url: 'test/ping', 19 | }) 20 | }) 21 | 22 | test('Should add property, if absent', () => { 23 | expect(ping({} as Request)).toEqual({ type: RequestType.Ping, url: '/ping' }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/html.ts: -------------------------------------------------------------------------------- 1 | import { ChromeRequestFields, HtmlRequest, Request, RequestType } from './_types' 2 | import { pipe } from './tools/pipe' 3 | import { fields } from './internal/fields' 4 | import { path } from './internal/path' 5 | import { type } from './internal/type' 6 | 7 | /** 8 | * Adjust Request url, by adding `/html` to it; Can add request parameters 9 | * @return new HtmlRequest, doesn't modify original Request 10 | */ 11 | export const html: { 12 | (o: ChromeRequestFields): (r: Request) => HtmlRequest 13 | (r: Request): HtmlRequest 14 | } = (x: Request | ChromeRequestFields): any => 15 | 'type' in x 16 | ? pipe(path('/html'), type(RequestType.Html))(x) 17 | : pipe(fields(x), path('/html'), type(RequestType.Html)) 18 | -------------------------------------------------------------------------------- /test/manual/test_issue_15.ts: -------------------------------------------------------------------------------- 1 | import https from 'https' 2 | import { createWriteStream } from 'fs' 3 | import { convert, gotenberg, office, pipe, please } from '../../src' 4 | 5 | // need to run Gotenberg like this 6 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 7 | 8 | const toPDF = pipe(gotenberg('http://localhost:3500'), convert, office, please) 9 | 10 | // https://file-examples.com/index.php/sample-documents-download/sample-doc-download/ 11 | https.get( 12 | 'https://file-examples.com/wp-content/uploads/2017/02/file-sample_100kB.docx', 13 | async (document) => { 14 | const pdf = await toPDF({ 'document.docx': document }) 15 | pdf.pipe(createWriteStream(`${__dirname}/test_issue_15.pdf`)) 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /src/markdown.ts: -------------------------------------------------------------------------------- 1 | import { ChromeRequestFields, MarkdownRequest, Request, RequestType } from './_types' 2 | import { pipe } from './tools/pipe' 3 | import { fields } from './internal/fields' 4 | import { path } from './internal/path' 5 | import { type } from './internal/type' 6 | 7 | /** 8 | * Adjust Request url, by adding `/markdown` to it; Can add request parameters 9 | * @return new MarkdownRequest, doesn't modify original Request 10 | */ 11 | export const markdown: { 12 | (o: ChromeRequestFields): (r: Request) => MarkdownRequest 13 | (r: Request): MarkdownRequest 14 | } = (x: Request | ChromeRequestFields): any => 15 | 'type' in x 16 | ? pipe(path('/markdown'), type(RequestType.Markdown))(x) 17 | : pipe(fields(x), path('/markdown'), type(RequestType.Markdown)) 18 | -------------------------------------------------------------------------------- /test/office.spec.ts: -------------------------------------------------------------------------------- 1 | import { office, Request, RequestType } from '../src' 2 | 3 | // dumb object to test purity 4 | const dumb = { type: RequestType.Undefined, url: 'test', fields: {} } 5 | 6 | test('Should add `/office` path and change type', () => { 7 | expect(office(dumb as Request)).toEqual({ 8 | ...dumb, 9 | type: RequestType.Office, 10 | url: 'test/office', 11 | }) 12 | }) 13 | 14 | test('Should not change original request', () => { 15 | expect(office(dumb as Request)).toEqual({ 16 | ...dumb, 17 | type: RequestType.Office, 18 | url: 'test/office', 19 | }) 20 | }) 21 | 22 | test('Should add property, if absent', () => { 23 | expect(office({} as Request)).toEqual({ 24 | type: RequestType.Office, 25 | url: '/office', 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/internal/path.spec.ts: -------------------------------------------------------------------------------- 1 | import { Request } from '../../src' 2 | import { path } from '../../src/internal/path' 3 | 4 | // dumb object to test purity 5 | const dumb = { url: 'test', fields: {} } 6 | 7 | test('Should add `/path` path', () => { 8 | const fn = path('/test') 9 | expect(typeof fn).toBe('function') 10 | expect(fn(dumb as Request)).toEqual({ ...dumb, url: 'test/test' }) 11 | }) 12 | 13 | test('Should not change original request', () => { 14 | const fn = path('/world') 15 | expect(typeof fn).toBe('function') 16 | expect(fn(dumb as Request)).toEqual({ ...dumb, url: 'test/world' }) 17 | }) 18 | 19 | test('Should add property, if absent', () => { 20 | const fn = path('hello') 21 | expect(typeof fn).toBe('function') 22 | expect(fn({} as Request)).toEqual({ url: 'hello' }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/set.ts: -------------------------------------------------------------------------------- 1 | import { FieldsModifier, Request, RequestFields } from './_types' 2 | import { fields } from './internal/fields' 3 | 4 | /** 5 | * Adjust Request fields, for any request 6 | * @return new typed Request, doesn't modify original Request 7 | */ 8 | export const set: { 9 | (...opts: (Partial | FieldsModifier)[]): ( 10 | request: RequestEx 11 | ) => RequestEx 12 | } = (...opts) => { 13 | const options: RequestFields = {} 14 | 15 | for (let i = opts.length; i--; ) { 16 | const op = opts[i] 17 | if (typeof op === 'function') { 18 | op(options) // this is fields modifier helper -> apply it 19 | } else { 20 | Object.assign(options, op) // extends result options with given option 21 | } 22 | } 23 | 24 | return fields(options) 25 | } 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // export main entry and finish functions 2 | export { gotenberg } from './gotenberg' 3 | export { please } from './please' 4 | 5 | // export conversion functions 6 | export { markdown } from './markdown' 7 | export { convert } from './convert' 8 | export { office } from './office' 9 | export { merge } from './merge' 10 | export { ping } from './ping' 11 | export { html } from './html' 12 | export { url } from './url' 13 | 14 | // export modifiers functions and constants 15 | export { adjust } from './adjust' 16 | export { add } from './add' 17 | export { set } from './set' 18 | export { to } from './to' 19 | export * from './add-helpers' 20 | export * from './set-helpers' 21 | export * from './to-helpers' 22 | export * from './page' 23 | 24 | // export pipe tool 25 | export { pipe } from './tools/pipe' 26 | 27 | // export types 28 | export * from './_types' 29 | -------------------------------------------------------------------------------- /test/html.spec.ts: -------------------------------------------------------------------------------- 1 | import { html, Request, RequestType } from '../src' 2 | 3 | // dumb object to test purity 4 | const dumb = { type: RequestType.Undefined, url: 'test', fields: {} } 5 | 6 | test('Should add `/html` path and change type', () => { 7 | expect(html(dumb as Request)).toEqual({ 8 | ...dumb, 9 | type: RequestType.Html, 10 | url: 'test/html', 11 | }) 12 | }) 13 | 14 | test('Should not change original request', () => { 15 | expect(html(dumb as Request)).toEqual({ 16 | ...dumb, 17 | type: RequestType.Html, 18 | url: 'test/html', 19 | }) 20 | }) 21 | 22 | test('Should return Request modifier, if options are given', () => { 23 | expect(html({ waitDelay: 10 })(dumb as Request)).toEqual({ 24 | ...dumb, 25 | type: RequestType.Html, 26 | url: 'test/html', 27 | fields: { 28 | waitDelay: 10, 29 | }, 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/adjust.ts: -------------------------------------------------------------------------------- 1 | import { Request } from './_types' 2 | 3 | /** 4 | * Recursively merge requests 5 | */ 6 | const merge = (request: RequestEx, modify: Partial) => { 7 | const result = { ...request, ...modify } 8 | 9 | for (const key in modify) { 10 | if ( 11 | modify[key] && 12 | request[key] && 13 | typeof modify[key] === 'object' && 14 | typeof request[key] === 'object' 15 | ) { 16 | result[key] = merge(request[key], modify[key]) 17 | } 18 | } 19 | 20 | return result 21 | } 22 | 23 | /** 24 | * Adjust any Request *object* fields, for any request 25 | * @return new typed Request, doesn't modify original Request 26 | */ 27 | export const adjust: { 28 | (modify: Partial): (request: RequestEx) => RequestEx 29 | } = (modify) => (request) => merge(request, modify) 30 | -------------------------------------------------------------------------------- /test/markdown.spec.ts: -------------------------------------------------------------------------------- 1 | import { markdown, Request, RequestType } from '../src' 2 | 3 | // dumb object to test purity 4 | const dumb = { type: RequestType.Undefined, url: 'test', fields: {} } 5 | 6 | test('Should add `/markdown` path and change type', () => { 7 | expect(markdown(dumb as Request)).toEqual({ 8 | ...dumb, 9 | type: RequestType.Markdown, 10 | url: 'test/markdown', 11 | }) 12 | }) 13 | 14 | test('Should not change original request', () => { 15 | expect(markdown(dumb as Request)).toEqual({ 16 | ...dumb, 17 | type: RequestType.Markdown, 18 | url: 'test/markdown', 19 | }) 20 | }) 21 | 22 | test('Should return Request modifier, if options are given', () => { 23 | expect(markdown({ waitDelay: 10 })(dumb as Request)).toEqual({ 24 | ...dumb, 25 | type: RequestType.Markdown, 26 | url: 'test/markdown', 27 | fields: { 28 | waitDelay: 10, 29 | }, 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/manual/test_ping.ts: -------------------------------------------------------------------------------- 1 | import { gotenberg, pipe, ping, please } from '../../src' 2 | 3 | // need to run Gotenberg like this 4 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 5 | 6 | // pipe( 7 | // gotenberg('http://localhost:3000'), 8 | // ping, 9 | // please 10 | // )({}) // <- empty source object to satisfy typings 11 | // .then(() => console.log('Gotenberg is up')) 12 | // .catch((error) => console.error('Gotenberg is down:', error)) 13 | 14 | // ;(async () => { 15 | // try { 16 | // await pipe(gotenberg('http://localhost:3500'), ping, please)({}) 17 | // console.log('Gotenberg is up') 18 | // } catch (error) { 19 | // console.error('Gotenberg is down:', error) 20 | // } 21 | // })() 22 | 23 | // 24 | ;(async () => { 25 | try { 26 | await please(ping(gotenberg('http://localhost:3500')({}))) 27 | console.log('Gotenberg is up') 28 | } catch (error) { 29 | console.error('Gotenberg is down:', error) 30 | } 31 | })() 32 | -------------------------------------------------------------------------------- /test/manual/test_issue_33.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { pipe, gotenberg, convert, html, adjust, please } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 gotenberg/gotenberg:7 6 | 7 | pipe( 8 | gotenberg(''), 9 | convert, // it is save to remove this line, if you want 10 | html, 11 | adjust({ 12 | // manually adjust endpoint, because 13 | // gotenberg:7 has different conversion endpoints 14 | url: 'http://localhost:3500/forms/chromium/convert/html', 15 | 16 | // manually adjust for fields 17 | fields: { 18 | printBackground: true, 19 | // `printBackground` is not valid field for gotenberg:6 20 | // so we have to cast to any, otherwise typescript will complain 21 | } as any, 22 | }), 23 | please 24 | )('test') 25 | .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/html_with_bg.pdf`))) 26 | .catch(console.error) 27 | -------------------------------------------------------------------------------- /test/manual/test_issue_47.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { pipe, gotenberg, convert, url, to, please } from '../../src' 3 | import got from 'got' 4 | 5 | // need to run Gotenberg like this 6 | // docker run --rm -p 3500:3000 gotenberg/gotenberg:7 7 | 8 | // const got = await import('got') 9 | 10 | // you can pass any config for your client 11 | // as third argument in `gotenberg` function 12 | const client = { 13 | post: (url, body, headers) => Promise.resolve(got.post({ url, body, headers, isStream: true })), 14 | } 15 | 16 | pipe( 17 | gotenberg(`http://localhost:3500/forms/chromium`, client), 18 | convert, 19 | url, 20 | to({ margins: [0, 0, 0, 0] }), 21 | (request) => { 22 | ;(request.fields as any).url = request.fields.remoteURL 23 | delete request.fields.remoteURL 24 | return request 25 | }, 26 | please 27 | )(`https://www.google.com`) 28 | .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/google_via_got.pdf`))) 29 | .catch(console.error) 30 | -------------------------------------------------------------------------------- /test/add-helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { header, headers, webhookHeader, webhookHeaders } from '../src' 2 | 3 | test('Test `header` function', () => { 4 | const object = {} 5 | header('Test', 'Foo')(object) 6 | expect(object).toEqual({ 'Gotenberg-Remoteurl-Test': 'Foo' }) 7 | }) 8 | 9 | test('Test `headers` function', () => { 10 | const object = {} 11 | headers({ Test1: 'Foo', Test2: 'Bar' })(object) 12 | expect(object).toEqual({ 13 | 'Gotenberg-Remoteurl-Test1': 'Foo', 14 | 'Gotenberg-Remoteurl-Test2': 'Bar', 15 | }) 16 | }) 17 | 18 | test('Test `webhookHeader` function', () => { 19 | const object = {} 20 | webhookHeader('Test', 'Foo')(object) 21 | expect(object).toEqual({ 'Gotenberg-Webhookurl-Test': 'Foo' }) 22 | }) 23 | 24 | test('Test `webjookHeaders` function', () => { 25 | const object = {} 26 | webhookHeaders({ Test1: 'Foo', Test2: 'Bar' })(object) 27 | expect(object).toEqual({ 28 | 'Gotenberg-Webhookurl-Test1': 'Foo', 29 | 'Gotenberg-Webhookurl-Test2': 'Bar', 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/manual/test_issue_51.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import * as got from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 gotenberg/gotenberg:7 6 | 7 | got 8 | .pipe( 9 | got.gotenberg(''), 10 | got.merge, 11 | got.adjust({ 12 | // manually adjust endpoint, because 13 | // gotenberg:7 has different one 14 | url: 'http://localhost:3500/forms/libreoffice/convert', 15 | 16 | // manually adjust for fields 17 | fields: { 18 | merge: true, 19 | // `merge` is not valid field for gotenberg:6 20 | // so we have to cast to any, otherwise typescript will complain 21 | } as any, // if you don't use typescript, just remove `as any` casting 22 | }), 23 | got.please 24 | )({ 25 | 'in1.docx': `file://${__dirname}/in1.docx`, 26 | 'in2.docx': `file://${__dirname}/in2.docx`, 27 | 'in3.docx': `file://${__dirname}/in3.docx`, 28 | }) 29 | .then((pdf) => pdf.pipe(fs.createWriteStream(`${__dirname}/out1.pdf`))) 30 | .catch(console.error) 31 | -------------------------------------------------------------------------------- /test/internal/headers.spec.ts: -------------------------------------------------------------------------------- 1 | import { Request } from '../../src' 2 | import { headers } from '../../src/internal/headers' 3 | 4 | // dumb object to test purity 5 | const dumb = { url: 'test', headers: { Test: 'Test' } } 6 | 7 | test('Should add headers', () => { 8 | const fn = headers({ 'X-Test-Header': 'Foo' }) 9 | expect(typeof fn).toBe('function') 10 | // tslint:disable-next-line no-any 11 | expect(fn(dumb as any)).toEqual({ 12 | ...dumb, 13 | headers: { Test: 'Test', 'X-Test-Header': 'Foo' }, 14 | }) 15 | }) 16 | 17 | test('Should not change original request', () => { 18 | const fn = headers({ 'X-Test-Header': 'Bar' }) 19 | expect(typeof fn).toBe('function') 20 | // tslint:disable-next-line no-any 21 | expect(fn(dumb as any)).toEqual({ 22 | ...dumb, 23 | headers: { Test: 'Test', 'X-Test-Header': 'Bar' }, 24 | }) 25 | }) 26 | 27 | test('Should add property, if absent', () => { 28 | const fn = headers({ 'X-Test-Header': 'Baz' }) 29 | expect(typeof fn).toBe('function') 30 | expect(fn({} as Request)).toEqual({ 31 | headers: { 'X-Test-Header': 'Baz' }, 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/internal/fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Request } from '../../src' 2 | import { fields } from '../../src/internal/fields' 3 | 4 | // dumb object to test purity 5 | const dumb = { url: 'test', fields: { landscape: false } } 6 | 7 | test('Should add fields', () => { 8 | const fn = fields({ waitTimeout: 10 }) 9 | expect(typeof fn).toBe('function') 10 | expect(fn(dumb as Request)).toEqual({ 11 | ...dumb, 12 | fields: { 13 | landscape: false, 14 | waitTimeout: 10, 15 | }, 16 | }) 17 | }) 18 | 19 | test('Should not change original request', () => { 20 | const fn = fields({ webhookURL: 'test string' }) 21 | expect(typeof fn).toBe('function') 22 | expect(fn(dumb as Request)).toEqual({ 23 | ...dumb, 24 | fields: { 25 | landscape: false, 26 | webhookURL: 'test string', 27 | }, 28 | }) 29 | }) 30 | 31 | test('Should add property, if absent', () => { 32 | const fn = fields({ resultFilename: 'test string' }) 33 | expect(typeof fn).toBe('function') 34 | expect(fn({} as Request)).toEqual({ 35 | fields: { 36 | resultFilename: 'test string', 37 | }, 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Victor Didenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/internal/type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HtmlRequest, 3 | MarkdownRequest, 4 | MergeRequest, 5 | OfficeRequest, 6 | PingRequest, 7 | Request, 8 | RequestType, 9 | UrlRequest, 10 | } from '../_types' 11 | 12 | /** 13 | * Adjust Request fields, by changing `type` 14 | * @return new typed Request, doesn't modify original Request 15 | */ 16 | export const type: { 17 | (type: RequestType.Url): (request: Request) => UrlRequest 18 | (type: RequestType.Ping): (request: Request) => PingRequest 19 | (type: RequestType.Html): (request: Request) => HtmlRequest 20 | (type: RequestType.Merge): (request: Request) => MergeRequest 21 | (type: RequestType.Office): (request: Request) => OfficeRequest 22 | (type: RequestType.Markdown): (request: Request) => MarkdownRequest 23 | } = 24 | (type: RequestType) => 25 | (request: Request): any => { 26 | if ('type' in request && request.type !== RequestType.Undefined) { 27 | throw new Error( 28 | `Cannot set "${RequestType[type]}" conversion, already set to "${ 29 | RequestType[request.type] 30 | }"` 31 | ) 32 | } 33 | 34 | return { 35 | ...request, 36 | type, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/tools/pipe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple `pipe` implementation 3 | */ 4 | // prettier-ignore 5 | export const pipe: { 6 | ( 7 | f1: (request: T) => R 8 | ): (source: T) => R 9 | ( 10 | f1: (request: T) => T1, 11 | f2: (request: T1) => R 12 | ): (source: T) => R 13 | ( 14 | f1: (request: T) => T1, 15 | f2: (request: T1) => T2, 16 | f3: (request: T2) => R 17 | ): (source: T) => R 18 | ( 19 | f1: (request: T) => T1, 20 | f2: (request: T1) => T2, 21 | f3: (request: T2) => T3, 22 | f4: (request: T3) => R 23 | ): (source: T) => R 24 | ( 25 | f1: (request: T) => T1, 26 | f2: (request: T1) => T2, 27 | f3: (request: T2) => T3, 28 | f4: (request: T3) => T4, 29 | f5: (request: T4) => R 30 | ): (source: T) => R 31 | ( 32 | f1: (request: T) => T1, 33 | f2: (request: T1) => T2, 34 | f3: (request: T2) => T3, 35 | f4: (request: T3) => T4, 36 | f5: (request: T4) => T5, 37 | f6: (request: T5) => R 38 | ): (source: T) => R 39 | } = (...fns: ((request: T) => T)[]) => (source: T): T => 40 | fns.reduce((request, fn) => fn(request), source) 41 | -------------------------------------------------------------------------------- /src/add-helpers.ts: -------------------------------------------------------------------------------- 1 | import { HeadersModifier, HttpHeaders } from './_types' 2 | import { setProperty } from './tools/fn' 3 | 4 | // https://thecodingmachine.github.io/gotenberg/#url.custom_http_headers 5 | 6 | /** 7 | * Adds/Modifies single header for Url conversion 8 | */ 9 | export const header = (name: string, value: number | string): HeadersModifier => 10 | setProperty(`Gotenberg-Remoteurl-${name}`)(value) 11 | 12 | /** 13 | * Adds/Modifies many headers for Url conversion 14 | */ 15 | export const headers = 16 | (headers: HttpHeaders): HeadersModifier => 17 | (_) => { 18 | for (const name in headers) { 19 | header(name, headers[name])(_) 20 | } 21 | } 22 | 23 | // https://thecodingmachine.github.io/gotenberg/#webhook.custom_http_headers 24 | 25 | /** 26 | * Adds/Modifies single header for Webhook 27 | */ 28 | export const webhookHeader = (name: string, value: number | string): HeadersModifier => 29 | setProperty(`Gotenberg-Webhookurl-${name}`)(value) 30 | 31 | /** 32 | * Adds/Modifies many headers for Webhook 33 | */ 34 | export const webhookHeaders = 35 | (headers: HttpHeaders): HeadersModifier => 36 | (_) => { 37 | for (const name in headers) { 38 | webhookHeader(name, headers[name])(_) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/set.spec.ts: -------------------------------------------------------------------------------- 1 | import { delay, filename, Request, RequestType, set } from '../src' 2 | 3 | // dumb object to test purity 4 | const dumb: Request = { 5 | type: RequestType.Undefined, 6 | url: 'test', 7 | fields: {}, 8 | client: { 9 | post: () => { 10 | throw new Error('not implemented') 11 | }, 12 | }, 13 | } 14 | 15 | test('Should set static fields', () => { 16 | expect(set()(dumb)).toEqual({ ...dumb }) 17 | expect(set({})(dumb)).toEqual({ ...dumb }) 18 | expect(set({ landscape: true })(dumb)).toEqual({ 19 | ...dumb, 20 | fields: { landscape: true }, 21 | }) 22 | expect(set({ resultFilename: 'index.pdf' })(dumb)).toEqual({ 23 | ...dumb, 24 | fields: { resultFilename: 'index.pdf' }, 25 | }) 26 | }) 27 | 28 | test('Should set fields using modifiers', () => { 29 | expect(set(delay(99))(dumb)).toEqual({ 30 | ...dumb, 31 | fields: { waitDelay: 99 }, 32 | }) 33 | expect(set(filename('index.pdf'))(dumb)).toEqual({ 34 | ...dumb, 35 | fields: { resultFilename: 'index.pdf' }, 36 | }) 37 | }) 38 | 39 | test('Should set fields both, using modifiers and static', () => { 40 | expect(set(delay(99), { resultFilename: 'test.pdf' })(dumb)).toEqual({ 41 | ...dumb, 42 | fields: { waitDelay: 99, resultFilename: 'test.pdf' }, 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/url.ts: -------------------------------------------------------------------------------- 1 | import { Request, RequestType, UrlRequest } from './_types' 2 | import { isString, isURL } from './internal/source-checkers' // tslint:disable-line no-circular-imports 3 | import { pipe } from './tools/pipe' 4 | import { fields } from './internal/fields' 5 | import { path } from './internal/path' 6 | import { type } from './internal/type' 7 | 8 | /** 9 | * Adjust Request url, by adding `/url` to it; Set `remoteURL` from source 10 | * @return new UrlRequest, doesn't modify original Request 11 | */ 12 | export const url: { 13 | (request: Request): UrlRequest 14 | } = (request) => { 15 | if (!isString(request.source) && !isURL(request.source)) { 16 | throw new Error('Invalid source, should be url string or instance of URL') 17 | } 18 | 19 | return pipe( 20 | fields({ 21 | remoteURL: request.source.toString(), 22 | 23 | // set all margins to 0 24 | // > Attention: when converting a website to PDF, you should remove all margins. 25 | // > If not, some of the content of the page might be hidden. 26 | // > @see https://thecodingmachine.github.io/gotenberg/#url.basic 27 | marginTop: 0, 28 | marginBottom: 0, 29 | marginLeft: 0, 30 | marginRight: 0, 31 | }), 32 | path('/url'), 33 | type(RequestType.Url) 34 | )({ 35 | ...request, 36 | source: undefined, // eliminate source from Request 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /test/set-helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { delay, filename, googleChromeRpccBufferSize, range, scale, timeout, webhook } from '../src' 2 | 3 | test('Test `filename` function', () => { 4 | const object = {} 5 | filename('index.html')(object) 6 | expect(object).toEqual({ resultFilename: 'index.html' }) 7 | }) 8 | 9 | test('Test `timeout` function', () => { 10 | const object = {} 11 | timeout(99)(object) 12 | expect(object).toEqual({ waitTimeout: 99 }) 13 | }) 14 | 15 | test('Test `delay` function', () => { 16 | const object = {} 17 | delay(99)(object) 18 | expect(object).toEqual({ waitDelay: 99 }) 19 | }) 20 | 21 | test('Test `webhook` function', () => { 22 | const object = {} 23 | webhook('http://1')(object) 24 | expect(object).toEqual({ webhookURL: 'http://1' }) 25 | webhook('http://2', 99)(object) 26 | expect(object).toEqual({ webhookURL: 'http://2', webhookURLTimeout: 99 }) 27 | }) 28 | 29 | test('Test `googleChromeRpccBufferSize` function', () => { 30 | const object = {} 31 | googleChromeRpccBufferSize(9999)(object) 32 | expect(object).toEqual({ googleChromeRpccBufferSize: 9999 }) 33 | }) 34 | 35 | test('Test `range` function', () => { 36 | const object = {} 37 | range('1-1')(object) 38 | expect(object).toEqual({ pageRanges: '1-1' }) 39 | }) 40 | 41 | test('Test `scale` function', () => { 42 | const object = {} 43 | scale(0.75)(object) 44 | expect(object).toEqual({ scale: 0.75 }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/manual/test_issue_13.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { convert, gotenberg, html, merge, pipe, please } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 thecodingmachine/gotenberg:6 6 | 7 | const toPDF = pipe(gotenberg('http://localhost:3500'), convert, html, please) 8 | const toMergedPDF = pipe(gotenberg('http://localhost:3500'), merge, please) 9 | 10 | async function createPdf(filename: string, content: string) { 11 | const pdf = await toPDF(content) 12 | pdf.pipe(createWriteStream(filename)) 13 | return new Promise((resolve) => pdf.on('end', resolve)) 14 | } 15 | 16 | async function main() { 17 | // create 18 | await createPdf(`${__dirname}/test_issue_13-file1.pdf`, 'pdf file 1') 19 | await createPdf(`${__dirname}/test_issue_13-file2.pdf`, 'pdf file 2') 20 | await createPdf(`${__dirname}/test_issue_13-file3.pdf`, 'pdf file 3') 21 | 22 | // merge 23 | // const mPdf = await toMergedPDF([ 24 | // ['file1.pdf', `file://${__dirname}/test_issue_13-file1.pdf`], 25 | // ['file2.pdf', `file://${__dirname}/test_issue_13-file2.pdf`], 26 | // ['file3.pdf', `file://${__dirname}/test_issue_13-file3.pdf`], 27 | // ]) 28 | const mPdf = await toMergedPDF([ 29 | `file://${__dirname}/test_issue_13-file1.pdf`, 30 | `file://${__dirname}/test_issue_13-file2.pdf`, 31 | `file://${__dirname}/test_issue_13-file3.pdf`, 32 | ]) 33 | mPdf.pipe(createWriteStream(`${__dirname}/test_issue_13-final.pdf`)) 34 | } 35 | 36 | main().catch(console.log) 37 | -------------------------------------------------------------------------------- /test/manual/test_issue_32.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | import { pipe, gotenberg, convert, url, to, please } from '../../src' 3 | 4 | // need to run Gotenberg like this 5 | // docker run --rm -p 3500:3000 gotenberg/gotenberg:7 6 | 7 | pipe( 8 | gotenberg(`http://localhost:3500/forms/chromium`), 9 | // gotenberg(`https://demo.gotenberg.dev/forms/chromium`), 10 | convert, 11 | url, 12 | to({ margins: [0, 0, 0, 0] }), 13 | (request) => { 14 | ;(request.fields as any).url = request.fields.remoteURL 15 | delete request.fields.remoteURL 16 | return request 17 | }, 18 | please 19 | )(`https://www.google.com`) 20 | .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/google.pdf`))) 21 | .catch(console.error) 22 | 23 | // pipe( 24 | // gotenberg(''), 25 | // convert, // it is save to remove this line, if you want 26 | // html, 27 | // adjust({ 28 | // // manually adjust endpoint, because 29 | // // gotenberg:7 has different conversion endpoints 30 | // url: 'http://localhost:3500/forms/chromium/convert/html', 31 | 32 | // // manually adjust for fields 33 | // fields: { 34 | // printBackground: true, 35 | // // `printBackground` is not valid field for gotenberg:6 36 | // // so we have to cast to any, otherwise typescript will complain 37 | // } as any, 38 | // }), 39 | // please 40 | // )('test') 41 | // .then((pdf) => pdf.pipe(createWriteStream(`${__dirname}/html_with_bg.pdf`))) 42 | // .catch(console.error) 43 | -------------------------------------------------------------------------------- /test/adjust.spec.ts: -------------------------------------------------------------------------------- 1 | import { adjust, Request, RequestType } from '../src' 2 | 3 | // dumb object to test purity 4 | const dumb: Request = { 5 | type: RequestType.Undefined, 6 | url: 'test', 7 | fields: { 8 | scale: 1, 9 | landscape: true, 10 | pageRanges: '1-2', 11 | }, 12 | client: { 13 | post: () => { 14 | throw new Error('not implemented') 15 | }, 16 | }, 17 | } 18 | 19 | test('Should adjust flat fields', () => { 20 | expect(adjust({})(dumb)).toEqual({ ...dumb }) 21 | expect(adjust({ url: 'changed' })(dumb)).toEqual({ ...dumb, url: 'changed' }) 22 | }) 23 | 24 | test('Should adjust deep fields', () => { 25 | expect(adjust({ fields: { landscape: false } })(dumb)).toEqual({ 26 | ...dumb, 27 | fields: { 28 | ...dumb.fields, 29 | landscape: false, 30 | }, 31 | }) 32 | expect(adjust({ headers: { Authorization: 'Bearer token' } })(dumb)).toEqual({ 33 | ...dumb, 34 | headers: { 35 | Authorization: 'Bearer token', 36 | }, 37 | }) 38 | expect( 39 | adjust({ headers: { Authorization: 'Bearer token' } })({ 40 | ...dumb, 41 | headers: { 'X-Header': 'test' }, 42 | }) 43 | ).toEqual({ 44 | ...dumb, 45 | headers: { 46 | Authorization: 'Bearer token', 47 | 'X-Header': 'test', 48 | }, 49 | }) 50 | }) 51 | 52 | test('Should replace deep fields', () => { 53 | expect( 54 | adjust({ headers: { Authorization: 'Bearer token' } })({ 55 | ...dumb, 56 | headers: { Authorization: 'Basic dXNlcjpwYXNzd29yZA==' }, 57 | }) 58 | ).toEqual({ ...dumb, headers: { Authorization: 'Bearer token' } }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/gotenberg.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' // tslint:disable-line no-circular-imports 2 | import { client as native } from './client/node' 3 | import { 4 | GotenbergClient, 5 | GotenbergClientClass, 6 | GotenbergClientFunction, 7 | Request, 8 | RequestType, 9 | Source, 10 | } from './_types' 11 | 12 | /** 13 | * Initializes Gotenberg request 14 | */ 15 | export const gotenberg = ( 16 | url: string | Buffer | URL, 17 | client?: GotenbergClient | GotenbergClientFunction | GotenbergClientClass | object, 18 | config?: object 19 | ) => { 20 | let instance: GotenbergClient 21 | 22 | // if GotenbergClient object / instance provided -> just use it 23 | if (typeof client === 'object' && 'post' in client) { 24 | instance = client 25 | } 26 | 27 | // if GotenbergClientFunction or GotenbergClientClass -> call or instantiate it 28 | else if (typeof client === 'function') { 29 | // there is no good way to distinguish regular function from class, 30 | // hope this will do ¯\_(ツ)_/¯ 31 | if (/^class\s/.test(Function.prototype.toString.call(client))) { 32 | // guess this is GotenbergClientClass 33 | instance = new (client as GotenbergClientClass)(config) 34 | } else { 35 | // guess this is GotenbergClientFunction 36 | instance = (client as GotenbergClientFunction)(config) 37 | } 38 | } 39 | 40 | // there is config object instead of client given 41 | // (or maybe it is just undefined) 42 | // -> use native client 43 | else { 44 | instance = native(config || client) 45 | } 46 | 47 | return (source: Source): Request => ({ 48 | type: RequestType.Undefined, 49 | url: url.toString(), 50 | client: instance, 51 | source, 52 | fields: {}, 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /test/merge.spec.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { merge, Request, RequestType } from '../src' 3 | 4 | // dumb object to test purity 5 | const dumb: Request = { 6 | type: RequestType.Undefined, 7 | url: 'test', 8 | fields: {}, 9 | client: { 10 | post: () => { 11 | throw new Error('not implemented') 12 | }, 13 | }, 14 | } 15 | 16 | test('Should accept iterable as source', () => { 17 | expect(() => merge({ ...dumb, source: [] })).not.toThrow() 18 | expect(() => merge({ ...dumb, source: new Map() })).not.toThrow() 19 | expect(() => merge({ ...dumb, source: new Set() })).not.toThrow() 20 | ;(function () { 21 | // use new function to get empty arguments 22 | expect(() => merge({ ...dumb, source: arguments })).not.toThrow() 23 | })() 24 | 25 | function* generator() {} // tslint:disable-line no-empty 26 | const iterator = { [Symbol.iterator]: generator } 27 | expect(() => merge({ ...dumb, source: iterator })).not.toThrow() 28 | expect(() => merge({ ...dumb, source: generator() })).not.toThrow() 29 | }) 30 | 31 | test('Should accept object as source', () => { 32 | expect(() => merge({ ...dumb, source: {} })).not.toThrow() 33 | }) 34 | 35 | test('Should fail on string, URL and Buffer', () => { 36 | expect(() => merge(dumb)).toThrow() 37 | expect(() => merge({ ...dumb, source: 'test' })).toThrow() 38 | expect(() => merge({ ...dumb, source: new URL('http://1') })).toThrow() 39 | expect(() => merge({ ...dumb, source: Buffer.from('test') })).toThrow() 40 | }) 41 | 42 | test('Should add `/merge` path and change type', () => { 43 | expect(merge({ ...dumb, source: [] })).toEqual({ 44 | ...dumb, 45 | source: [], 46 | type: RequestType.Merge, 47 | url: 'test/merge', 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/url.spec.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { Request, RequestType, url } from '../src' 3 | 4 | // dumb object to test purity 5 | const dumb: Request = { 6 | type: RequestType.Undefined, 7 | url: 'test', 8 | fields: {}, 9 | client: { 10 | post: () => { 11 | throw new Error('not implemented') 12 | }, 13 | }, 14 | } 15 | 16 | test('Should accept string and URL as source', () => { 17 | expect(() => url({ ...dumb, source: 'http://1' })).not.toThrow() 18 | expect(() => url({ ...dumb, source: new URL('http://1') })).not.toThrow() 19 | }) 20 | 21 | test('Should fail on any other source', function () { 22 | expect(() => url(dumb)).toThrow() 23 | expect(() => url({ ...dumb, source: [] })).toThrow() 24 | expect(() => url({ ...dumb, source: new Map() })).toThrow() 25 | expect(() => url({ ...dumb, source: new Set() })).toThrow() 26 | expect(() => url({ ...dumb, source: arguments })).toThrow() 27 | expect(() => url({ ...dumb, source: {} })).toThrow() 28 | expect(() => url({ ...dumb, source: Buffer.from('test') })).toThrow() 29 | 30 | function* generator() {} // tslint:disable-line no-empty 31 | const iterator = { [Symbol.iterator]: generator } 32 | expect(() => url({ ...dumb, source: iterator })).toThrow() 33 | expect(() => url({ ...dumb, source: generator() })).toThrow() 34 | }) 35 | 36 | test('Should add `/url` path, change type, remove source and set zero margins', () => { 37 | expect(url({ ...dumb, source: 'http://1' })).toEqual({ 38 | ...dumb, 39 | type: RequestType.Url, 40 | url: 'test/url', 41 | source: undefined, 42 | fields: { 43 | remoteURL: 'http://1', 44 | marginTop: 0, 45 | marginBottom: 0, 46 | marginLeft: 0, 47 | marginRight: 0, 48 | }, 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/to-helpers.ts: -------------------------------------------------------------------------------- 1 | import { FieldsModifier, MarginOptions, PaperOptions } from './_types' 2 | import { setProperty } from './tools/fn' 3 | import { 4 | A3, 5 | A4, 6 | A5, 7 | A6, 8 | LARGE_MARGINS, 9 | LEGAL, 10 | LETTER, 11 | NORMAL_MARGINS, 12 | NO_MARGINS, 13 | TABLOID, 14 | } from './page' 15 | 16 | /** 17 | * Modifies `landscape` form field to be true 18 | */ 19 | export const landscape: FieldsModifier = setProperty('landscape')(true) 20 | 21 | /** 22 | * Modifies `landscape` form field to be undefined (~ false) 23 | */ 24 | export const portrait: FieldsModifier = setProperty('landscape')() // == portrait is default orientation 25 | 26 | /** 27 | * Modifies paper size 28 | */ 29 | export const paperSize = (paper: PaperOptions): FieldsModifier => 30 | Array.isArray(paper) 31 | ? setProperty('paperWidth', 'paperHeight')(...paper) 32 | : setProperty('paperWidth', 'paperHeight')(paper.width, paper.height) 33 | 34 | // some predefined paper size modifiers 35 | export const a3 = paperSize(A3) 36 | export const a4 = paperSize(A4) 37 | export const a5 = paperSize(A5) 38 | export const a6 = paperSize(A6) 39 | export const legal = paperSize(LEGAL) 40 | export const letter = paperSize(LETTER) 41 | export const tabloid = paperSize(TABLOID) 42 | 43 | /** 44 | * Modifies margins 45 | */ 46 | export const marginSizes = (margins: MarginOptions): FieldsModifier => 47 | Array.isArray(margins) 48 | ? setProperty('marginTop', 'marginRight', 'marginBottom', 'marginLeft')(...margins) 49 | : setProperty('marginTop', 'marginRight', 'marginBottom', 'marginLeft')( 50 | margins.top, 51 | margins.right, 52 | margins.bottom, 53 | margins.left 54 | ) 55 | 56 | // some predefined margins modifiers 57 | export const noMargins = marginSizes(NO_MARGINS) 58 | export const normalMargins = marginSizes(NORMAL_MARGINS) 59 | export const largeMargins = marginSizes(LARGE_MARGINS) 60 | -------------------------------------------------------------------------------- /test/internal/type.spec.ts: -------------------------------------------------------------------------------- 1 | import { Request, RequestType } from '../../src' 2 | import { type } from '../../src/internal/type' 3 | 4 | // dumb object to test purity 5 | const dumb = { type: RequestType.Undefined, url: 'test' } 6 | 7 | test('Should change type to Url', () => { 8 | const fn = type(RequestType.Url) 9 | expect(typeof fn).toBe('function') 10 | expect(fn(dumb as Request)).toEqual({ 11 | ...dumb, 12 | type: RequestType.Url, 13 | }) 14 | }) 15 | 16 | test('Should change type to Ping', () => { 17 | const fn = type(RequestType.Ping) 18 | expect(typeof fn).toBe('function') 19 | expect(fn(dumb as Request)).toEqual({ 20 | ...dumb, 21 | type: RequestType.Ping, 22 | }) 23 | }) 24 | 25 | test('Should change type to Html', () => { 26 | const fn = type(RequestType.Html) 27 | expect(typeof fn).toBe('function') 28 | expect(fn(dumb as Request)).toEqual({ 29 | ...dumb, 30 | type: RequestType.Html, 31 | }) 32 | }) 33 | 34 | test('Should change type to Merge', () => { 35 | const fn = type(RequestType.Merge) 36 | expect(typeof fn).toBe('function') 37 | expect(fn(dumb as Request)).toEqual({ 38 | ...dumb, 39 | type: RequestType.Merge, 40 | }) 41 | }) 42 | 43 | test('Should change type to Office', () => { 44 | const fn = type(RequestType.Office) 45 | expect(typeof fn).toBe('function') 46 | expect(fn(dumb as Request)).toEqual({ 47 | ...dumb, 48 | type: RequestType.Office, 49 | }) 50 | }) 51 | 52 | test('Should change type to Markdown', () => { 53 | const fn = type(RequestType.Markdown) 54 | expect(typeof fn).toBe('function') 55 | expect(fn(dumb as Request)).toEqual({ 56 | ...dumb, 57 | type: RequestType.Markdown, 58 | }) 59 | }) 60 | 61 | test('Should throw on double conversion', () => { 62 | const request = { type: RequestType.Url } as Request 63 | const fn = type(RequestType.Office) 64 | expect(() => fn(request)).toThrow(`Cannot set "Office" conversion, already set to "Url"`) 65 | }) 66 | -------------------------------------------------------------------------------- /src/to.ts: -------------------------------------------------------------------------------- 1 | import { ConversionOptions, MarginOptions, PaperOptions, Request, RequestFields } from './_types' 2 | import { fields } from './internal/fields' 3 | import { marginSizes, paperSize } from './to-helpers' 4 | 5 | /** 6 | * Adjust Request fields, for any request 7 | * @return new typed Request, doesn't modify original Request 8 | */ 9 | export const to: { 10 | (...opts: ConversionOptions[]): (request: RequestEx) => RequestEx 11 | } = (...opts): any => { 12 | const options: RequestFields = {} 13 | 14 | // page size and margins options 15 | let paper: PaperOptions | undefined 16 | let margins: MarginOptions | undefined 17 | 18 | // check every given option 19 | for (let i = opts.length; i--; ) { 20 | const op = opts[i] 21 | 22 | // this is fields modifier helper 23 | if (typeof op === 'function') { 24 | op(options) 25 | continue 26 | } 27 | 28 | // this is definitely page size or margins 29 | if (Array.isArray(op)) { 30 | if (op.length === 2) paper = op as [number, number] 31 | if (op.length === 4) margins = op as [number, number, number, number] 32 | continue 33 | } 34 | 35 | // check if options is page size object 36 | if ('width' in op || 'height' in op) { 37 | paper = op 38 | continue 39 | } 40 | 41 | // check if options is margins object 42 | if ('top' in op || 'right' in op || 'bottom' in op || 'left' in op) { 43 | margins = op 44 | continue 45 | } 46 | 47 | // check page field 48 | if ('paper' in op && op.paper) { 49 | paper = op.paper 50 | op.paper = undefined // eliminate page from option 51 | } 52 | 53 | // check margins field 54 | if ('margins' in op && op.margins) { 55 | margins = op.margins 56 | op.margins = undefined // eliminate margins from option 57 | } 58 | 59 | // extends result options with given option 60 | Object.assign(options, op) 61 | } 62 | 63 | // update page size and margins, if we have some 64 | paper && paperSize(paper)(options) 65 | margins && marginSizes(margins)(options) 66 | 67 | return fields(options) 68 | } 69 | -------------------------------------------------------------------------------- /test/gotenberg.spec.ts: -------------------------------------------------------------------------------- 1 | import { RequestType } from '../src' 2 | import { gotenberg } from '../src/gotenberg' 3 | 4 | test('Test `gotenberg` function without client', () => { 5 | const fn = gotenberg('http://120.0.0.1:3000') 6 | expect(typeof fn).toBe('function') 7 | expect(fn('test')).toMatchObject({ 8 | type: RequestType.Undefined, 9 | url: 'http://120.0.0.1:3000', 10 | source: 'test', 11 | fields: {}, 12 | }) 13 | }) 14 | 15 | test('Test `gotenberg` function with custom client', () => { 16 | const client = { 17 | get() {}, // tslint:disable-line: no-empty 18 | post() {}, // tslint:disable-line: no-empty 19 | } 20 | const fn = gotenberg('http://120.0.0.1:3000', client) 21 | expect(typeof fn).toBe('function') 22 | expect(fn('test')).toEqual({ 23 | type: RequestType.Undefined, 24 | client, 25 | url: 'http://120.0.0.1:3000', 26 | source: 'test', 27 | fields: {}, 28 | }) 29 | }) 30 | 31 | test('Test `gotenberg` function with custom functional client', () => { 32 | const mock = jest.fn() 33 | const clientImpl = { 34 | get() {}, // tslint:disable-line: no-empty 35 | post() {}, // tslint:disable-line: no-empty 36 | } 37 | const client = function (arg: any) { 38 | mock(arg) 39 | return clientImpl 40 | } 41 | const fn = gotenberg('http://120.0.0.1:3000', client, { base: 'test' }) 42 | expect(typeof fn).toBe('function') 43 | expect(fn('test')).toEqual({ 44 | type: RequestType.Undefined, 45 | client: clientImpl, 46 | url: 'http://120.0.0.1:3000', 47 | source: 'test', 48 | fields: {}, 49 | }) 50 | expect(mock.mock.calls.length).toBe(1) 51 | expect(mock.mock.calls[0][0]).toEqual({ base: 'test' }) 52 | }) 53 | 54 | test('Test `gotenberg` function with custom class client', () => { 55 | const mock = jest.fn() 56 | class Client { 57 | constructor(arg: any) { 58 | mock(arg) 59 | } 60 | get() {} // tslint:disable-line: no-empty 61 | post() {} // tslint:disable-line: no-empty 62 | } 63 | const fn = gotenberg('http://120.0.0.1:3000', Client, { base: 'test' }) 64 | expect(typeof fn).toBe('function') 65 | expect(fn('test')).toEqual({ 66 | type: RequestType.Undefined, 67 | client: expect.any(Client), 68 | url: 'http://120.0.0.1:3000', 69 | source: 'test', 70 | fields: {}, 71 | }) 72 | expect(mock.mock.calls.length).toBe(1) 73 | expect(mock.mock.calls[0][0]).toEqual({ base: 'test' }) 74 | }) 75 | -------------------------------------------------------------------------------- /.yaspeller.json: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": [".git", ".vscode", "node_modules", "lib"], 3 | "lang": "en", 4 | "fileExtensions": [".md"], 5 | // "fileExtensions": [".md", ".ts"], 6 | "dictionary": [ 7 | "(G|g)otenberg", 8 | "(J|T)S", 9 | "TypeScript", 10 | "(M|m)arkdown", 11 | "Node(js|JS)", 12 | "TODO", 13 | "Setplex", 14 | 15 | "toBe", 16 | "toThrow", 17 | "enum", 18 | "typeof", 19 | "instanceof", 20 | "startsWith", 21 | "int", 22 | "pdf", 23 | "odt", 24 | "pptx", 25 | "odp", 26 | "stylesheets", 27 | "docx", 28 | "unoconv", 29 | "rtf", 30 | "clnt", 31 | "noop", 32 | "src", 33 | "uri", 34 | "fn", 35 | "fs", 36 | "fns", 37 | "param", 38 | "req", 39 | "tslint", 40 | "fileNameRE", 41 | "statusCode", 42 | "statusMessage", 43 | "basename", 44 | "extname", 45 | "webhook", 46 | "formdata", 47 | "iterables", 48 | "getHeaders", 49 | "isArray", 50 | "isBuffer", 51 | "isFileName", 52 | "isFileUri", 53 | "isIterable", 54 | "isObject", 55 | "isPlain", 56 | "isStream", 57 | "isString", 58 | "isTuple", 59 | "isURL", 60 | "toStream", 61 | "toStreams", 62 | "toEqual", 63 | "toTuples", 64 | "fromFile", 65 | "createReadStream", 66 | 67 | "waitTimeout", 68 | "webhookURL", 69 | "webhookURLTimeout", 70 | "resultFilename", 71 | "waitDelay", 72 | "googleChromeRpccBufferSize", 73 | "paper(Width|Height)", 74 | "margin(Top|Bottom|Left|Right)", 75 | "landscape", 76 | "remoteURL", 77 | "paperSize", 78 | "marginSizes", 79 | "noMargins", 80 | "normalMargins", 81 | "largeMargins", 82 | 83 | "(a|A|T|f|z|Z)[0-9a]", 84 | 85 | "FormData", 86 | "(I|i)terable", 87 | "ReadableStream", 88 | "ReadStream", 89 | "GotenbergClient", 90 | "GotenbergClient(Class|Function)", 91 | "RequestType", 92 | "FileURI", 93 | "RequestFields", 94 | "(Common|Chrome|Html|Markdown|Office|Url|Merge)RequestFields", 95 | "FieldsModifier", 96 | "(Paper|Margin|Conversion)Options", 97 | "Source", 98 | "PlainSources", 99 | "(Plain|Tuple|Object|TupleStreams)Source", 100 | "Request", 101 | "RequestEx", 102 | "(Url|Ping|Html|Merge|Office|Markdown|Typed)Request" 103 | ], 104 | "ignoreText": [], 105 | "ignoreUrls": true, 106 | "ignoreUppercase": true, 107 | "findRepeatWords": false, 108 | "maxRequests": 5 109 | } 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gotenberg-js-client", 3 | "version": "0.7.4", 4 | "description": "A simple JS/TS for interacting with a Gotenberg API", 5 | "author": "Victor Didenko (https://yumaa.name)", 6 | "license": "MIT", 7 | "keywords": [ 8 | "gotenberg", 9 | "pdf", 10 | "puppeteer", 11 | "unoconv" 12 | ], 13 | "scripts": { 14 | "dev": "ts-node src/index.ts", 15 | "test": "jest", 16 | "build": "rm -rf pkg/ && pika build", 17 | "format": "prettier --write \"src/**/*.ts\" --write \"test/**/*.ts\"", 18 | "lint": "tslint -p tsconfig.json && yarn spell", 19 | "spell": "yaspeller .", 20 | "release": "pika publish", 21 | "version": "yarn build", 22 | "size": "size-limit" 23 | }, 24 | "size-limit": [ 25 | { 26 | "path": "pkg/dist-node/index.js", 27 | "webpack": false, 28 | "limit": "4293 B" 29 | } 30 | ], 31 | "@pika/pack": { 32 | "pipeline": [ 33 | [ 34 | "@pika/plugin-ts-standard-pkg" 35 | ], 36 | [ 37 | "pika-plugin-typedefs-to-flow" 38 | ], 39 | [ 40 | "@pika/plugin-build-node", 41 | { 42 | "minNodeVersion": "10" 43 | } 44 | ], 45 | [ 46 | "pika-plugin-package.json", 47 | { 48 | "+author": "^", 49 | "*files": [ 50 | "-bin/" 51 | ], 52 | "-devDependencies": {} 53 | } 54 | ] 55 | ] 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/yumauri/gotenberg-js-client" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/yumauri/gotenberg-js-client/issues" 63 | }, 64 | "homepage": "https://github.com/yumauri/gotenberg-js-client#readme", 65 | "dependencies": { 66 | "form-data": "^4.0.0" 67 | }, 68 | "devDependencies": { 69 | "@pika/pack": "^0.5.0", 70 | "@pika/plugin-build-node": "^0.9.2", 71 | "@pika/plugin-ts-standard-pkg": "^0.9.2", 72 | "@size-limit/preset-small-lib": "^4.11.0", 73 | "@types/jest": "^26.0.23", 74 | "@types/node": "^15.6.1", 75 | "flowgen": "^1.14.1", 76 | "jest": "^27.0.1", 77 | "nock": "^13.0.11", 78 | "pika-plugin-package.json": "^1.0.2", 79 | "pika-plugin-typedefs-to-flow": "^0.0.3", 80 | "prettier": "^2.3.0", 81 | "size-limit": "^4.11.0", 82 | "ts-jest": "^27.0.1", 83 | "ts-node": "^10.0.0", 84 | "tslint": "^6.1.3", 85 | "tslint-config-prettier": "^1.18.0", 86 | "tslint-config-security": "^1.16.0", 87 | "tslint-config-standard-plus": "^2.3.0", 88 | "typescript": "^4.3.2", 89 | "yaspeller": "^7.0.0" 90 | }, 91 | "engines": { 92 | "node": ">=12.0.0" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/to.spec.ts: -------------------------------------------------------------------------------- 1 | import { a4, A4, HtmlRequest, landscape, noMargins, NO_MARGINS, RequestType, to } from '../src' 2 | 3 | // dumb object to test purity 4 | const dumb: HtmlRequest = { 5 | type: RequestType.Html, 6 | url: 'test', 7 | fields: {}, 8 | client: { 9 | post: () => { 10 | throw new Error('not implemented') 11 | }, 12 | }, 13 | } 14 | 15 | test('Should set static fields', () => { 16 | expect(to()(dumb)).toEqual({ ...dumb }) 17 | expect(to({})(dumb)).toEqual({ ...dumb }) 18 | expect(to({ landscape: true })(dumb)).toEqual({ 19 | ...dumb, 20 | fields: { landscape: true }, 21 | }) 22 | expect(to({ landscape: true })(dumb)).toEqual({ 23 | ...dumb, 24 | fields: { landscape: true }, 25 | }) 26 | }) 27 | 28 | test('Should set fields using modifiers', () => { 29 | expect(to(a4)(dumb)).toEqual({ 30 | ...dumb, 31 | fields: { 32 | paperWidth: A4[0], 33 | paperHeight: A4[1], 34 | }, 35 | }) 36 | expect(to(landscape)(dumb)).toEqual({ 37 | ...dumb, 38 | fields: { landscape: true }, 39 | }) 40 | }) 41 | 42 | test('Should set "my" static fields', () => { 43 | expect(to({ paper: A4 })(dumb)).toEqual({ 44 | ...dumb, 45 | fields: { 46 | paperWidth: A4[0], 47 | paperHeight: A4[1], 48 | }, 49 | }) 50 | expect(to(A4)(dumb)).toEqual({ 51 | ...dumb, 52 | fields: { 53 | paperWidth: A4[0], 54 | paperHeight: A4[1], 55 | }, 56 | }) 57 | expect(to({ width: A4[0] })(dumb)).toEqual({ 58 | ...dumb, 59 | fields: { 60 | paperWidth: A4[0], 61 | }, 62 | }) 63 | expect(to({ margins: NO_MARGINS })(dumb)).toEqual({ 64 | ...dumb, 65 | fields: { 66 | marginTop: NO_MARGINS[0], 67 | marginRight: NO_MARGINS[1], 68 | marginBottom: NO_MARGINS[2], 69 | marginLeft: NO_MARGINS[3], 70 | }, 71 | }) 72 | expect(to(NO_MARGINS)(dumb)).toEqual({ 73 | ...dumb, 74 | fields: { 75 | marginTop: NO_MARGINS[0], 76 | marginRight: NO_MARGINS[1], 77 | marginBottom: NO_MARGINS[2], 78 | marginLeft: NO_MARGINS[3], 79 | }, 80 | }) 81 | expect(to({ top: 10, left: 20 })(dumb)).toEqual({ 82 | ...dumb, 83 | fields: { 84 | marginTop: 10, 85 | marginLeft: 20, 86 | }, 87 | }) 88 | }) 89 | 90 | test('Should set fields both, using modifiers and static', () => { 91 | expect(to(noMargins, { landscape: true })(dumb)).toEqual({ 92 | ...dumb, 93 | fields: { 94 | marginTop: NO_MARGINS[0], 95 | marginRight: NO_MARGINS[1], 96 | marginBottom: NO_MARGINS[2], 97 | marginLeft: NO_MARGINS[3], 98 | landscape: true, 99 | }, 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /src/set-helpers.ts: -------------------------------------------------------------------------------- 1 | import { FieldsModifier } from './_types' 2 | import { setProperty } from './tools/fn' 3 | 4 | /** 5 | * Modifies `resultFilename` form field 6 | */ 7 | export const filename: { 8 | (name: string): FieldsModifier 9 | } = setProperty('resultFilename') 10 | 11 | /** 12 | * Modifies `waitTimeout` form field 13 | * 14 | * By default, the value of the form field waitTimeout cannot be more than 30 seconds. 15 | * You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_TIMEOUT`. 16 | * https://thecodingmachine.github.io/gotenberg/#environment_variables.maximum_wait_timeout 17 | */ 18 | export const timeout: { 19 | (timeout: number): FieldsModifier 20 | } = setProperty('waitTimeout') 21 | 22 | /** 23 | * Modifies `waitDelay` form field 24 | * 25 | * By default, the value of the form field waitDelay cannot be more than 10 seconds. 26 | * You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_DELAY`. 27 | * https://thecodingmachine.github.io/gotenberg/#environment_variables.maximum_wait_delay 28 | */ 29 | export const delay: { 30 | (delay: number): FieldsModifier 31 | } = setProperty('waitDelay') 32 | 33 | /** 34 | * Modifies `webhookURL` and `webhookURLTimeout` form fields 35 | * 36 | * By default, the value of the form field webhookURLTimeout cannot be more than 30 seconds. 37 | * You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WEBHOOK_URL_TIMEOUT`. 38 | * https://thecodingmachine.github.io/gotenberg/#environment_variables.maximum_webhook_url_timeout 39 | */ 40 | export const webhook: { 41 | (url: string, timeout?: number): FieldsModifier 42 | } = setProperty('webhookURL', 'webhookURLTimeout') 43 | 44 | /** 45 | * Modifies `googleChromeRpccBufferSize` form field 46 | * 47 | * You may increase this buffer size with the environment variable `DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE`. 48 | * The hard limit is 100 MB (= 1_048_576_000 B) and is defined by Google Chrome itself. 49 | * https://thecodingmachine.github.io/gotenberg/#environment_variables.default_google_chrome_rpcc_buffer_size 50 | */ 51 | export const googleChromeRpccBufferSize: { 52 | (googleChromeRpccBufferSize: number): FieldsModifier 53 | } = setProperty('googleChromeRpccBufferSize') 54 | 55 | /** 56 | * Modifies `pageRanges` form field 57 | * 58 | * https://thecodingmachine.github.io/gotenberg/#html.page_ranges 59 | * https://thecodingmachine.github.io/gotenberg/#office.page_ranges 60 | */ 61 | export const range: { 62 | (range: string): FieldsModifier 63 | } = setProperty('pageRanges') 64 | 65 | /** 66 | * Modifies `scale` form field 67 | * 68 | * https://thecodingmachine.github.io/gotenberg/#html.paper_size_margins_orientation_scaling 69 | */ 70 | export const scale: { 71 | (scale: number): FieldsModifier 72 | } = setProperty('scale') 73 | -------------------------------------------------------------------------------- /test/internal/source-converters.spec.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { Readable } from 'stream' 3 | import { basename } from 'path' 4 | import { createReadStream, ReadStream } from 'fs' 5 | import { 6 | DEFAULT_FILENAME, 7 | fromFile, 8 | toStream, 9 | toStreams, 10 | toTuples, 11 | } from '../../src/internal/source-converters' 12 | 13 | // tslint:disable no-any 14 | 15 | test('Test `toTuples` function', () => { 16 | expect(() => toTuples(undefined as any)).toThrow() 17 | expect(toTuples(new URL('http://1'))).toEqual([]) 18 | expect(toTuples('file://test.html')).toEqual([[DEFAULT_FILENAME, 'file://test.html']]) 19 | expect(toTuples('file://test.doc')).toEqual([['test.doc', 'file://test.doc']]) 20 | expect(toTuples('test')).toEqual([[DEFAULT_FILENAME, 'test']]) 21 | 22 | const buffer = Buffer.from('test') 23 | expect(toTuples(buffer)).toEqual([[DEFAULT_FILENAME, buffer]]) 24 | 25 | const stream = new Readable() 26 | expect(toTuples(stream)).toEqual([[DEFAULT_FILENAME, stream]]) 27 | 28 | const file = createReadStream(__filename) 29 | expect(toTuples(file)).toEqual([[basename(__filename), file]]) 30 | 31 | expect(toTuples(['index.html', 'test'])).toEqual([['index.html', 'test']]) 32 | expect(toTuples({ 'index.html': 'test' })).toEqual([['index.html', 'test']]) 33 | 34 | const map = new Map() 35 | map.set('index.html', 'test') 36 | expect(toTuples(map)).toEqual([['index.html', 'test']]) 37 | 38 | const set = new Set() 39 | set.add(new Set()) 40 | expect(() => toTuples(set)).toThrow('Bad source, don\'t know what to do with "[object Set]"') 41 | }) 42 | 43 | test('Test `toTuples` function, different edge cases', () => { 44 | // line 46 45 | const file = createReadStream(`${__dirname}/../manual/statement.html`) 46 | expect(toTuples(file)).toEqual([['index.html', file]]) 47 | 48 | // line 66, `hasOwnProperty` 49 | function Src(this: any) { 50 | this['index.html'] = 'test' 51 | } 52 | Src.prototype['header.html'] = '' 53 | expect(toTuples(new Src())).toEqual([['index.html', 'test']]) 54 | }) 55 | 56 | test('Test `fromFile` function', () => { 57 | expect(fromFile('file:' + __filename) instanceof ReadStream).toBe(true) 58 | expect(fromFile('file://' + __filename) instanceof ReadStream).toBe(true) 59 | }) 60 | 61 | test('Test `toStream` function', async () => { 62 | const stream0 = new Readable() 63 | expect(toStream(stream0)).toEqual(stream0) 64 | expect(toStream('file:' + __filename) instanceof ReadStream).toBe(true) 65 | expect(toStream('test') instanceof Readable).toBe(true) 66 | 67 | const chunks: any[] = [] 68 | const stream = toStream('test') 69 | const result = await new Promise((resolve, reject) => { 70 | stream.on('data', (chunk) => chunks.push(chunk)) 71 | stream.on('error', reject) 72 | stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) 73 | }) 74 | expect(result).toEqual('test') 75 | }) 76 | 77 | test('Test `toStreams` function', () => { 78 | expect(toStreams()).toEqual([]) 79 | 80 | const result1 = toStreams(Buffer.from('test')) 81 | expect(result1 instanceof Array).toBe(true) 82 | expect(result1[0] instanceof Array).toBe(true) 83 | expect(result1[0][0]).toEqual(DEFAULT_FILENAME) 84 | expect(result1[0][1] instanceof Readable).toBe(true) 85 | }) 86 | -------------------------------------------------------------------------------- /src/internal/source-checkers.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' // tslint:disable-line no-circular-imports 2 | import { Readable } from 'stream' 3 | import { FileURI, ObjectSource, PlainSource, Source, TupleSource } from '../_types' 4 | 5 | /** 6 | * Check if argument is String 7 | */ 8 | export const isString = (x: Source | undefined | null): x is string => typeof x === 'string' 9 | 10 | /** 11 | * Check if argument is Buffer 12 | */ 13 | export const isBuffer = (x: Source | undefined | null): x is Buffer => 14 | x != null && x instanceof Buffer 15 | 16 | /** 17 | * Check if argument is Stream 18 | */ 19 | export const isStream = (x: Source | undefined | null): x is NodeJS.ReadableStream => 20 | x != null && x instanceof Readable 21 | 22 | /** 23 | * Check if argument is URL 24 | */ 25 | export const isURL = (x: Source | undefined | null): x is URL => x != null && x instanceof URL 26 | 27 | /** 28 | * Check if argument is uri to local file 29 | * https://en.wikipedia.org/wiki/File_URI_scheme 30 | */ 31 | export const isFileUri = (x: Source | undefined | null): x is FileURI => 32 | isString(x) && x.startsWith('file:') 33 | 34 | /** 35 | * Check if argument is PlainSource - either String, Stream or Buffer 36 | */ 37 | export const isPlain = (x: Source | undefined | null): x is PlainSource => 38 | isString(x) || isStream(x) || isBuffer(x) // || isFileUri(x) <- redundant check 39 | 40 | /** 41 | * Check if argument is TupleSource - two-values array, like [key, PlainSource] 42 | */ 43 | export const isTuple = (x: object | undefined | null): x is TupleSource => 44 | Array.isArray(x) && x.length === 2 && typeof x[0] === 'string' && isPlain(x[1]) 45 | 46 | /** 47 | * Check if argument is ObjectSource, with PlainSources inside 48 | */ 49 | export const isObject = (x: Source | undefined | null): x is ObjectSource => { 50 | if ( 51 | x == null || 52 | typeof x !== 'object' || 53 | Array.isArray(x) || 54 | typeof x[Symbol.iterator] === 'function' || 55 | x instanceof URL 56 | ) { 57 | return false 58 | } 59 | for (const key in x) { 60 | if (x.hasOwnProperty(key) && !isPlain(x[key])) return false 61 | } 62 | return true 63 | } 64 | 65 | /** 66 | * Check if argument is Iterable over PlainSource or TupleSource 67 | */ 68 | export const isIterable = ( 69 | x: Source | undefined | null 70 | ): x is Iterable => { 71 | if (x == null || typeof x === 'string') return false 72 | if (typeof x[Symbol.iterator] === 'function') { 73 | for (const src of x as Iterable) { 74 | if (src == null) return false 75 | if (!isPlain(src) && !isTuple(src) && !isObject(src)) return false 76 | } 77 | return true 78 | } 79 | return false 80 | } 81 | 82 | /** 83 | * Check, if given argument is simple string, and presumably is is just file name 84 | */ 85 | const filenameRE = /.+\..+/ 86 | const filenameReservedRE = /[<>:"/\\|?*\u0000-\u001F]/g 87 | const windowsReservedNameRE = /^(con|prn|aux|nul|com\d|lpt\d)$/i 88 | const MAX_FILE_NAME_LENGTH = 255 89 | export const isFileName = (x: Source | undefined | null) => 90 | isString(x) && 91 | x.length <= MAX_FILE_NAME_LENGTH && 92 | x !== '.' && 93 | x !== '..' && 94 | !x.startsWith('file:') && // in ideal world there should be `!isFileUri(x)`, but TypeScript sucks here 95 | !filenameReservedRE.test(x) && 96 | !windowsReservedNameRE.test(x) && 97 | filenameRE.test(x) 98 | -------------------------------------------------------------------------------- /src/client/node.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import https from 'https' 3 | import FormData from 'form-data' 4 | import { URL } from 'url' 5 | import { GotenbergClientFunction } from '../_types' 6 | 7 | /** 8 | * Little helper to parse url and get needed request method - either HTTP or HTTPS 9 | * @param url 10 | */ 11 | const parse = (url: string): [URL, typeof http.request | typeof https.request] => { 12 | const _url = new URL(url) 13 | const request = _url.protocol === 'http:' ? http.request : https.request 14 | return [_url, request] 15 | } 16 | 17 | /** 18 | * Perform POST request to Gotenberg API 19 | */ 20 | export function post( 21 | this: object | null, 22 | url: string, 23 | data: FormData, 24 | headers?: http.OutgoingHttpHeaders 25 | ): Promise { 26 | const [_url, request] = parse(url) 27 | return new Promise((resolve, reject) => { 28 | const req = request(_url, { 29 | method: 'POST', 30 | ...this, // extends with config options 31 | headers: { 32 | ...data.getHeaders(), 33 | ...headers, 34 | ...(this ? (this as any).headers : null), // extends with config headers 35 | }, 36 | }) 37 | 38 | req.on('error', reject) 39 | req.on('response', (res) => { 40 | if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { 41 | resolve(res) 42 | } else { 43 | let error = res.statusCode + ' ' + res.statusMessage 44 | 45 | // something is wrong, get error message from Gotenberg 46 | const chunks: Buffer[] = [] 47 | res.on('data', (chunk: Buffer) => chunks.push(chunk)) 48 | res.on('end', () => { 49 | try { 50 | error += ' (' + JSON.parse(Buffer.concat(chunks).toString()).message + ')' 51 | } catch (err) { 52 | // ignore 53 | } 54 | reject(new Error(error)) 55 | }) 56 | } 57 | }) 58 | 59 | data.pipe(req) // pipe Form data to request 60 | // pipe should automatically call `req.end()` after stream ends 61 | }) 62 | } 63 | 64 | /** 65 | * Perform POST request to Gotenberg API 66 | */ 67 | export function get(this: object | null, url: string): Promise { 68 | const [_url, request] = parse(url) 69 | return new Promise((resolve, reject) => { 70 | const req = request(_url, { 71 | method: 'GET', 72 | ...this, // extends with config options 73 | }) 74 | 75 | req.on('error', reject) 76 | req.on('response', (res) => { 77 | if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { 78 | resolve(res) 79 | } else { 80 | res.resume() // ignore response body 81 | reject(new Error(res.statusCode + ' ' + res.statusMessage)) 82 | } 83 | }) 84 | 85 | req.end() // send request 86 | }) 87 | } 88 | 89 | /** 90 | * GotenbergClient function implementation. 91 | * 92 | * Uses native Nodejs modules `http` and `https`, can accept any options 93 | * for http -> https://nodejs.org/docs/latest-v10.x/api/http.html#http_http_request_options_callback 94 | * for https -> https://nodejs.org/docs/latest-v10.x/api/https.html#https_https_request_options_callback 95 | */ 96 | export const client: GotenbergClientFunction = (config?: object) => ({ 97 | post: post.bind(config || null), 98 | get: get.bind(config || null), 99 | }) 100 | -------------------------------------------------------------------------------- /src/please.ts: -------------------------------------------------------------------------------- 1 | import FormData from 'form-data' 2 | import { PingRequest, RequestFields, RequestType, TupleStreamsSource, TypedRequest } from './_types' 3 | import { DEFAULT_FILENAME, toStreams } from './internal/source-converters' 4 | 5 | /** 6 | * Helper function to convert fields and files to form data 7 | * https://github.com/form-data/form-data 8 | */ 9 | const formdata = (fields: RequestFields, files: TupleStreamsSource[]) => { 10 | const data = new FormData() 11 | 12 | // append all form values 13 | for (const field in fields) { 14 | if (fields.hasOwnProperty(field)) { 15 | const value = fields[field] 16 | if (value !== undefined) { 17 | data.append(field, String(value)) 18 | } 19 | } 20 | } 21 | 22 | // append all form files 23 | for (let i = 0; i < files.length; i++) { 24 | const file = files[i] 25 | data.append(file[0], file[1], { filename: file[0] }) 26 | } 27 | 28 | return data 29 | } 30 | 31 | /** 32 | * Validate sources' file names 33 | */ 34 | const validateSources = ( 35 | type: RequestType, 36 | sources: TupleStreamsSource[] 37 | ): TupleStreamsSource[] => { 38 | const filenames = sources.map((source) => source[0]) 39 | 40 | // check for duplicates 41 | const duplicates = filenames.filter((name, index, arr) => arr.indexOf(name) !== index) 42 | if (duplicates.length > 0) { 43 | throw new Error(`There are duplicates in file names: ${duplicates.join(',')}`) 44 | } 45 | 46 | // check sources against request type 47 | 48 | const hasDefault = filenames.includes(DEFAULT_FILENAME) 49 | if ((type === RequestType.Html || type === RequestType.Markdown) && !hasDefault) { 50 | throw new Error( 51 | `File "${DEFAULT_FILENAME}" is required for ${ 52 | type === RequestType.Html ? 'HTML' : 'Markdown' 53 | } conversion` 54 | ) 55 | } 56 | 57 | if (type === RequestType.Office && hasDefault) { 58 | throw new Error( 59 | `Default filename "${DEFAULT_FILENAME}" is not allowed for Office conversion, ` + 60 | `looks like you didn't set filename for document` 61 | ) 62 | } 63 | 64 | return sources 65 | } 66 | 67 | /** 68 | * Send actual request to Gotenberg 69 | * @return ReadableStream 70 | */ 71 | export const please: { 72 | (request: T): Promise 73 | (request: T): Promise 74 | } = (request: TypedRequest): any => { 75 | // ping request 76 | // https://thecodingmachine.github.io/gotenberg/#ping 77 | if (request.type === RequestType.Ping) { 78 | if (typeof request.client.get === 'function') { 79 | return request.client.get(request.url).then((response) => { 80 | // https://nodejs.org/docs/latest-v10.x/api/http.html#http_class_http_clientrequest 81 | // If no 'response' handler is added, then the response will be entirely discarded. 82 | // However, if a 'response' event handler is added, then the data from the response 83 | // object must be consumed, either by calling response.read() whenever there is 84 | // a 'readable' event, or by adding a 'data' handler, or by calling the .resume() method. 85 | // Until the data is consumed, the 'end' event will not fire. Also, until the data 86 | // is read it will consume memory that can eventually lead to a 'process out of memory' error. 87 | response.resume() 88 | }) 89 | } 90 | throw new Error(`Gotenberg client doesn't implements "get" method`) 91 | } 92 | 93 | // any other conversion request 94 | const sources = validateSources(request.type, toStreams(request.source)) 95 | const form = formdata(request.fields, sources) 96 | return request.client.post(request.url, form, request.headers) 97 | } 98 | -------------------------------------------------------------------------------- /test/client/node.spec.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | import FormData from 'form-data' 3 | import { client } from '../../src/client/node' 4 | 5 | // Helper function to get response JSON body 6 | async function toJSON(response: any) { 7 | const chunks: any[] = [] 8 | const text = await new Promise((resolve, reject) => { 9 | response.on('data', (chunk: any) => chunks.push(chunk)) 10 | response.on('error', reject) 11 | response.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) 12 | }) 13 | return JSON.parse(text) 14 | } 15 | 16 | test('`client` should return functional client', () => { 17 | const clnt = client() 18 | expect(typeof clnt.get).toBe('function') 19 | expect(typeof clnt.post).toBe('function') 20 | }) 21 | 22 | test('Should do GET request', async () => { 23 | nock('https://127.0.0.1:3000') // 24 | .get('/ping') 25 | .reply(200, { status: 'OK' }) 26 | 27 | const clnt = client() 28 | const response = await clnt.get!('https://127.0.0.1:3000/ping') 29 | expect(await toJSON(response)).toEqual({ status: 'OK' }) 30 | }) 31 | 32 | test('Should throw on bad GET request', async () => { 33 | nock('https://127.0.0.1:3000') // 34 | .get('/ping') 35 | .reply(500) 36 | 37 | const clnt = client() 38 | let error: any 39 | try { 40 | await clnt.get!('https://127.0.0.1:3000/ping') 41 | } catch (e) { 42 | error = e 43 | } 44 | expect(error).toEqual(new Error('500 null')) 45 | }) 46 | 47 | test('Should do POST request', async () => { 48 | nock('https://127.0.0.1:3000') // 49 | .post('/convert/html') 50 | .reply(200, { status: 'OK' }) 51 | 52 | const clnt = client() 53 | const response = await clnt.post('https://127.0.0.1:3000/convert/html', new FormData()) 54 | expect(await toJSON(response)).toEqual({ status: 'OK' }) 55 | }) 56 | 57 | test('Should throw on bad POST request', async () => { 58 | nock('https://127.0.0.1:3000') // 59 | .post('/convert/html') 60 | .reply(500, { status: 'ERROR' }) 61 | 62 | const clnt = client() 63 | let error: any 64 | try { 65 | await clnt.post('https://127.0.0.1:3000/convert/html', new FormData()) 66 | } catch (e) { 67 | error = e 68 | } 69 | expect(error).toEqual(new Error('500 null (undefined)')) // hm??? 70 | }) 71 | 72 | test('Should handle http', async () => { 73 | nock('http://127.0.0.1:3000') // 74 | .get('/ping') 75 | .reply(200, { status: 'OK' }) 76 | 77 | const clnt = client() 78 | const response = await clnt.get!('http://127.0.0.1:3000/ping') 79 | expect(await toJSON(response)).toEqual({ status: 'OK' }) 80 | }) 81 | 82 | test('Should merge http.request options with config', async () => { 83 | let basicAuthHeader: string | null = null 84 | 85 | nock('https://127.0.0.1:3000') // 86 | .post('/convert/html') 87 | .reply(200, function () { 88 | if (this.req.headers && this.req.headers.authorization) { 89 | basicAuthHeader = this.req.headers.authorization 90 | } 91 | return { status: 'OK' } 92 | }) 93 | 94 | const clnt = client({ auth: 'user:password' }) 95 | const response = await clnt.post('https://127.0.0.1:3000/convert/html', new FormData()) 96 | expect(await toJSON(response)).toEqual({ status: 'OK' }) 97 | expect(basicAuthHeader).toEqual('Basic dXNlcjpwYXNzd29yZA==') 98 | }) 99 | 100 | test('Should merge headers with config', async () => { 101 | let tokenAuthHeader: string | null = null 102 | 103 | nock('https://127.0.0.1:3000') // 104 | .post('/convert/html') 105 | .reply(200, function () { 106 | if (this.req.headers && this.req.headers.authorization) { 107 | tokenAuthHeader = this.req.headers.authorization 108 | } 109 | return { status: 'OK' } 110 | }) 111 | 112 | const clnt = client({ headers: { Authorization: 'Bearer token' } }) 113 | const response = await clnt.post('https://127.0.0.1:3000/convert/html', new FormData()) 114 | expect(await toJSON(response)).toEqual({ status: 'OK' }) 115 | expect(tokenAuthHeader).toEqual('Bearer token') 116 | }) 117 | -------------------------------------------------------------------------------- /test/to-helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | a3, 3 | A3, 4 | a4, 5 | A4, 6 | a5, 7 | A5, 8 | a6, 9 | A6, 10 | landscape, 11 | largeMargins, 12 | LARGE_MARGINS, 13 | legal, 14 | LEGAL, 15 | letter, 16 | LETTER, 17 | marginSizes, 18 | noMargins, 19 | normalMargins, 20 | NORMAL_MARGINS, 21 | NO_MARGINS, 22 | paperSize, 23 | portrait, 24 | tabloid, 25 | TABLOID, 26 | } from '../src' 27 | 28 | test('Test `landscape` function', () => { 29 | const object = {} 30 | landscape(object) 31 | expect(object).toEqual({ landscape: true }) 32 | }) 33 | 34 | test('Test `portrait` function', () => { 35 | const object = { landscape: true } 36 | portrait(object) 37 | expect(object).toEqual({ landscape: undefined }) 38 | }) 39 | 40 | test('Test `a3` function', () => { 41 | const object = {} 42 | a3(object) 43 | expect(object).toEqual({ paperWidth: A3[0], paperHeight: A3[1] }) 44 | }) 45 | 46 | test('Test `a4` function', () => { 47 | const object = {} 48 | a4(object) 49 | expect(object).toEqual({ paperWidth: A4[0], paperHeight: A4[1] }) 50 | }) 51 | 52 | test('Test `a5` function', () => { 53 | const object = {} 54 | a5(object) 55 | expect(object).toEqual({ paperWidth: A5[0], paperHeight: A5[1] }) 56 | }) 57 | 58 | test('Test `a6` function', () => { 59 | const object = {} 60 | a6(object) 61 | expect(object).toEqual({ paperWidth: A6[0], paperHeight: A6[1] }) 62 | }) 63 | 64 | test('Test `legal` function', () => { 65 | const object = {} 66 | legal(object) 67 | expect(object).toEqual({ paperWidth: LEGAL[0], paperHeight: LEGAL[1] }) 68 | }) 69 | 70 | test('Test `letter` function', () => { 71 | const object = {} 72 | letter(object) 73 | expect(object).toEqual({ paperWidth: LETTER[0], paperHeight: LETTER[1] }) 74 | }) 75 | 76 | test('Test `tabloid` function', () => { 77 | const object = {} 78 | tabloid(object) 79 | expect(object).toEqual({ paperWidth: TABLOID[0], paperHeight: TABLOID[1] }) 80 | }) 81 | 82 | test('Test `paperSize` function', () => { 83 | const object = {} 84 | paperSize([1, 2])(object) 85 | expect(object).toEqual({ paperWidth: 1, paperHeight: 2 }) 86 | paperSize({ width: 5, height: 6 })(object) 87 | expect(object).toEqual({ paperWidth: 5, paperHeight: 6 }) 88 | paperSize({ width: 10 })(object) 89 | expect(object).toEqual({ paperWidth: 10 }) 90 | paperSize({ height: 15 })(object) 91 | expect(object).toEqual({ paperHeight: 15 }) 92 | }) 93 | 94 | test('Test `noMargins` function', () => { 95 | const object = {} 96 | noMargins(object) 97 | expect(object).toEqual({ 98 | marginTop: NO_MARGINS[0], 99 | marginRight: NO_MARGINS[1], 100 | marginBottom: NO_MARGINS[2], 101 | marginLeft: NO_MARGINS[3], 102 | }) 103 | }) 104 | 105 | test('Test `normalMargins` function', () => { 106 | const object = {} 107 | normalMargins(object) 108 | expect(object).toEqual({ 109 | marginTop: NORMAL_MARGINS[0], 110 | marginRight: NORMAL_MARGINS[1], 111 | marginBottom: NORMAL_MARGINS[2], 112 | marginLeft: NORMAL_MARGINS[3], 113 | }) 114 | }) 115 | 116 | test('Test `largeMargins` function', () => { 117 | const object = {} 118 | largeMargins(object) 119 | expect(object).toEqual({ 120 | marginTop: LARGE_MARGINS[0], 121 | marginRight: LARGE_MARGINS[1], 122 | marginBottom: LARGE_MARGINS[2], 123 | marginLeft: LARGE_MARGINS[3], 124 | }) 125 | }) 126 | 127 | test('Test `marginSizes` function', () => { 128 | const object = {} 129 | marginSizes([1, 2, 3, 4])(object) 130 | expect(object).toEqual({ 131 | marginTop: 1, 132 | marginRight: 2, 133 | marginBottom: 3, 134 | marginLeft: 4, 135 | }) 136 | marginSizes({ top: 5, left: 6 })(object) 137 | expect(object).toEqual({ 138 | marginTop: 5, 139 | marginLeft: 6, 140 | }) 141 | marginSizes({ top: 10, right: 20, bottom: 15, left: 30 })(object) 142 | expect(object).toEqual({ 143 | marginTop: 10, 144 | marginRight: 20, 145 | marginBottom: 15, 146 | marginLeft: 30, 147 | }) 148 | marginSizes({ bottom: 15 })(object) 149 | expect(object).toEqual({ 150 | marginBottom: 15, 151 | }) 152 | }) 153 | -------------------------------------------------------------------------------- /src/internal/source-converters.ts: -------------------------------------------------------------------------------- 1 | import { basename, extname } from 'path' 2 | import { createReadStream, ReadStream } from 'fs' 3 | import { Readable } from 'stream' 4 | import { FileURI, PlainSource, Source, TupleSource, TupleStreamsSource } from '../_types' 5 | import { 6 | isBuffer, 7 | isFileName, 8 | isFileUri, 9 | isIterable, 10 | isObject, 11 | isStream, 12 | isString, 13 | isTuple, 14 | isURL, 15 | } from './source-checkers' 16 | 17 | export const DEFAULT_FILENAME = 'index.html' 18 | 19 | /** 20 | * Convert any possible source to tuples array 21 | */ 22 | export const toTuples = (source: Source, recursive = false): TupleSource[] => { 23 | // if single URL is given -> remove it (should be removed by `url`, but nonetheless) 24 | if (isURL(source)) return [] 25 | 26 | // if single file uri 27 | if (isFileUri(source)) { 28 | return !recursive && extname(source) === '.html' 29 | ? [[DEFAULT_FILENAME, source]] // single file uri and not inside recursion -> assume this is 'index.html' 30 | : [[basename(source), source]] // if inside recursion or file is not .html -> just get name from file uri 31 | } 32 | 33 | // single string or buffer 34 | if (isString(source) || isBuffer(source)) { 35 | // just assume it is 'index.html', as most useful and common case 36 | return [[DEFAULT_FILENAME, source]] 37 | } 38 | 39 | // if single stream 40 | if (isStream(source)) { 41 | if (source instanceof ReadStream) { 42 | // file stream 43 | // https://nodejs.org/api/fs.html#fs_readstream_path 44 | const name = basename(String(source.path)) 45 | return !recursive && extname(name) === '.html' 46 | ? [[DEFAULT_FILENAME, source]] // single file stream and not inside recursion -> assume this is 'index.html' 47 | : [[name, source]] // if inside recursion or file is not .html -> just get name from file uri 48 | } else { 49 | // some strange, not file stream -> just assume it is 'index.html' 50 | return [[DEFAULT_FILENAME, source]] 51 | } 52 | } 53 | 54 | // single tuple like we want to be -> just return it 55 | if (isTuple(source)) { 56 | if (isFileName(source[0])) { 57 | return [source] 58 | } 59 | throw new Error(`Source name "${source[0]}" doesn't look like file name`) 60 | } 61 | 62 | // if object source 63 | if (isObject(source)) { 64 | const ret: TupleSource[] = [] 65 | for (const key in source) { 66 | if (source.hasOwnProperty(key)) { 67 | if (isFileName(key)) { 68 | ret.push([key, source[key]]) 69 | } else { 70 | throw new Error(`Source name "${key}" doesn't look like file name`) 71 | } 72 | } 73 | } 74 | return ret 75 | } 76 | 77 | // if iterable source 78 | if (isIterable(source)) { 79 | const ret: TupleSource[] = [] 80 | for (const src of source) { 81 | // recursively convert to tuples 82 | ret.push(...toTuples(src, true)) 83 | } 84 | return ret 85 | } 86 | 87 | // if we get there, something is definitely wrong 88 | throw new Error(`Bad source, don't know what to do with "${source}"`) 89 | } 90 | 91 | /** 92 | * Read file to stream 93 | * `path` should starts with 'file:' or 'file://' 94 | * see https://en.wikipedia.org/wiki/File_URI_scheme 95 | */ 96 | export const fromFile = (path: FileURI): NodeJS.ReadableStream => 97 | createReadStream(path.replace(/^file:(\/\/)?/, '')) 98 | 99 | /** 100 | * Convert any plain source to stream 101 | */ 102 | export const toStream = (source: PlainSource): NodeJS.ReadableStream => 103 | isStream(source) 104 | ? source 105 | : isFileUri(source) 106 | ? fromFile(source) 107 | : // https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_new_stream_readable_options 108 | new Readable({ 109 | read() { 110 | this.push(source) 111 | this.push(null) 112 | }, 113 | }) 114 | 115 | /** 116 | * Convert any possible source to tuples array with streams only 117 | */ 118 | export const toStreams = (source?: Source): TupleStreamsSource[] => { 119 | if (!source) return [] 120 | const tuples = toTuples(source) 121 | const ret: TupleStreamsSource[] = [] 122 | for (let i = 0; i < tuples.length; i++) { 123 | ret.push([tuples[i][0], toStream(tuples[i][1])]) 124 | } 125 | return ret 126 | } 127 | -------------------------------------------------------------------------------- /test/please.spec.ts: -------------------------------------------------------------------------------- 1 | import FormData from 'form-data' 2 | import { createReadStream } from 'fs' 3 | import { HtmlRequest, MarkdownRequest, OfficeRequest, PingRequest, RequestType } from '../src' 4 | import { please } from '../src/please' 5 | 6 | test('Should make client POST call with request', async () => { 7 | const get = jest.fn() 8 | const post = jest.fn() 9 | const request: HtmlRequest = { 10 | type: RequestType.Html, 11 | client: { get, post }, 12 | url: 'http://120.0.0.1:3000/convert/html', 13 | source: 'test', 14 | fields: {}, 15 | } 16 | 17 | await please(request) 18 | 19 | expect(post.mock.calls.length).toBe(1) 20 | expect(post.mock.calls[0][0]).toEqual('http://120.0.0.1:3000/convert/html') 21 | expect(post.mock.calls[0][1]).toEqual(expect.any(FormData)) 22 | }) 23 | 24 | test('Should make client GET call with ping request', async () => { 25 | const get = jest.fn() 26 | const post = jest.fn() 27 | const request: PingRequest = { 28 | type: RequestType.Ping, 29 | client: { 30 | get: async (url) => { 31 | get(url) 32 | return createReadStream(__filename) 33 | }, 34 | post, 35 | }, 36 | url: 'http://120.0.0.1:3000/ping', 37 | fields: {}, 38 | } 39 | 40 | await please(request) 41 | 42 | expect(get.mock.calls.length).toBe(1) 43 | expect(get.mock.calls[0][0]).toEqual('http://120.0.0.1:3000/ping') 44 | }) 45 | 46 | test('Should throw on ping request if there is no get method', () => { 47 | const post = jest.fn() 48 | const request: PingRequest = { 49 | type: RequestType.Ping, 50 | client: { post }, 51 | url: 'http://120.0.0.1:3000/ping', 52 | fields: {}, 53 | } 54 | 55 | expect(() => please(request)).toThrow(`Gotenberg client doesn't implements "get" method`) 56 | }) 57 | 58 | test('Should make client POST call with request', async () => { 59 | const get = jest.fn() 60 | const post = jest.fn() 61 | 62 | const fields = Object.create({ test: 'test' }) // this is to test 'hasOwnProperty' 63 | fields.landscape = true 64 | fields.resultFilename = 'index.pdf' 65 | fields.waitDelay = undefined 66 | 67 | const request: HtmlRequest = { 68 | type: RequestType.Html, 69 | client: { get, post }, 70 | url: 'http://120.0.0.1:3000/convert/html', 71 | source: 'test', 72 | fields, 73 | headers: { 'Gotenberg-Remoteurl-Test': 'Foo' }, 74 | } 75 | 76 | await please(request) 77 | 78 | expect(post.mock.calls.length).toBe(1) 79 | expect(post.mock.calls[0][0]).toEqual('http://120.0.0.1:3000/convert/html') 80 | expect(post.mock.calls[0][1]).toEqual(expect.any(FormData)) 81 | expect(post.mock.calls[0][2]).toEqual({ 'Gotenberg-Remoteurl-Test': 'Foo' }) 82 | }) 83 | 84 | test('Should throw on duplicates', () => { 85 | const request: HtmlRequest = { 86 | type: RequestType.Html, 87 | source: [ 88 | ['index.html', 'test'], 89 | ['index.html', 'test'], 90 | ], 91 | } as any 92 | 93 | expect(() => please(request)).toThrow(`There are duplicates in file names: index.html`) 94 | }) 95 | 96 | test('Should throw on wrong source filenames', () => { 97 | const request1: HtmlRequest = { 98 | type: RequestType.Html, 99 | source: { 'test.doc': 'test' }, 100 | } as any 101 | 102 | expect(() => please(request1)).toThrow(`File "index.html" is required for HTML conversion`) 103 | 104 | const request2: MarkdownRequest = { 105 | type: RequestType.Markdown, 106 | source: { 'test.doc': 'test' }, 107 | } as any 108 | 109 | expect(() => please(request2)).toThrow(`File "index.html" is required for Markdown conversion`) 110 | 111 | const request3: OfficeRequest = { 112 | type: RequestType.Office, 113 | source: { 'index.html': 'test' }, 114 | } as any 115 | 116 | expect(() => please(request3)).toThrow( 117 | `Default filename "index.html" is not allowed for Office conversion, ` + 118 | `looks like you didn't set filename for document` 119 | ) 120 | }) 121 | 122 | test('Should throw on wrong source filename 2', () => { 123 | const request1: OfficeRequest = { 124 | type: RequestType.Office, 125 | source: ['aaaaa', 'test'], 126 | } as any 127 | 128 | expect(() => please(request1)).toThrow(`Source name "aaaaa" doesn't look like file name`) 129 | 130 | const request2: OfficeRequest = { 131 | type: RequestType.Office, 132 | source: { aaaaa: 'test' }, 133 | } as any 134 | 135 | expect(() => please(request2)).toThrow(`Source name "aaaaa" doesn't look like file name`) 136 | 137 | const request3: OfficeRequest = { 138 | type: RequestType.Office, 139 | source: [['aaaaa', 'test']], 140 | } as any 141 | 142 | expect(() => please(request3)).toThrow(`Source name "aaaaa" doesn't look like file name`) 143 | 144 | const request4: OfficeRequest = { 145 | type: RequestType.Office, 146 | source: [{ aaaaa: 'test' }], 147 | } as any 148 | 149 | expect(() => please(request4)).toThrow(`Source name "aaaaa" doesn't look like file name`) 150 | 151 | const request5: OfficeRequest = { 152 | type: RequestType.Office, 153 | source: [['aaaaa', 'test'], { aaaaa: 'test' }], 154 | } as any 155 | 156 | expect(() => please(request5)).toThrow(`Source name "aaaaa" doesn't look like file name`) 157 | }) 158 | -------------------------------------------------------------------------------- /src/_types.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' // tslint:disable-line no-circular-imports 2 | import FormData from 'form-data' 3 | 4 | //////////////////////////////////////////////////////////////////////////////// 5 | /// gotenberg client /////////////////////////////////////////////////////////// 6 | //////////////////////////////////////////////////////////////////////////////// 7 | 8 | /** 9 | * Gotenberg client interface 10 | */ 11 | export interface GotenbergClient { 12 | post: ( 13 | url: string, 14 | data: FormData, 15 | headers?: { 16 | [header: string]: number | string | string[] | undefined 17 | } 18 | ) => Promise 19 | get?: (url: string) => Promise 20 | } 21 | 22 | /** 23 | * Gotenberg client interface with configurator function 24 | * Will be called with config third gotenberg object 25 | */ 26 | export interface GotenbergClientFunction { 27 | (config?: object): GotenbergClient 28 | } 29 | 30 | /** 31 | * Gotenberg client class interface 32 | * Will be initialized with `new (config)` third gotenberg object 33 | */ 34 | export interface GotenbergClientClass { 35 | new (config?: object): GotenbergClient 36 | } 37 | 38 | //////////////////////////////////////////////////////////////////////////////// 39 | /// form fields //////////////////////////////////////////////////////////////// 40 | //////////////////////////////////////////////////////////////////////////////// 41 | 42 | // common form fields, for any conversion 43 | export type CommonRequestFields = { 44 | // It takes a float as value (e.g 2.5 for 2.5 seconds) 45 | // https://thecodingmachine.github.io/gotenberg/#timeout 46 | waitTimeout?: number 47 | 48 | // If provided, the API will send the resulting PDF file in a POST request with the `application/pdf` Content-Type to given URL 49 | // https://thecodingmachine.github.io/gotenberg/#webhook 50 | webhookURL?: string 51 | 52 | // It takes a float as value (e.g 2.5 for 2.5 seconds) 53 | // https://thecodingmachine.github.io/gotenberg/#webhook.timeout 54 | webhookURLTimeout?: number 55 | 56 | // If provided, the API will return the resulting PDF file with the given filename. Otherwise a random filename is used 57 | // Attention: this feature does not work if the form field webhookURL is given 58 | // https://thecodingmachine.github.io/gotenberg/#result_filename 59 | resultFilename?: string 60 | } 61 | 62 | // chrome form fields 63 | export type ChromeRequestFields = { 64 | // The wait delay is a duration in seconds (e.g 2.5 for 2.5 seconds) 65 | // https://thecodingmachine.github.io/gotenberg/#html.wait_delay 66 | waitDelay?: number 67 | 68 | // It takes an integer as value (e.g. 1048576 for 1 MB). The hard limit is 100 MB and is defined by Google Chrome itself 69 | // https://thecodingmachine.github.io/gotenberg/#html.rpcc_buffer_size 70 | googleChromeRpccBufferSize?: number 71 | } 72 | 73 | // html conversion form fields 74 | // https://thecodingmachine.github.io/gotenberg/#html 75 | export type HtmlRequestFields = { 76 | // By default, it will be rendered with A4 size, 1 inch margins and portrait orientation 77 | // Paper size and margins have to be provided in inches. Same for margins 78 | // https://thecodingmachine.github.io/gotenberg/#html.paper_size_margins_orientation 79 | paperWidth?: number 80 | paperHeight?: number 81 | marginTop?: number 82 | marginBottom?: number 83 | marginLeft?: number 84 | marginRight?: number 85 | landscape?: boolean 86 | 87 | // https://thecodingmachine.github.io/gotenberg/#html.page_ranges 88 | pageRanges?: string 89 | 90 | // https://thecodingmachine.github.io/gotenberg/#html.paper_size_margins_orientation_scaling 91 | scale?: number 92 | } 93 | 94 | // markdown conversion form fields 95 | // Markdown conversions work the same as HTML conversions 96 | // https://thecodingmachine.github.io/gotenberg/#markdown 97 | export type MarkdownRequestFields = HtmlRequestFields 98 | 99 | // office documents conversion form fields 100 | // https://thecodingmachine.github.io/gotenberg/#office 101 | export type OfficeRequestFields = { 102 | // By default, it will be rendered with portrait orientation 103 | // https://thecodingmachine.github.io/gotenberg/#office.orientation 104 | landscape?: boolean 105 | 106 | // https://thecodingmachine.github.io/gotenberg/#office.page_ranges 107 | pageRanges?: string 108 | } 109 | 110 | // url conversion form fields 111 | // Attention: when converting a website to PDF, you should remove all margins 112 | // If not, some of the content of the page might be hidden 113 | // https://thecodingmachine.github.io/gotenberg/#url 114 | export type UrlRequestFields = HtmlRequestFields & { 115 | remoteURL?: string 116 | } 117 | 118 | // merge conversion doesn't have any form fields 119 | // https://thecodingmachine.github.io/gotenberg/#merge 120 | export type MergeRequestFields = {} 121 | 122 | // all available form fields 123 | export type RequestFields = CommonRequestFields & 124 | UrlRequestFields & 125 | HtmlRequestFields & 126 | MergeRequestFields & 127 | OfficeRequestFields & 128 | ChromeRequestFields & 129 | MarkdownRequestFields 130 | 131 | //////////////////////////////////////////////////////////////////////////////// 132 | /// Html | Markdown | Office extended options (margins | page size) //////////// 133 | //////////////////////////////////////////////////////////////////////////////// 134 | 135 | export type FieldsModifier = (fields: RequestFields) => void 136 | export type PaperOptions = [number, number] | { width?: number; height?: number } 137 | export type MarginOptions = 138 | | [number, number, number, number] 139 | | { top?: number; right?: number; bottom?: number; left?: number } 140 | export type ConversionOptions = 141 | | PaperOptions 142 | | MarginOptions 143 | | FieldsModifier 144 | | (HtmlRequestFields & 145 | OfficeRequestFields & 146 | MarkdownRequestFields & { paper?: PaperOptions } & { 147 | margins?: MarginOptions 148 | }) 149 | 150 | //////////////////////////////////////////////////////////////////////////////// 151 | /// available sources ////////////////////////////////////////////////////////// 152 | //////////////////////////////////////////////////////////////////////////////// 153 | 154 | export type FileURI = string // TODO: https://github.com/microsoft/TypeScript/issues/6579 155 | export type PlainSource = string | Buffer | FileURI | NodeJS.ReadableStream 156 | export type TupleSource = [string, PlainSource] 157 | export type ObjectSource = { [name: string]: PlainSource } 158 | export type Source = 159 | | URL // for url conversions 160 | | PlainSource 161 | | TupleSource 162 | | ObjectSource 163 | | Array 164 | | Iterable 165 | export type TupleStreamsSource = [string, NodeJS.ReadableStream] 166 | 167 | //////////////////////////////////////////////////////////////////////////////// 168 | /// request headers //////////////////////////////////////////////////////////// 169 | //////////////////////////////////////////////////////////////////////////////// 170 | 171 | export type HeadersModifier = (headers: HttpHeaders) => void 172 | export type HttpHeaders = { 173 | [header: string]: number | string 174 | } 175 | 176 | //////////////////////////////////////////////////////////////////////////////// 177 | /// request types ////////////////////////////////////////////////////////////// 178 | //////////////////////////////////////////////////////////////////////////////// 179 | 180 | export enum RequestType { 181 | Url, 182 | Ping, 183 | Html, 184 | Merge, 185 | Office, 186 | Markdown, 187 | Undefined, 188 | } 189 | 190 | export type Request = { 191 | type: RequestType 192 | url: string 193 | client: GotenbergClient 194 | source?: Source 195 | fields: RequestFields 196 | headers?: HttpHeaders 197 | } 198 | 199 | export type UrlRequest = Request & { type: RequestType.Url } 200 | export type PingRequest = Request & { type: RequestType.Ping } 201 | export type HtmlRequest = Request & { type: RequestType.Html } 202 | export type MergeRequest = Request & { type: RequestType.Merge } 203 | export type OfficeRequest = Request & { type: RequestType.Office } 204 | export type MarkdownRequest = Request & { type: RequestType.Markdown } 205 | 206 | export type TypedRequest = 207 | | UrlRequest 208 | | PingRequest 209 | | HtmlRequest 210 | | MergeRequest 211 | | OfficeRequest 212 | | MarkdownRequest 213 | -------------------------------------------------------------------------------- /test/internal/source-checkers.spec.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { Readable } from 'stream' 3 | import { 4 | isBuffer, 5 | isFileName, 6 | isFileUri, 7 | isIterable, 8 | isObject, 9 | isPlain, 10 | isStream, 11 | isString, 12 | isTuple, 13 | isURL, 14 | } from '../../src/internal/source-checkers' 15 | 16 | // tslint:disable no-any 17 | 18 | // test dumb sources 19 | const string = 'test' // tslint:disable-line variable-name 20 | const url = new URL('http://1') 21 | const array = [] 22 | const map = new Map() 23 | const set = new Set() 24 | const object = {} 25 | const buffer = Buffer.from('test') 26 | const generator = function* gen() {} // tslint:disable-line no-empty 27 | const iterator = { [Symbol.iterator]: generator } 28 | 29 | test('Test `isBuffer` function', function () { 30 | expect(isBuffer(undefined)).toBe(false) 31 | expect(isBuffer(null)).toBe(false) 32 | expect(isBuffer(string)).toBe(false) 33 | expect(isBuffer(url)).toBe(false) 34 | expect(isBuffer(array)).toBe(false) 35 | expect(isBuffer(map)).toBe(false) 36 | expect(isBuffer(set)).toBe(false) 37 | expect(isBuffer(arguments)).toBe(false) 38 | expect(isBuffer(object)).toBe(false) 39 | expect(isBuffer(buffer)).toBe(true) // <- 40 | expect(isBuffer(generator())).toBe(false) 41 | expect(isBuffer(iterator)).toBe(false) 42 | }) 43 | 44 | test('Test `isFileName` function', function () { 45 | expect(isFileName(undefined)).toBe(false) 46 | expect(isFileName(null)).toBe(false) 47 | expect(isFileName(string)).toBe(false) 48 | expect(isFileName('index.html')).toBe(true) // <- 49 | expect(isFileName('test.md')).toBe(true) // <- 50 | expect(isFileName('image.gif')).toBe(true) // <- 51 | expect(isFileName('中文.gif')).toBe(true) // <- 52 | expect(isFileName('عربي.jpg')).toBe(true) // <- 53 | expect(isFileName('Screenshot 2021-12-24 at 09.16.20.png')).toBe(true) // <- 54 | expect(isFileName('.test.png')).toBe(true) // <- 55 | expect(isFileName('.png')).toBe(false) 56 | expect(isFileName('🙀.png')).toBe(true) // <- 57 | expect(isFileName('ces La esencia del cristianismo Dios es persona (jóvenes).docx')).toBe(true) // <- 58 | expect(isFileName(url)).toBe(false) 59 | expect(isFileName(array)).toBe(false) 60 | expect(isFileName(map)).toBe(false) 61 | expect(isFileName(set)).toBe(false) 62 | expect(isFileName(arguments)).toBe(false) 63 | expect(isFileName(object)).toBe(false) 64 | expect(isFileName(buffer)).toBe(false) 65 | expect(isFileName(generator())).toBe(false) 66 | expect(isFileName(iterator)).toBe(false) 67 | expect(isFileName('中文')).toBe(false) 68 | expect(isFileName('عربي')).toBe(false) 69 | }) 70 | 71 | test('Test `isFileUri` function', function () { 72 | expect(isFileUri(undefined)).toBe(false) 73 | expect(isFileUri(null)).toBe(false) 74 | expect(isFileUri(string)).toBe(false) 75 | expect(isFileUri('file://test')).toBe(true) // <- 76 | expect(isFileUri('file:test')).toBe(true) // <- 77 | expect(isFileUri(url)).toBe(false) 78 | expect(isFileUri(array)).toBe(false) 79 | expect(isFileUri(map)).toBe(false) 80 | expect(isFileUri(set)).toBe(false) 81 | expect(isFileUri(arguments)).toBe(false) 82 | expect(isFileUri(object)).toBe(false) 83 | expect(isFileUri(buffer)).toBe(false) 84 | expect(isFileUri(generator())).toBe(false) 85 | expect(isFileUri(iterator)).toBe(false) 86 | }) 87 | 88 | test('Test `isIterable` function', function () { 89 | expect(isIterable(undefined)).toBe(false) 90 | expect(isIterable(null)).toBe(false) 91 | expect(isIterable(string)).toBe(false) 92 | expect(isIterable(url)).toBe(false) 93 | expect(isIterable(array)).toBe(true) // <- 94 | expect(isIterable(map)).toBe(true) // <- 95 | expect(isIterable(set)).toBe(true) // <- 96 | ;(function () { 97 | // use new function to get empty arguments 98 | expect(isIterable(arguments)).toBe(true) // <- 99 | })() 100 | expect(isIterable(object)).toBe(false) 101 | expect(isIterable(buffer)).toBe(false) 102 | expect(isIterable(generator())).toBe(true) // <- 103 | expect(isIterable(iterator)).toBe(true) // <- 104 | 105 | // iterable should contain plain source 106 | expect(isIterable([undefined as any])).toBe(false) 107 | expect(isIterable([null as any])).toBe(false) 108 | expect(isIterable([string])).toBe(true) // <- 109 | expect(isIterable([url as any])).toBe(false) 110 | expect(isIterable([array as any])).toBe(false) 111 | expect(isIterable([map as any])).toBe(false) 112 | expect(isIterable([set as any])).toBe(false) 113 | expect(isIterable([arguments as any])).toBe(false) 114 | expect(isIterable([object])).toBe(true) // <- 115 | expect(isIterable([buffer])).toBe(true) // <- 116 | expect(isIterable([generator() as any])).toBe(false) 117 | expect(isIterable([iterator as any])).toBe(false) 118 | }) 119 | 120 | test('Test `isObject` function', function () { 121 | expect(isObject(undefined)).toBe(false) 122 | expect(isObject(null)).toBe(false) 123 | expect(isObject(string)).toBe(false) 124 | expect(isObject(url)).toBe(false) 125 | expect(isObject(array)).toBe(false) 126 | expect(isObject(map)).toBe(false) 127 | expect(isObject(set)).toBe(false) 128 | expect(isObject(arguments)).toBe(false) 129 | expect(isObject(object)).toBe(true) // <- 130 | expect(isObject(buffer)).toBe(false) 131 | expect(isObject(generator())).toBe(false) 132 | expect(isObject(iterator)).toBe(false) 133 | 134 | // object should contain plain source 135 | expect(isObject({ test: undefined as any })).toBe(false) 136 | expect(isObject({ test: null as any })).toBe(false) 137 | expect(isObject({ test: string })).toBe(true) // <- 138 | expect(isObject({ test: url as any })).toBe(false) 139 | expect(isObject({ test: array as any })).toBe(false) 140 | expect(isObject({ test: map as any })).toBe(false) 141 | expect(isObject({ test: set as any })).toBe(false) 142 | expect(isObject({ test: arguments as any })).toBe(false) 143 | expect(isObject({ test: object as any })).toBe(false) 144 | expect(isObject({ test: buffer })).toBe(true) // <- 145 | expect(isObject({ test: generator() as any })).toBe(false) 146 | expect(isObject({ test: iterator as any })).toBe(false) 147 | }) 148 | 149 | test('Test `isPlain` function', function () { 150 | expect(isPlain(undefined)).toBe(false) 151 | expect(isPlain(null)).toBe(false) 152 | expect(isPlain(string)).toBe(true) // <- 153 | expect(isPlain(url)).toBe(false) // do not consider URL as plain 154 | expect(isPlain(array)).toBe(false) 155 | expect(isPlain(map)).toBe(false) 156 | expect(isPlain(set)).toBe(false) 157 | expect(isPlain(arguments)).toBe(false) 158 | expect(isPlain(object)).toBe(false) 159 | expect(isPlain(buffer)).toBe(true) // <- 160 | expect(isPlain(generator())).toBe(false) 161 | expect(isPlain(iterator)).toBe(false) 162 | }) 163 | 164 | test('Test `isStream` function', function () { 165 | expect(isStream(undefined)).toBe(false) 166 | expect(isStream(null)).toBe(false) 167 | expect(isStream(string)).toBe(false) 168 | expect(isStream(url)).toBe(false) 169 | expect(isStream(array)).toBe(false) 170 | expect(isStream(map)).toBe(false) 171 | expect(isStream(set)).toBe(false) 172 | expect(isStream(arguments)).toBe(false) 173 | expect(isStream(object)).toBe(false) 174 | expect(isStream(buffer)).toBe(false) 175 | expect(isStream(generator())).toBe(false) 176 | expect(isStream(iterator)).toBe(false) 177 | expect(isStream(new Readable())).toBe(true) // <- 178 | }) 179 | 180 | test('Test `isString` function', function () { 181 | expect(isString(undefined)).toBe(false) 182 | expect(isString(null)).toBe(false) 183 | expect(isString(string)).toBe(true) // <- 184 | expect(isString(url)).toBe(false) 185 | expect(isString(array)).toBe(false) 186 | expect(isString(map)).toBe(false) 187 | expect(isString(set)).toBe(false) 188 | expect(isString(arguments)).toBe(false) 189 | expect(isString(object)).toBe(false) 190 | expect(isString(buffer)).toBe(false) 191 | expect(isString(generator())).toBe(false) 192 | expect(isString(iterator)).toBe(false) 193 | }) 194 | 195 | test('Test `isTuple` function', function () { 196 | expect(isTuple(undefined)).toBe(false) 197 | expect(isTuple(null)).toBe(false) 198 | expect(isTuple(string as any)).toBe(false) 199 | expect(isTuple(url)).toBe(false) 200 | expect(isTuple(array)).toBe(false) 201 | expect(isTuple(map)).toBe(false) 202 | expect(isTuple(set)).toBe(false) 203 | expect(isTuple(arguments)).toBe(false) 204 | expect(isTuple(object)).toBe(false) 205 | expect(isTuple(buffer)).toBe(false) 206 | expect(isTuple(generator())).toBe(false) 207 | expect(isTuple(iterator)).toBe(false) 208 | 209 | // first element of tuple should be string 210 | expect(isTuple([undefined, 'test'])).toBe(false) 211 | expect(isTuple([null, 'test'])).toBe(false) 212 | expect(isTuple([string, 'test'])).toBe(true) // <- 213 | expect(isTuple([url, 'test'])).toBe(false) 214 | expect(isTuple([array, 'test'])).toBe(false) 215 | expect(isTuple([map, 'test'])).toBe(false) 216 | expect(isTuple([set, 'test'])).toBe(false) 217 | expect(isTuple([arguments, 'test'])).toBe(false) 218 | expect(isTuple([object, 'test'])).toBe(false) 219 | expect(isTuple([buffer, 'test'])).toBe(false) 220 | expect(isTuple([generator(), 'test'])).toBe(false) 221 | expect(isTuple([iterator, 'test'])).toBe(false) 222 | 223 | // second element of tuple should be plain source 224 | expect(isTuple(['test', undefined])).toBe(false) 225 | expect(isTuple(['test', null])).toBe(false) 226 | expect(isTuple(['test', string])).toBe(true) // <- 227 | expect(isTuple(['test', url])).toBe(false) 228 | expect(isTuple(['test', array])).toBe(false) 229 | expect(isTuple(['test', map])).toBe(false) 230 | expect(isTuple(['test', set])).toBe(false) 231 | expect(isTuple(['test', arguments])).toBe(false) 232 | expect(isTuple(['test', object])).toBe(false) 233 | expect(isTuple(['test', buffer])).toBe(true) // <- 234 | expect(isTuple(['test', generator()])).toBe(false) 235 | expect(isTuple(['test', iterator])).toBe(false) 236 | }) 237 | 238 | test('Test `isURL` function', function () { 239 | expect(isURL(undefined)).toBe(false) 240 | expect(isURL(null)).toBe(false) 241 | expect(isURL(string)).toBe(false) 242 | expect(isURL(url)).toBe(true) // <- 243 | expect(isURL(array)).toBe(false) 244 | expect(isURL(map)).toBe(false) 245 | expect(isURL(set)).toBe(false) 246 | expect(isURL(arguments)).toBe(false) 247 | expect(isURL(object)).toBe(false) 248 | expect(isURL(buffer)).toBe(false) 249 | expect(isURL(generator())).toBe(false) 250 | expect(isURL(iterator)).toBe(false) 251 | }) 252 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gotenberg JS/TS client 2 | 3 | [![Build Status](https://github.com/yumauri/gotenberg-js-client/workflows/build/badge.svg)](https://github.com/yumauri/gotenberg-js-client/actions?workflow=build) 4 | [![Coverage Status](https://coveralls.io/repos/github/yumauri/gotenberg-js-client/badge.svg?branch=master)](https://coveralls.io/github/yumauri/gotenberg-js-client?branch=master) 5 | [![License](https://img.shields.io/github/license/yumauri/gotenberg-js-client.svg?color=yellow)](./LICENSE) 6 | [![NPM](https://img.shields.io/npm/v/gotenberg-js-client)](https://www.npmjs.com/package/gotenberg-js-client) 7 | ![Made with Love](https://img.shields.io/badge/made%20with-❤-red.svg) 8 | 9 | A simple JS/TS client for interacting with a [Gotenberg](https://gotenberg.dev/) API.
10 | [Gotenberg](https://gotenberg.dev/) is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF. 11 | 12 | - HTML and Markdown conversions using Google Chrome headless 13 | - Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv) 14 | - Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beautiful PDFs! 15 | - Easily interact with the API using [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries (and now - JavaScript too ;) 16 | 17 | ## Install 18 | 19 | ```bash 20 | $ yarn add gotenberg-js-client 21 | ``` 22 | 23 | Or using `npm` 24 | 25 | ```bash 26 | $ npm install --save gotenberg-js-client 27 | ``` 28 | 29 | ## NB ⚠️ 30 | 31 | This library is not yet fully compatible with Gotenberg 7.
32 | You can find more info in [this comment](https://github.com/yumauri/gotenberg-js-client/issues/32#issuecomment-981140727). 33 | 34 | There are three main issues: 35 | 36 | - Gotenberg 7 has introduced new concept of conversion modules, thus, changing conversion URLs, so now they are different, than ones, this library creates. This can be sidestepped using custom connection string or adjusting URL manually (see [this comment](https://github.com/yumauri/gotenberg-js-client/issues/32#issuecomment-981140727)). 37 | - New modules has some new possibilities/parameters, which are impossible to pass, using this library. This can be sidestepped using `adjust`, and casting to `any`, if you use TypeScript (see [this issue](https://github.com/yumauri/gotenberg-js-client/issues/33) for reference). 38 | - Gotenberg 7 can potentially has many many different custom conversion modules, you can even write your own one. You can combine p.1 and p.2 to use any module with any path with any parameters, but I guess it will look not good. But it should work nonetheless. 39 | 40 | So, nothing you can live without, but there are some inconveniences. For a while ;) 41 | 42 | ## Usage 43 | 44 | ```typescript 45 | import { pipe, gotenberg, convert, html, please } from 'gotenberg-js-client' 46 | 47 | const toPDF = pipe( 48 | gotenberg('http://localhost:3000'), 49 | convert, 50 | html, 51 | please 52 | ) 53 | 54 | // --- 8< --- 55 | 56 | // convert file from disk 57 | const pdf = await toPDF('file://index.html') 58 | 59 | // or convert stream 60 | const pdf = await toPDF(fs.createReadStream('index.html')) 61 | 62 | // or convert string! 63 | const pdf = await toPDF('...') 64 | 65 | // library returns NodeJS.ReadableStream, 66 | // so you can save it to file, if you want, for example 67 | pdf.pipe(fs.createWriteStream('index.pdf')) 68 | 69 | // or you can send it as response in Express application 70 | app.get('/pdf', function (req, res) { 71 | //... 72 | pdf.pipe(res) 73 | }) 74 | ``` 75 | 76 | You can define any source like `string`, `Buffer`, [file link](https://en.wikipedia.org/wiki/File_URI_scheme), `stream.Readable`, or `URL` (for url conversions).
77 | Detailed sources format you can find [here](https://github.com/yumauri/gotenberg-js-client/wiki/Source). 78 | 79 | ## Header, footer and assets 80 | 81 | You can define sources as array or object, for example: 82 | 83 | ```typescript 84 | // `toPDF` function is defined above ↑↑↑ 85 | 86 | // as object 87 | const pdf = await toPDF({ 88 | 'index.html': 'file://index.html', 89 | 'header.html': 'file://header.html', 90 | 'footer.html': 'file://footer.html', 91 | 'style.css': 'file://style.css', 92 | 'img.png': 'file://img.png', 93 | 'font.wof': 'file://font.wof', 94 | }) 95 | 96 | // as array of tuples 97 | const pdf = await toPDF([ 98 | ['index.html', 'file://index.html'], 99 | ['header.html', 'file://header.html'], 100 | ['footer.html', 'file://footer.html'], 101 | ]) 102 | 103 | // as even 1-dimensional array of files 104 | // in that case filenames will be retrieved from file path 105 | const pdf = await toPDF([ 106 | 'file://index.html', 107 | 'file://header.html', 108 | 'file://footer.html', 109 | ]) 110 | ``` 111 | 112 | Instead of array you can use any [iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols), like `Map`, `Set`, `arguments`, iterator from generator function, or any object with `[Symbol.iterator]` defined.
113 | Detailed sources format you can find [here](https://github.com/yumauri/gotenberg-js-client/wiki/Source). 114 | 115 | ## Paper size, margins, orientation 116 | 117 | When converting HTML or Markdown, you can use `to` helper, to set paper size, margins and orientation: 118 | 119 | ```typescript 120 | import { 121 | pipe, 122 | gotenberg, 123 | convert, 124 | html, 125 | please, 126 | to, 127 | a4, 128 | landscape, 129 | } from 'gotenberg-js-client' 130 | 131 | const toPDF = pipe( 132 | gotenberg('http://localhost:3000'), 133 | convert, 134 | html, 135 | to(a4, landscape), 136 | please 137 | ) 138 | ``` 139 | 140 | You can use simple object(s) for `to` argument(s) as well: 141 | 142 | ```typescript 143 | //... 144 | to({ 145 | paperWidth: 8.27, 146 | paperHeight: 11.69, 147 | marginTop: 0, 148 | marginBottom: 0, 149 | marginLeft: 0, 150 | marginRight: 0, 151 | landscape: true, 152 | }) 153 | //... 154 | 155 | // or 156 | to([8.27, 11.69], [0, 0, 0, 0], { landscape: true }) 157 | //... 158 | 159 | // or 160 | to({ paper: [8.27, 11.69], margins: [0, 0, 0, 0], landscape: true }) 161 | //... 162 | 163 | // or 164 | to({ width: 8.27, height: 11.69 }, { landscape: true }) 165 | //... 166 | 167 | // or 168 | to({ top: 0, bottom: 0 }) 169 | //... 170 | 171 | // or any other combination 172 | ``` 173 | 174 | When using array for paper size, order should be `[width, height]`
175 | When using array for margins, order should be `[top, right, bottom, left]` (just like in CSS) 176 | 177 | ## Common options 178 | 179 | You can set common options, like [resultFilename](https://thecodingmachine.github.io/gotenberg/#result_filename), or [waitTimeout](https://thecodingmachine.github.io/gotenberg/#timeout), or, actually, you can override _any_ option, using `set` helper: 180 | 181 | ```typescript 182 | //... 183 | set({ 184 | resultFilename: 'foo.pdf', 185 | waitTimeout: 2.5, 186 | }) 187 | //... 188 | ``` 189 | 190 | There are some _modifiers_ functions as well, like `filename`, `timeout`, `delay`, `webhook` and `googleChromeRpccBufferSize`: 191 | 192 | ```typescript 193 | //... 194 | set(filename('foo.pdf'), timeout(2.5)) 195 | //... 196 | ``` 197 | 198 | Also you can specify page ranges using `set(range)` (will not work with `merge`): 199 | 200 | ```typescript 201 | //... 202 | set(range('1-1')) 203 | //... 204 | ``` 205 | 206 | or scale, using `set(scale)` (works with HTML, Markdown and URL conversions): 207 | 208 | ```typescript 209 | //... 210 | set(scale(0.75)) 211 | //... 212 | ``` 213 | 214 | ## Markdown // [Gotenberg documentation](https://thecodingmachine.github.io/gotenberg/#markdown) 215 | 216 | ```typescript 217 | import { pipe, gotenberg, convert, markdown, please } from 'gotenberg-js-client' 218 | 219 | const toPDF = pipe( 220 | gotenberg('http://localhost:3000'), 221 | convert, 222 | markdown, 223 | please 224 | ) 225 | 226 | // --- 8< --- 227 | 228 | const pdf = await toPDF({ 229 | 'index.html': ` 230 | 231 | 232 | 233 | 234 | My PDF 235 | 236 | 237 | {{ toHTML .DirPath "content.md" }} 238 | 239 | `, 240 | 241 | 'content.md': ` 242 | # My awesome markdown 243 | ... 244 | `, 245 | }) 246 | ``` 247 | 248 | Note: I use strings here as an example, remind that you can use other supported [source](https://github.com/yumauri/gotenberg-js-client/wiki/Source) type. 249 | 250 | ## Office // [Gotenberg documentation](https://thecodingmachine.github.io/gotenberg/#office) 251 | 252 | ```typescript 253 | import { 254 | pipe, 255 | gotenberg, 256 | convert, 257 | office, 258 | to, 259 | landscape, 260 | set, 261 | filename, 262 | please, 263 | } from 'gotenberg-js-client' 264 | 265 | const toPDF = pipe( 266 | gotenberg('http://localhost:3000'), 267 | convert, 268 | office, 269 | to(landscape), 270 | set(filename('result.pdf')), 271 | please 272 | ) 273 | 274 | // --- 8< --- 275 | 276 | const pdf = await toPDF('file://document.docx') 277 | ``` 278 | 279 | Note: I use [file link](https://en.wikipedia.org/wiki/File_URI_scheme) here as an example, remind that you can use other supported [source](https://github.com/yumauri/gotenberg-js-client/wiki/Source) type, say, `Buffer`, or `stream.Readable`: 280 | 281 | ```typescript 282 | https.get( 283 | 'https://file-examples.com/wp-content/uploads/2017/02/file-sample_100kB.docx', 284 | async (document) => { 285 | const pdf = await toPDF({ 'document.docx': document }) 286 | // ... 287 | } 288 | ) 289 | ``` 290 | 291 | ## Url // [Gotenberg documentation](https://thecodingmachine.github.io/gotenberg/#url) 292 | 293 | ```typescript 294 | import { pipe, gotenberg, convert, url, please } from 'gotenberg-js-client' 295 | 296 | const toPDF = pipe( 297 | gotenberg('http://localhost:3000'), 298 | convert, 299 | url, 300 | please 301 | ) 302 | 303 | // --- 8< --- 304 | 305 | // you can use link as string 306 | const pdf = await toPDF('https://google.com') 307 | 308 | // or URL object 309 | const pdf = await toPDF(new URL('https://google.com')) 310 | ``` 311 | 312 | Note: The only supported source for Url conversion is text url or instance of `URL` class. 313 | 314 | You can set remote url header (for example, for [authentication](https://github.com/thecodingmachine/gotenberg/issues/81) or [host specifying](https://github.com/thecodingmachine/gotenberg/issues/116)) with helper `add(header)` (or `add(headers)`, or both): 315 | 316 | ```typescript 317 | const toPDF = pipe( 318 | gotenberg('http://localhost:3000'), 319 | convert, 320 | url, 321 | add( 322 | header('Foo-Header', 'Foo'), 323 | header('Bar-Header', 'Bar'), 324 | headers({ 'Baz1-Header': 'Baz1', 'Baz2-Header': 'Baz2' }) 325 | ), 326 | please 327 | ) 328 | ``` 329 | 330 | (This also applies for Webhook headers, just use `webhookHeader` instead of `header` and `webhookHeaders` instead of `headers`). 331 | 332 | ## Merge // [Gotenberg documentation](https://thecodingmachine.github.io/gotenberg/#merge) 333 | 334 | Like you would think: 335 | 336 | ```typescript 337 | import { pipe, gotenberg, merge, please } from 'gotenberg-js-client' 338 | 339 | const toMergedPDF = pipe( 340 | gotenberg('http://localhost:3000'), 341 | merge, 342 | please 343 | ) 344 | ``` 345 | 346 | ## Advanced fine adjustment 347 | 348 | There is special function `adjust`, which you can use to modify _any_ field in prepared internal `Request` object. You can check internal `Request` object structure in types. Any object, passed to `adjust`, will be merged with prepared `Request`. 349 | 350 | For example, you can modify `url`, if your Gotenberg instance is working behind reverse proxy with some weird url replacement rules: 351 | 352 | ```typescript 353 | import { pipe, gotenberg, convert, html, adjust, please } from 'gotenberg-js-client' 354 | 355 | // Original Gotenberg HTML conversion endpoint is 356 | // -> /convert/html 357 | // But your reverse proxy uses location 358 | // -> /hidden/html/conversion 359 | const toPDF = pipe( 360 | gotenberg('http://localhost:3000'), 361 | convert, 362 | html, 363 | adjust({ url: '/hidden/html/conversion' }), 364 | please 365 | ) 366 | ``` 367 | 368 | But, using that function, remember about Peter Parker principle: 369 | > "With great power comes great responsibility" 370 | 371 | ## Bonus 372 | 373 | If you happen to use this package from JavaScript, you will, obviously, lost type safety, but in return, you can use [proposed pipe operator](https://github.com/tc39/proposal-pipeline-operator) (with [Babel plugin](https://babeljs.io/docs/en/babel-plugin-proposal-pipeline-operator)), to get beauty like this: 374 | 375 | ```javascript 376 | const toPDF = source => 377 | source 378 | |> gotenberg('http://localhost:3000') 379 | |> convert 380 | |> html 381 | |> to(a4, noMargins) 382 | |> set(filename('out.pdf')) 383 | |> please 384 | ``` 385 | 386 | ## Names clashes 387 | 388 | If you don't like to have simple imported names in your namespace, you can use `import *` syntax: 389 | 390 | ```typescript 391 | import * as got from 'gotenberg-js-client' 392 | 393 | const toPDF = got.pipe( 394 | got.gotenberg('http://localhost:3000'), 395 | got.convert, 396 | got.html, 397 | got.please 398 | ) 399 | ``` 400 | 401 | ## Sponsored 402 | 403 | [Setplex OTT Platform](https://setplex.com/en/) 404 | 405 | [Setplex OTT Platform](https://setplex.com/en/) 406 | -------------------------------------------------------------------------------- /test/manual/statement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Invoice Statement 6 | 272 | 273 | 274 |
275 | 278 |
279 |

sales@setplex.com
280 | tel.:+1-855-738-7539

281 |
282 |
283 |
284 |
285 |
286 |

INVOICE

287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 300 |
INVOICE NUMBER1
INVOICE DATE17.07.2019
301 |
302 |
303 |
304 |

BILL TO

305 |

IIvanov

306 |

Ivan Ivanov

307 |

Ohio / Columbus

308 |

926 Steensland Trail / United States of America

309 |

ivan.ivanov99@gmail.com

310 |
311 |
312 | 313 | 314 | 315 | 316 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 358 | 359 | 360 | 361 | 362 | 367 | 370 | 373 | 374 | 375 | 376 | 377 | 378 |
PROCESS DATE 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 |
SERVICEAMOUNTPRICE, USD
Nora & Apps100 seats100
CDN Streaming100 seats250
VOD Storage150 GBs30
Catchup Channels 7D10 channels350
344 |
TOTAL AMOUNT, USDPAYMENT TYPETRANSACTION IDSTATUS
117.07.2019730 355 | Credit Card - XXXX7654 356 | 357 | 60124519509PAID
363 |

COMMENTS

364 |

1. Purpose of this invoice is REGISTRATION of services

365 |

366 |

SUBTOTAL

368 |

TAX RATE

369 |

TAX

730

371 |

0%

372 |

0

TOTAL, USD730
379 |
380 |
381 | If you have any questions about this invoice, please contact us
382 | tel.: +1-855-738-7539
383 | sales@setplex.com 384 |
385 |
386 | 405 | 406 | 407 | --------------------------------------------------------------------------------