From 928ef4ecb3f88c0d5ef3008a8e0f14e727ad54f8 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Thu, 1 Jul 2021 10:25:39 -0600 Subject: [PATCH] Better enforcement of typescript/linting rules + cleanup for compliance --- .eslintrc.json | 5 +- package-lock.json | 141 +++++++++++++++++++++++++++++ package.json | 11 ++- src/auth.ts | 9 +- src/images/generate.ts | 5 +- src/images/mathjax.ts | 3 + src/images/probe.ts | 18 ++-- src/images/svg.ts | 5 +- src/layout/generic_layout.ts | 72 ++++++++++----- src/layout/match_layout.ts | 20 ++-- src/layout/presentation_helpers.ts | 39 +++----- src/parser/css.ts | 11 ++- src/parser/env.ts | 18 ++-- src/parser/extract_slides.ts | 55 +++++++---- src/parser/parser.ts | 9 +- src/parser/syntax_highlight.ts | 44 ++++----- src/slide_generator.ts | 35 ++++--- src/slides.ts | 2 +- test/auth.spec.ts | 29 +++--- test/extract_slides.spec.ts | 2 +- test/generic_layout.spec.ts | 92 ++++++++++--------- test/match_layout.spec.ts | 134 ++++++++++++++++++--------- test/slide_generator.spec.ts | 2 +- tsconfig.json | 3 +- yarn.lock | 53 +++++++++++ 25 files changed, 564 insertions(+), 253 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index f95bb33..14c2775 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "./node_modules/gts/" + "extends": "./node_modules/gts/", + "rules": { + "@typescript-eslint/ban-ts-comment": 0 + } } diff --git a/package-lock.json b/package-lock.json index d151023..b12cbfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,17 +61,26 @@ "@babel/preset-env": "7.14.7", "@babel/preset-typescript": "7.14.5", "@babel/register": "7.14.5", + "@types/chai": "^4.2.19", + "@types/chai-as-promised": "^7.1.4", + "@types/chai-subset": "^1.3.3", "@types/debug": "^4.1.5", "@types/extend": "^3.0.1", + "@types/jsonfile": "^6.0.0", "@types/lowdb": "1.0.10", + "@types/lowlight": "^0.0.2", "@types/markdown-it": "12.0.2", "@types/mkdirp": "^1.0.1", + "@types/mocha": "^8.2.2", + "@types/mock-fs": "^4.13.0", "@types/node": "15.12.4", "@types/parse-color": "^1.0.0", + "@types/parse5": "^6.0.0", "@types/probe-image-size": "^7.0.0", "@types/promise-retry": "^1.1.3", "@types/request-promise-native": "^1.0.17", "@types/sharp": "0.28.3", + "@types/tmp": "^0.2.0", "@types/uuid": "^8.3.0", "chai": "4.3.4", "chai-as-promised": "7.1.1", @@ -1901,6 +1910,30 @@ "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", "dev": true }, + "node_modules/@types/chai": { + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.19.tgz", + "integrity": "sha512-jRJgpRBuY+7izT7/WNXP/LsMO9YonsstuL+xuvycDyESpoDoIAsMd7suwpB4h9oEWB+ZlPTqJJ8EHomzNhwTPQ==", + "dev": true + }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz", + "integrity": "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -1925,6 +1958,15 @@ "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, + "node_modules/@types/jsonfile": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.0.0.tgz", + "integrity": "sha512-mUHbRieyluPtL3c466K7oUGua1lAVlz45PV4U3bHs5CXdBlDIeXJI5xQXa6IZYnrgmcJzJp/CiTZB4zfShAi6w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/linkify-it": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.1.tgz", @@ -1946,6 +1988,12 @@ "@types/lodash": "*" } }, + "node_modules/@types/lowlight": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@types/lowlight/-/lowlight-0.0.2.tgz", + "integrity": "sha512-37DldsUs2l4rXI2YQgVn+NKVEaaUbBIzJg3eYzAXimGrtre8vxqE65wAGqYs9W6IsoOfgj74se/rBc9yoRXOHQ==", + "dev": true + }, "node_modules/@types/markdown-it": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.0.2.tgz", @@ -1978,6 +2026,21 @@ "@types/node": "*" } }, + "node_modules/@types/mocha": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.2.tgz", + "integrity": "sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==", + "dev": true + }, + "node_modules/@types/mock-fs": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.0.tgz", + "integrity": "sha512-FUqxhURwqFtFBCuUj3uQMp7rPSQs//b3O9XecAVxhqS9y4/W8SIJEZFq2mmpnFVZBXwR/2OyPLE97CpyYiB8Mw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "15.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz", @@ -1996,6 +2059,12 @@ "integrity": "sha512-kR998d57VzX3h6Wty4H6F6dnDbrcSGO9K7rN/RiBTKF39y463GGmglaNPY4D+IJknMvvKhtTbhVL3HeZjcJYcA==", "dev": true }, + "node_modules/@types/parse5": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.0.tgz", + "integrity": "sha512-oPwPSj4a1wu9rsXTEGIJz91ISU725t0BmSnUhb57sI+M8XEmvUop84lzuiYdq0Y5M6xLY8DBPg0C2xEQKLyvBA==", + "dev": true + }, "node_modules/@types/probe-image-size": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/probe-image-size/-/probe-image-size-7.0.0.tgz", @@ -2064,6 +2133,12 @@ "@types/node": "*" } }, + "node_modules/@types/tmp": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz", + "integrity": "sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ==", + "dev": true + }, "node_modules/@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -12557,6 +12632,30 @@ "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", "dev": true }, + "@types/chai": { + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.19.tgz", + "integrity": "sha512-jRJgpRBuY+7izT7/WNXP/LsMO9YonsstuL+xuvycDyESpoDoIAsMd7suwpB4h9oEWB+ZlPTqJJ8EHomzNhwTPQ==", + "dev": true + }, + "@types/chai-as-promised": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz", + "integrity": "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -12581,6 +12680,15 @@ "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, + "@types/jsonfile": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.0.0.tgz", + "integrity": "sha512-mUHbRieyluPtL3c466K7oUGua1lAVlz45PV4U3bHs5CXdBlDIeXJI5xQXa6IZYnrgmcJzJp/CiTZB4zfShAi6w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/linkify-it": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.1.tgz", @@ -12602,6 +12710,12 @@ "@types/lodash": "*" } }, + "@types/lowlight": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@types/lowlight/-/lowlight-0.0.2.tgz", + "integrity": "sha512-37DldsUs2l4rXI2YQgVn+NKVEaaUbBIzJg3eYzAXimGrtre8vxqE65wAGqYs9W6IsoOfgj74se/rBc9yoRXOHQ==", + "dev": true + }, "@types/markdown-it": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.0.2.tgz", @@ -12634,6 +12748,21 @@ "@types/node": "*" } }, + "@types/mocha": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.2.tgz", + "integrity": "sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==", + "dev": true + }, + "@types/mock-fs": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.0.tgz", + "integrity": "sha512-FUqxhURwqFtFBCuUj3uQMp7rPSQs//b3O9XecAVxhqS9y4/W8SIJEZFq2mmpnFVZBXwR/2OyPLE97CpyYiB8Mw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "15.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz", @@ -12652,6 +12781,12 @@ "integrity": "sha512-kR998d57VzX3h6Wty4H6F6dnDbrcSGO9K7rN/RiBTKF39y463GGmglaNPY4D+IJknMvvKhtTbhVL3HeZjcJYcA==", "dev": true }, + "@types/parse5": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.0.tgz", + "integrity": "sha512-oPwPSj4a1wu9rsXTEGIJz91ISU725t0BmSnUhb57sI+M8XEmvUop84lzuiYdq0Y5M6xLY8DBPg0C2xEQKLyvBA==", + "dev": true + }, "@types/probe-image-size": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/probe-image-size/-/probe-image-size-7.0.0.tgz", @@ -12719,6 +12854,12 @@ "@types/node": "*" } }, + "@types/tmp": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz", + "integrity": "sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ==", + "dev": true + }, "@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", diff --git a/package.json b/package.json index 10eaf52..3f0c81c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ ], "scripts": { "clean": "gts clean", - "compile": "babel --extensions '.ts,.js' --source-maps both -d lib/ src/", + "compile": "tsc && babel --extensions '.ts,.js' --source-maps both -d lib/ src/", "prepublish": "npm run compile", "exec": "npm run compile && node bin/md2gslides.js", "test": "mocha --require ./test/register --timeout 5000 \"test/**/*.spec.ts\"", @@ -87,17 +87,26 @@ "@babel/preset-env": "7.14.7", "@babel/preset-typescript": "7.14.5", "@babel/register": "7.14.5", + "@types/chai": "^4.2.19", + "@types/chai-as-promised": "^7.1.4", + "@types/chai-subset": "^1.3.3", "@types/debug": "^4.1.5", "@types/extend": "^3.0.1", + "@types/jsonfile": "^6.0.0", "@types/lowdb": "1.0.10", + "@types/lowlight": "^0.0.2", "@types/markdown-it": "12.0.2", "@types/mkdirp": "^1.0.1", + "@types/mocha": "^8.2.2", + "@types/mock-fs": "^4.13.0", "@types/node": "15.12.4", "@types/parse-color": "^1.0.0", + "@types/parse5": "^6.0.0", "@types/probe-image-size": "^7.0.0", "@types/promise-retry": "^1.1.3", "@types/request-promise-native": "^1.0.17", "@types/sharp": "0.28.3", + "@types/tmp": "^0.2.0", "@types/uuid": "^8.3.0", "chai": "4.3.4", "chai-as-promised": "7.1.1", diff --git a/src/auth.ts b/src/auth.ts index 26f2fc6..0e4fa0e 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -63,13 +63,10 @@ export default class UserAuthorizer { * * This may block briefly to ensure the token file exists. * - * @param {String} clientId Client ID - * @param {String} clientSecret Client secret - * @param {String} filePath Path to file where tokens are saved - * @param {UserAuthorizer~promptCallback} prompt Function to acquire the authorization code + * @param options */ public constructor(options: AuthOptions) { - this.db = this.initDbSync(options.filePath); + this.db = UserAuthorizer.initDbSync(options?.filePath); this.clientId = options.clientId; this.clientSecret = options.clientSecret; this.prompt = options.prompt; @@ -127,7 +124,7 @@ export default class UserAuthorizer { * @returns {lowdb} database instance * @private */ - private initDbSync(filePath: string): lowdb.LowdbSync { + private static initDbSync(filePath?: string): lowdb.LowdbSync { let adapter: lowdb.AdapterSync; if (filePath) { const parentDir = path.dirname(filePath); diff --git a/src/images/generate.ts b/src/images/generate.ts index 247537b..573646e 100644 --- a/src/images/generate.ts +++ b/src/images/generate.ts @@ -16,10 +16,11 @@ import Debug from 'debug'; import renderSVG from './svg'; import renderMathJax from './/mathjax'; import {ImageDefinition} from '../slides'; +import assert from 'assert'; const debug = Debug('md2gslides'); -const renderers = { +const renderers: {[key: string]: (img: ImageDefinition) => Promise} = { svg: renderSVG, math: renderMathJax, }; @@ -37,6 +38,8 @@ async function maybeGenerateImage( return image; } + assert(image.type); + const imageType = image.type.trim().toLowerCase(); const renderer = renderers[imageType]; diff --git a/src/images/mathjax.ts b/src/images/mathjax.ts index 4efff65..1b2f3d1 100644 --- a/src/images/mathjax.ts +++ b/src/images/mathjax.ts @@ -13,9 +13,11 @@ // limitations under the License. import Debug from 'debug'; +// @ts-ignore import mathJax from 'mathjax-node'; import renderSVG from './svg'; import {ImageDefinition} from '../slides'; +import assert from 'assert'; const debug = Debug('md2gslides'); let mathJaxInitialized = false; @@ -58,6 +60,7 @@ function addOrMergeStyles(svg: string, style?: string): string { async function renderMathJax(image: ImageDefinition): Promise { debug('Generating math image: %O', image); + assert(image.source); lazyInitMathJax(); const out = await mathJax.typeset({ math: image.source, diff --git a/src/images/probe.ts b/src/images/probe.ts index 8cd7d7b..dac03f0 100644 --- a/src/images/probe.ts +++ b/src/images/probe.ts @@ -18,6 +18,7 @@ import {ImageDefinition} from '../slides'; import retry from 'promise-retry'; import fs from 'fs'; import {URL} from 'url'; +import assert from 'assert'; const debug = Debug('md2gslides'); const retriableCodes = ['ENOTFOUND', 'ECONNRESET', 'ETIMEDOUT']; @@ -32,7 +33,7 @@ interface ImageSize { height: number; } -async function probeUrl(url): Promise { +async function probeUrl(url: string): Promise { return await retry(async doRetry => { try { return await probeImageSize(url); @@ -45,7 +46,7 @@ async function probeUrl(url): Promise { }, retryOptions); } -async function probeFile(path): Promise { +async function probeFile(path: string): Promise { const stream = fs.createReadStream(path); try { return await probeImageSize(stream); @@ -56,17 +57,18 @@ async function probeFile(path): Promise { async function probeImage(image: ImageDefinition): Promise { debug('Probing image size: %s', image.url); - let promise; + assert(image.url); const parsedUrl = new URL(image.url); if (parsedUrl.protocol === 'file:') { - promise = probeFile(parsedUrl.pathname); + const size = await probeFile(parsedUrl.pathname); + image.width = size.width; + image.height = size.height; } else { - promise = probeUrl(image.url); + const size = await probeUrl(image.url); + image.width = size.width; + image.height = size.height; } - const size = await promise; - image.width = size.width; - image.height = size.height; return image; } diff --git a/src/images/svg.ts b/src/images/svg.ts index 3ae7525..3927d95 100644 --- a/src/images/svg.ts +++ b/src/images/svg.ts @@ -14,12 +14,15 @@ import Debug from 'debug'; import sharp from 'sharp'; import tmp from 'tmp-promise'; +import {ImageDefinition} from '../slides'; +import assert from 'assert'; const debug = Debug('md2gslides'); tmp.setGracefulCleanup(); -async function renderSVG(image): Promise { +async function renderSVG(image: ImageDefinition): Promise { debug('Generating SVG', image); + assert(image.source); const path = await tmp.tmpName({postfix: '.png'}); const buffer = Buffer.from(image.source); await sharp(buffer, {density: 2400}).png().toFile(path); diff --git a/src/layout/generic_layout.ts b/src/layout/generic_layout.ts index bbe7a62..03b700a 100644 --- a/src/layout/generic_layout.ts +++ b/src/layout/generic_layout.ts @@ -15,15 +15,23 @@ import Debug from 'debug'; import {uuid} from '../utils'; import extend from 'extend'; +// @ts-ignore import Layout from 'layout'; import * as _ from 'lodash'; import {slides_v1 as SlidesV1} from 'googleapis'; -import {SlideDefinition, TextDefinition, VideoDefinition} from '../slides'; +import { + ImageDefinition, + SlideDefinition, + TableDefinition, + TextDefinition, + VideoDefinition, +} from '../slides'; import { findLayoutIdByName, findPlaceholder, findSpeakerNotesObjectId, } from './presentation_helpers'; +import assert from 'assert'; const debug = Debug('md2gslides'); @@ -101,13 +109,19 @@ export default class GenericLayout { } if (this.slide.bodies) { + assert(this.slide.objectId); const bodyElements = findPlaceholder( this.presentation, this.slide.objectId, 'BODY' ); - this.slide.bodies.forEach((body, index) => { - const placeholder = bodyElements[index]; + const bodyCount = Math.min( + bodyElements?.length ?? 0, + this.slide.bodies.length + ); + for (let i = 0; i < bodyCount; ++i) { + const placeholder = bodyElements![i]; + const body = this.slide.bodies[i]; this.appendFillPlaceholderTextRequest(body.text, placeholder, requests); if (body.images && body.images.length) { this.appendCreateImageRequests(body.images, placeholder, requests); @@ -115,10 +129,11 @@ export default class GenericLayout { if (body.videos && body.videos.length) { this.appendCreateVideoRequests(body.videos, placeholder, requests); } - }); + } } if (this.slide.notes) { + assert(this.slide.objectId); const objectId = findSpeakerNotesObjectId( this.presentation, this.slide.objectId @@ -134,16 +149,17 @@ export default class GenericLayout { } protected appendFillPlaceholderTextRequest( - value: TextDefinition, + value: TextDefinition | undefined, placeholder: string | SlidesV1.Schema$PageElement, requests: SlidesV1.Schema$Request[] - ): SlidesV1.Schema$Request[] { + ): void { if (!value) { debug('No text for placeholder %s'); return; } if (typeof placeholder === 'string') { + assert(this.slide.objectId); const pageElements = findPlaceholder( this.presentation, this.slide.objectId, @@ -165,7 +181,9 @@ export default class GenericLayout { protected appendInsertTextRequests( text: TextDefinition, - locationProps, + locationProps: + | Partial + | Partial, requests: SlidesV1.Schema$Request[] ): void { // Insert the raw text first @@ -208,6 +226,7 @@ export default class GenericLayout { locationProps ), }; + assert(request.updateTextStyle?.style); request.updateTextStyle.fields = this.computeShallowFieldMask( request.updateTextStyle.style ); @@ -243,7 +262,7 @@ export default class GenericLayout { } protected appendSetBackgroundImageRequest( - image, + image: ImageDefinition, requests: SlidesV1.Schema$Request[] ): void { debug( @@ -268,8 +287,8 @@ export default class GenericLayout { } protected appendCreateImageRequests( - images, - placeholder, + images: ImageDefinition[], + placeholder: SlidesV1.Schema$PageElement | undefined, requests: SlidesV1.Schema$Request[] ): void { // TODO - Fix weird cast @@ -340,7 +359,7 @@ export default class GenericLayout { protected appendCreateVideoRequests( videos: VideoDefinition[], - placeholder, + placeholder: SlidesV1.Schema$PageElement | undefined, requests: SlidesV1.Schema$Request[] ): void { if (videos.length > 1) { @@ -405,7 +424,7 @@ export default class GenericLayout { } protected appendCreateTableRequests( - tables, + tables: TableDefinition[], requests: SlidesV1.Schema$Request[] ): void { if (tables.length > 1) { @@ -434,8 +453,8 @@ export default class GenericLayout { { objectId: tableId, cellLocation: { - rowIndex: r, - columnIndex: c, + rowIndex: parseInt(r), + columnIndex: parseInt(c), }, }, requests @@ -447,25 +466,32 @@ export default class GenericLayout { protected calculateBoundingBox( element: SlidesV1.Schema$PageElement ): BoundingBox { + assert(element); + assert(element.size?.height?.magnitude); + assert(element.size?.width?.magnitude); const height = element.size.height.magnitude; const width = element.size.width.magnitude; - const scaleX = element.transform.scaleX || 1; - const scaleY = element.transform.scaleY || 1; - const shearX = element.transform.shearX || 0; - const shearY = element.transform.shearY || 0; + const scaleX = element.transform?.scaleX ?? 1; + const scaleY = element.transform?.scaleY ?? 1; + const shearX = element.transform?.shearX ?? 0; + const shearY = element.transform?.shearY ?? 0; return { width: scaleX * width + shearX * height, height: scaleY * height + shearY * width, - x: element.transform.translateX, - y: element.transform.translateY, + x: element.transform?.translateX ?? 0, + y: element.transform?.translateY ?? 0, }; } - protected getBodyBoundingBox(placeholder): BoundingBox { + protected getBodyBoundingBox( + placeholder: SlidesV1.Schema$PageElement | undefined + ): BoundingBox { if (placeholder) { return this.calculateBoundingBox(placeholder); } + assert(this.presentation.pageSize?.width?.magnitude); + assert(this.presentation.pageSize?.height?.magnitude); return { width: this.presentation.pageSize.width.magnitude, height: this.presentation.pageSize.height.magnitude, @@ -474,10 +500,10 @@ export default class GenericLayout { }; } - protected computeShallowFieldMask(object: object): string { + protected computeShallowFieldMask(object: T): string { const fields = []; for (const field of Object.keys(object)) { - if (object[field] !== undefined) { + if (object[field as keyof T] !== undefined) { fields.push(field); } } diff --git a/src/layout/match_layout.ts b/src/layout/match_layout.ts index 619a7dc..59aadb1 100644 --- a/src/layout/match_layout.ts +++ b/src/layout/match_layout.ts @@ -30,12 +30,12 @@ export default function matchLayout( slide: SlideDefinition ): GenericLayout { // if we have manually set the slide layout get the master from the presentation - let layoutName: string = undefined; + let layoutName: string | undefined = undefined; if (slide.customLayout !== undefined) { - const layout = presentation.layouts.find( - layout => layout.layoutProperties.displayName === slide.customLayout + const layout = presentation.layouts?.find( + layout => layout.layoutProperties?.displayName === slide.customLayout ); - if (layout) { + if (layout?.layoutProperties?.name) { layoutName = layout.layoutProperties.name; } } @@ -56,12 +56,12 @@ function defineLayout(name: string, matchFn: MatchFn): void { }); } -function hasText(text: TextDefinition): boolean { - return text && text.rawText && text.rawText.length !== 0; +function hasText(text?: TextDefinition): boolean { + return text?.rawText !== undefined && text.rawText.length > 0; } -function hasBigTitle(slide: SlideDefinition): boolean { - return hasText(slide.title) && slide.title.big; +function hasBigTitle(slide?: SlideDefinition): boolean { + return hasText(slide?.title) && slide?.title?.big === true; } function hasTextContent(slide: SlideDefinition): boolean { @@ -69,8 +69,8 @@ function hasTextContent(slide: SlideDefinition): boolean { } // Anything which takes up the main body space -function hasContent(slide: SlideDefinition): boolean { - return slide.bodies.length !== 0 || slide.tables.length !== 0; +function hasContent(slide?: SlideDefinition): boolean { + return slide?.bodies.length !== 0 || slide?.tables.length !== 0; } // Define rules for picking slide layouts based on the default diff --git a/src/layout/presentation_helpers.ts b/src/layout/presentation_helpers.ts index bc1a970..7c9492e 100644 --- a/src/layout/presentation_helpers.ts +++ b/src/layout/presentation_helpers.ts @@ -13,6 +13,7 @@ // limitations under the License. import {slides_v1 as SlidesV1} from 'googleapis'; +import assert from 'assert'; export interface Dimensions { width: number; @@ -22,6 +23,7 @@ export interface Dimensions { /** * Locates a page by ID * + * @param presentation * @param {string} pageId Object ID of page to find * @returns {Object} Page or null if not found */ @@ -38,6 +40,8 @@ export function findPage( export function pageSize( presentation: SlidesV1.Schema$Presentation ): Dimensions { + assert(presentation.pageSize?.width?.magnitude); + assert(presentation.pageSize?.height?.magnitude); return { width: presentation.pageSize.width.magnitude, height: presentation.pageSize.height.magnitude, @@ -47,6 +51,7 @@ export function pageSize( /** * Locates a layout. * + * @param presentation * @param {string} name * @returns {string} layout ID or null if not found */ @@ -58,17 +63,18 @@ export function findLayoutIdByName( return undefined; } const layout = presentation.layouts.find( - (l): boolean => l.layoutProperties.name === name + (l): boolean => l.layoutProperties?.name === name ); if (!layout) { return undefined; } - return layout.objectId; + return layout.objectId ?? undefined; } /** * Find a named placeholder on the page. * + * @param presentation * @param {string} pageId Object ID of page to find element on * @param name Placeholder name. * @returns {Array} Array of placeholders @@ -105,37 +111,16 @@ export function findPlaceholder( return undefined; } -/** - * Locates a element on a page by ID. - * - * @param {string} pageId Object ID of page to find element on - * @returns {Object} Object or null if not found - */ -export function findPageElement( - presentation: SlidesV1.Schema$Presentation, - pageId: string, - id: string -): SlidesV1.Schema$PageElement | undefined { - const page = findPage(presentation, pageId); - if (!page) { - throw new Error(`Can't find page ${pageId}`); - } - - for (const element of page.pageElements) { - if (element.objectId === id) { - return element; - } - } - return undefined; -} - export function findSpeakerNotesObjectId( presentation: SlidesV1.Schema$Presentation, pageId: string ): string | undefined { const page = findPage(presentation, pageId); if (page) { - return page.slideProperties.notesPage.notesProperties.speakerNotesObjectId; + return ( + page.slideProperties?.notesPage?.notesProperties?.speakerNotesObjectId ?? + undefined + ); } return undefined; } diff --git a/src/parser/css.ts b/src/parser/css.ts index 071ae38..621f743 100644 --- a/src/parser/css.ts +++ b/src/parser/css.ts @@ -14,7 +14,9 @@ import parseColor from 'parse-color'; import Debug from 'debug'; +// @ts-ignore import inlineStylesParse from 'inline-styles-parse'; +// @ts-ignore import nativeCSS from 'native-css'; import {Color, StyleDefinition} from '../slides'; import * as _ from 'lodash'; @@ -28,7 +30,7 @@ export interface Stylesheet { [key: string]: CssRule; } -function parseColorString(hexString: string): Color { +function parseColorString(hexString: string): Color | undefined { const c = parseColor(hexString); if (!c.rgba) { return; @@ -45,11 +47,10 @@ function parseColorString(hexString: string): Color { } function normalizeKeys(css: CssRule): CssRule { - const normalized = _.mapKeys(css, (value, key) => _.camelCase(key)); - return normalized; + return _.mapKeys(css, (value, key) => _.camelCase(key)); } -export function parseStyleSheet(stylesheet: string): Stylesheet { +export function parseStyleSheet(stylesheet: string | undefined): Stylesheet { return nativeCSS.convert(stylesheet) as Stylesheet; } @@ -99,7 +100,7 @@ export function updateStyleDefinition( const match = (value as string).match(/(\d+)(?:pt)?/); if (!match) { debug('Invalid font-size value: %s', value); - return; + break; } style.fontSize = { magnitude: Number.parseInt(match[1]), diff --git a/src/parser/env.ts b/src/parser/env.ts index 8eb4aee..49bbb38 100644 --- a/src/parser/env.ts +++ b/src/parser/env.ts @@ -25,6 +25,8 @@ import {uuid} from '../utils'; import extend from 'extend'; import * as _ from 'lodash'; import {Stylesheet} from './css'; +import assert from 'assert'; +import {Element} from 'parse5'; export class Context { public slides: SlideDefinition[] = []; @@ -37,11 +39,11 @@ export class Context { public row: TextDefinition[] = []; public table?: TableDefinition; public list?: ListDefinition; - public inlineHtmlContext?: object; + public inlineHtmlContext?: Element; public images: ImageDefinition[] = []; public videos: VideoDefinition[] = []; - public constructor(css: Stylesheet) { + public constructor(css?: Stylesheet) { this.css = css; this.startSlide(); } @@ -60,6 +62,7 @@ export class Context { } public appendText(content: string): void { + assert(this.text); this.text.rawText += content; } @@ -87,13 +90,8 @@ export class Context { public startSlide(): void { this.currentSlide = { objectId: uuid(), - customLayout: null, - title: null, - subtitle: null, - backgroundImage: null, bodies: [], tables: [], - notes: null, }; } @@ -104,12 +102,16 @@ export class Context { public startStyle(newStyle: StyleDefinition): void { const previousStyle = this.currentStyle(); const style = extend({}, newStyle, previousStyle); - style.start = this.text.rawText.length; + style.start = this.text?.rawText.length ?? 0; this.styles.push(style); } public endStyle(): void { const style = this.styles.pop(); + assert(style); + if (!this.text) { + return; // Ignore empty text style + } style.end = this.text.rawText.length; if (style.start === style.end) { return; // Ignore empty ranges diff --git a/src/parser/extract_slides.ts b/src/parser/extract_slides.ts index ef00b5e..1e0b223 100644 --- a/src/parser/extract_slides.ts +++ b/src/parser/extract_slides.ts @@ -15,13 +15,14 @@ import Debug from 'debug'; import extend from 'extend'; import Token from 'markdown-it/lib/token'; -import parse5 from 'parse5'; +import parse5, {Element} from 'parse5'; import fileUrl from 'file-url'; import {SlideDefinition, StyleDefinition} from '../slides'; import parseMarkdown from './parser'; import {Context} from './env'; import highlightSyntax from './syntax_highlight'; import {parseStyleSheet, parseInlineStyle, updateStyleDefinition} from './css'; +import assert from 'assert'; const debug = Debug('md2gslides'); @@ -40,7 +41,7 @@ const fullTokenRules: MarkdownRules = {}; let ruleSet: MarkdownRules; -function attr(token: Token, name: string): string { +function attr(token: Token, name: string): string | undefined { if (!token.attrs) { return undefined; } @@ -51,7 +52,7 @@ function attr(token: Token, name: string): string { return attr[1]; } -function hasClass(token, cls): boolean { +function hasClass(token: Token, cls: string): boolean { return cls === attr(token, 'class'); } @@ -108,18 +109,20 @@ inlineTokenRules['heading_open'] = (token, context) => { inlineTokenRules['heading_close'] = (token, context) => context.endStyle(); inlineTokenRules['inline'] = (token, context) => { + if (!token.children) { + return; + } for (const child of token.children) { processMarkdownToken(child, context); } }; inlineTokenRules['html_inline'] = (token, context) => { - const fragment = parse5.parseFragment( - context.inlineHtmlContext, - token.content - ); + const fragment = context.inlineHtmlContext + ? parse5.parseFragment(context.inlineHtmlContext, token.content) + : parse5.parseFragment(token.content); if (fragment.childNodes && fragment.childNodes.length) { - const node = fragment.childNodes[0]; + const node = fragment.childNodes[0] as Element; const style: StyleDefinition = {}; switch (node.nodeName) { @@ -151,12 +154,14 @@ inlineTokenRules['html_inline'] = (token, context) => { throw new Error('Unsupported inline HTML element: ' + node.nodeName); } - const styleAttr = node.attrs.find(attr => attr.name === 'style'); + const styleAttr = node.attrs.find( + (attr: {name: string}) => attr.name === 'style' + ); if (styleAttr) { const css = parseInlineStyle(styleAttr.value); updateStyleDefinition(css, style); } - context.inlineHtmlContext = fragment.childNodes[0]; + context.inlineHtmlContext = node; context.startStyle(style); } else { context.endStyle(); @@ -171,6 +176,7 @@ inlineTokenRules['text'] = (token, context) => { }; inlineTokenRules['paragraph_open'] = (token, context) => { + assert(context.currentSlide); if (hasClass(token, 'column')) { context.markerParagraph = true; const body = { @@ -243,7 +249,7 @@ inlineTokenRules['strong_close'] = (token, context) => context.endStyle(); inlineTokenRules['link_open'] = (token, context) => { const style = applyTokenStyle(token, { link: { - url: attr(token, 'href'), + url: attr(token, 'href') ?? '#', }, }); context.startStyle(style); @@ -278,6 +284,7 @@ inlineTokenRules['bullet_list_open'] = inlineTokenRules['ordered_list_open'] = ( token, context ) => { + assert(context.text); const style = applyTokenStyle(token, {}); context.startStyle(style); if (context.list) { @@ -296,6 +303,8 @@ inlineTokenRules['bullet_list_open'] = inlineTokenRules['ordered_list_open'] = ( inlineTokenRules['bullet_list_close'] = inlineTokenRules['ordered_list_close'] = (token, context) => { + assert(context.list); + assert(context.text); if (context.list.depth === 0) { // TODO - Support nested lists with mixed styles when API supports it. // Currently nested lists must match the parent style. @@ -304,7 +313,7 @@ inlineTokenRules['bullet_list_close'] = inlineTokenRules['ordered_list_close'] = end: context.text.rawText.length, type: token.tag === 'ul' ? 'unordered' : 'ordered', }); - context.list = null; + context.list = undefined; } else { context.list.depth -= 1; } @@ -312,6 +321,7 @@ inlineTokenRules['bullet_list_close'] = inlineTokenRules['ordered_list_close'] = }; inlineTokenRules['list_item_open'] = (token, context) => { + assert(context.list); const style = applyTokenStyle(token, {}); context.startStyle(style); context.appendText(new Array(context.list.depth + 1).join('\t')); @@ -329,10 +339,12 @@ fullTokenRules['heading_open'] = (token, context) => { const style = applyTokenStyle(token, {}); context.startTextBlock(); context.startStyle(style); + assert(context.text); context.text.big = hasClass(token, 'big'); }; fullTokenRules['heading_close'] = (token, context) => { + assert(context.currentSlide); if (token.tag === 'h1') { context.currentSlide.title = context.text; } else if (token.tag === 'h2') { @@ -345,6 +357,7 @@ fullTokenRules['heading_close'] = (token, context) => { }; fullTokenRules['html_block'] = (token, context) => { + assert(context.currentSlide); const re = //m; const match = re.exec(token.content); if (match === null) { @@ -374,14 +387,15 @@ fullTokenRules['hr'] = (token, context) => { }; fullTokenRules['image'] = (token, context) => { + assert(context.currentSlide); let url = attr(token, 'src'); - if (!url.match(/(file|https?):/)) { + if (url && !url.match(/(file|https?):/)) { url = fileUrl(url); } const image = { url: url, - width: undefined, - height: undefined, + width: 0, + height: 0, padding: 0, offsetX: 0, offsetY: 0, @@ -437,6 +451,8 @@ fullTokenRules['table_open'] = (token, context) => { }; fullTokenRules['table_close'] = (token, context) => { + assert(context.currentSlide); + assert(context.table); context.currentSlide.tables.push(context.table); context.endStyle(); }; @@ -454,6 +470,7 @@ fullTokenRules['tr_open'] = (token, context) => { }; fullTokenRules['tr_close'] = (token, context) => { + assert(context.table); const row = context.row; context.table.cells.push(row); context.table.columns = Math.max(context.table.columns, row.length); @@ -490,17 +507,19 @@ fullTokenRules['th_open'] = (token, context) => { }; fullTokenRules['td_close'] = fullTokenRules['th_close'] = (token, context) => { + assert(context.text); context.endStyle(); context.row.push(context.text); context.startTextBlock(); }; fullTokenRules['generated_image'] = (token, context) => { + assert(context.currentSlide); const image = { source: token.content, type: token.info.trim(), - width: undefined, - height: undefined, + width: 0, + height: 0, style: attr(token, 'style'), padding: 0, offsetX: 0, @@ -530,7 +549,7 @@ fullTokenRules['generated_image'] = (token, context) => { */ export default function extractSlides( markdown: string, - stylesheet: string = null + stylesheet?: string ): SlideDefinition[] { const tokens = parseMarkdown(markdown); const css = parseStyleSheet(stylesheet); diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 356abbc..f8f21fa 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -14,13 +14,20 @@ import markdownIt from 'markdown-it'; import Token from 'markdown-it/lib/token'; +// @ts-ignore import attrs from 'markdown-it-attrs'; +// @ts-ignore import lazyHeaders from 'markdown-it-lazy-headers'; +// @ts-ignore import emoji from 'markdown-it-emoji'; +// @ts-ignore import expandTabs from 'markdown-it-expand-tabs'; +// @ts-ignore import video from 'markdown-it-video'; +// @ts-ignore import customFence from 'markdown-it-fence'; -function generatedImage(md): void { + +function generatedImage(md: unknown): void { return customFence(md, 'generated_image', { marker: '$', validate: () => true, diff --git a/src/parser/syntax_highlight.ts b/src/parser/syntax_highlight.ts index 5843916..288266e 100644 --- a/src/parser/syntax_highlight.ts +++ b/src/parser/syntax_highlight.ts @@ -14,7 +14,7 @@ import low from 'lowlight'; import {Context} from './env'; -import {updateStyleDefinition} from './css'; +import {CssRule, updateStyleDefinition} from './css'; import {StyleDefinition} from '../slides'; type RuleFn = (node: lowlight.HastNode, context: Context) => void; @@ -26,37 +26,43 @@ const hastRules: Rules = {}; // Type guard // eslint-disable-next-line @typescript-eslint/no-explicit-any -function isTextNode(node: any): node is lowlight.AST.Text { - return node.value !== undefined; +function isTextNode(node: lowlight.HastNode): node is lowlight.AST.Text { + return node.type === 'text'; } // Type guard // eslint-disable-next-line @typescript-eslint/no-explicit-any function isElementNode(node: any): node is lowlight.AST.Element { - return node.properties !== undefined; + return node.type === 'element'; } function processHastNode(node: lowlight.HastNode, context: Context): void { - if (node.type !== 'text' && node.type !== 'element') { + if (isTextNode(node)) { + // For code blocks, replace line feeds with vertical tabs to keep + // the block as a single paragraph. This avoid the extra vertical + // space that appears between paragraphs + context.appendText(node.value.replace(/\n/g, '\u000b')); return; } - const ruleName = node.type === 'text' ? 'text' : node.tagName; - const fn = hastRules[ruleName]; - if (!fn) { - return; + if (isElementNode(node)) { + const ruleName = node.tagName; + const fn = hastRules[ruleName]; + if (!fn) { + return; + } + fn(node, context); } - fn(node, context); } function extractStyle( node: lowlight.HastNode, - cssRules: object + cssRules: {[key: string]: CssRule} ): StyleDefinition { + let style = {}; if (!isElementNode(node)) { - return; + return style; } const classNames = node.properties['className']; - let style = {}; for (const cls of classNames || []) { const normalizedClassName = cls.replace(/-/g, '_'); const rule = cssRules[normalizedClassName]; @@ -67,21 +73,11 @@ function extractStyle( return style; } -hastRules['text'] = (node, context) => { - if (!isTextNode(node)) { - return; - } - // For code blocks, replace line feeds with vertical tabs to keep - // the block as a single paragraph. This avoid the extra vertical - // space that appears between paragraphs - context.appendText(node.value.replace(/\n/g, '\u000b')); -}; - hastRules['span'] = (node, context) => { if (!isElementNode(node)) { return; } - const style = extractStyle(node, context.css); + const style = extractStyle(node, context.css ?? {}); context.startStyle(style); for (const childNode of node.children || []) { processHastNode(childNode as lowlight.HastNode, context); diff --git a/src/slide_generator.ts b/src/slide_generator.ts index 1cd84ab..ee315ef 100644 --- a/src/slide_generator.ts +++ b/src/slide_generator.ts @@ -22,6 +22,7 @@ import uploadLocalImage from './images/upload'; import {OAuth2Client} from 'google-auth-library'; import probeImage from './images/probe'; import maybeGenerateImage from './images/generate'; +import assert from 'assert'; const debug = Debug('md2gslides'); @@ -44,7 +45,7 @@ const debug = Debug('md2gslides'); * @see https://github.com/google/google-api-nodejs-client */ export default class SlideGenerator { - private slides: SlideDefinition[]; + private slides: SlideDefinition[] = []; private api: SlidesV1.Slides; private presentation: SlidesV1.Schema$Presentation; private allowUpload = false; @@ -102,19 +103,20 @@ export default class SlideGenerator { name: title, }, }); + assert(res.data.id); return SlideGenerator.forPresentation(oauth2Client, res.data.id); } /** * Returns a generator that writes to an existing presentation. * - * @param {gOAuth2Client} oauth2Client User credentials + * @param {OAuth2Client} oauth2Client User credentials * @param {string} presentationId ID of presentation to use * @returns {Promise.} */ public static async forPresentation( oauth2Client: OAuth2Client, - presentationId + presentationId: string ): Promise { const api = google.slides({version: 'v1', auth: oauth2Client}); const res = await api.presentations.get({presentationId: presentationId}); @@ -126,12 +128,15 @@ export default class SlideGenerator { * Generate slides from markdown * * @param {String} markdown Markdown to import + * @param css + * @param useFileio * @returns {Promise.} ID of generated slide */ public async generateFromMarkdown( - markdown, - {css, useFileio} + markdown: string, + {css, useFileio}: {css: string; useFileio: boolean} ): Promise { + assert(this.presentation?.presentationId); this.slides = extractSlides(markdown, css); this.allowUpload = useFileio; await this.generateImages(); @@ -150,8 +155,9 @@ export default class SlideGenerator { */ public async erase(): Promise { debug('Erasing previous slides'); - if (this.presentation.slides === null) { - return Promise.resolve(null); + assert(this.presentation?.presentationId); + if (!this.presentation.slides) { + return Promise.resolve(); } const requests = this.presentation.slides.map(slide => ({ @@ -187,7 +193,10 @@ export default class SlideGenerator { } protected async uploadLocalImages(): Promise { - const uploadImageifLocal = async (image): Promise => { + const uploadImageifLocal = async ( + image: ImageDefinition + ): Promise => { + assert(image.url); const parsedUrl = new URL(image.url); if (parsedUrl.protocol !== 'file:') { return; @@ -259,10 +268,13 @@ export default class SlideGenerator { * @param batch Batch of operations to execute * @returns {Promise.<*>} */ - protected async updatePresentation(batch): Promise { + protected async updatePresentation( + batch: SlidesV1.Schema$BatchUpdatePresentationRequest + ): Promise { debug('Updating presentation: %O', batch); - if (batch.requests.length === 0) { - return Promise.resolve(null); + assert(this.presentation?.presentationId); + if (!batch.requests || batch.requests.length === 0) { + return Promise.resolve(); } const res = await this.api.presentations.batchUpdate({ presentationId: this.presentation.presentationId, @@ -277,6 +289,7 @@ export default class SlideGenerator { * @returns {Promise.<*>} */ protected async reloadPresentation(): Promise { + assert(this.presentation?.presentationId); const res = await this.api.presentations.get({ presentationId: this.presentation.presentationId, }); diff --git a/src/slides.ts b/src/slides.ts index de3299d..013f525 100644 --- a/src/slides.ts +++ b/src/slides.ts @@ -59,7 +59,7 @@ export interface LinkDefinition { } export interface BodyDefinition { - text: TextDefinition; + text: TextDefinition | undefined; images: ImageDefinition[]; videos: VideoDefinition[]; } diff --git a/test/auth.spec.ts b/test/auth.spec.ts index ebc8f2a..fd4e5f7 100644 --- a/test/auth.spec.ts +++ b/test/auth.spec.ts @@ -64,6 +64,7 @@ describe('UserAuthorizer', () => { clientId: '123', clientSecret: 'abc', filePath: '/not_a_real_dir/token.json', + prompt: () => Promise.resolve('code'), }; new UserAuthorizer(options); expect(() => fs.accessSync('/not_a_real_dir')).to.not.throw(Error); @@ -74,17 +75,15 @@ describe('UserAuthorizer', () => { clientId: '123', clientSecret: 'abc', filePath: '/tmp/tokens.json', - prompt: function () { - return Promise.reject(new Error('Prompt not expected')); - }, + prompt: () => Promise.reject(new Error('Prompt not expected')), }; describe('with no saved token', () => { it('should report error if no code provided', () => { - const authorizer = new UserAuthorizer(options); - authorizer.prompt = () => { - return Promise.resolve(null); - }; + const authorizer = new UserAuthorizer({ + ...options, + prompt: () => Promise.resolve('code'), + }); const credentials = authorizer.getUserCredentials( 'user@example.com', 'https://www.googleapis.com/auth/slides' @@ -94,10 +93,10 @@ describe('UserAuthorizer', () => { it('should report error if invalid code provided', () => { stubTokenRequestError(); - const authorizer = new UserAuthorizer(options); - authorizer.prompt = () => { - return Promise.resolve('not a valid code'); - }; + const authorizer = new UserAuthorizer({ + ...options, + prompt: () => Promise.resolve('not a valid code'), + }); const credentials = authorizer.getUserCredentials( 'user@example.com', 'https://www.googleapis.com/auth/slides' @@ -107,10 +106,10 @@ describe('UserAuthorizer', () => { it('should exchange the code if provided', () => { stubTokenRequest(); - const authorizer = new UserAuthorizer(options); - authorizer.prompt = () => { - return Promise.resolve('code'); - }; + const authorizer = new UserAuthorizer({ + ...options, + prompt: () => Promise.resolve('code'), + }); const credentials = authorizer.getUserCredentials( 'user@example.com', 'https://www.googleapis.com/auth/slides' diff --git a/test/extract_slides.spec.ts b/test/extract_slides.spec.ts index aacef9e..d95e03a 100644 --- a/test/extract_slides.spec.ts +++ b/test/extract_slides.spec.ts @@ -58,7 +58,7 @@ describe('extractSlides', () => { }); it('should have no title', () => { - return expect(slides).to.have.nested.property('[0].title', null); + return expect(slides).to.not.have.nested.property('[0].title'); }); it('should have empty bodies', () => { diff --git a/test/generic_layout.spec.ts b/test/generic_layout.spec.ts index 642dec6..4063903 100644 --- a/test/generic_layout.spec.ts +++ b/test/generic_layout.spec.ts @@ -18,6 +18,8 @@ import chaiSubset from 'chai-subset'; import path from 'path'; import GenericLayout from '../src/layout/generic_layout'; import jsonfile from 'jsonfile'; +import {slides_v1} from 'googleapis'; +import {SlideDefinition} from '../src/slides'; const expect = chai.expect; chai.use(chaiAsPromised); @@ -30,10 +32,10 @@ describe('GenericLayout', () => { ); describe('with title slide', () => { - const requests = []; + const requests: slides_v1.Schema$Request[] = []; before(() => { - const input = { + const input: SlideDefinition = { objectId: 'title-slide', title: { rawText: 'This is a title slide', @@ -47,11 +49,8 @@ describe('GenericLayout', () => { listMarkers: [], big: false, }, - backgroundImage: null, bodies: [], tables: [], - videos: [], - images: [], notes: { rawText: 'Speaker notes here.', textRuns: [], @@ -92,10 +91,10 @@ describe('GenericLayout', () => { }); describe('with title & body slide', () => { - const requests = []; + const requests: slides_v1.Schema$Request[] = []; before(() => { - const input = { + const input: SlideDefinition = { objectId: 'body-slide', title: { rawText: 'Title & body slide', @@ -103,10 +102,10 @@ describe('GenericLayout', () => { listMarkers: [], big: false, }, - subtitle: null, - backgroundImage: null, bodies: [ { + images: [], + videos: [], text: { rawText: 'This is the slide body.\n', textRuns: [], @@ -116,8 +115,6 @@ describe('GenericLayout', () => { }, ], tables: [], - videos: [], - images: [], }; const layout = new GenericLayout('', presentation, input); layout.appendContentRequests(requests); @@ -143,16 +140,15 @@ describe('GenericLayout', () => { }); describe('with two column slide', () => { - const requests = []; + const requests: slides_v1.Schema$Request[] = []; before(() => { - const input = { + const input: SlideDefinition = { objectId: 'two-column-slide', - title: null, - subtitle: null, - backgroundImage: null, bodies: [ { + images: [], + videos: [], text: { big: false, rawText: 'This is the left column\n', @@ -161,6 +157,8 @@ describe('GenericLayout', () => { }, }, { + images: [], + videos: [], text: { big: false, rawText: 'This is the right column\n', @@ -170,8 +168,6 @@ describe('GenericLayout', () => { }, ], tables: [], - videos: [], - images: [], }; const layout = new GenericLayout('', presentation, input); layout.appendContentRequests(requests); @@ -197,20 +193,23 @@ describe('GenericLayout', () => { }); describe('with background images', () => { - const requests = []; + const requests: slides_v1.Schema$Request[] = []; before(() => { - const input = { + const input: SlideDefinition = { objectId: 'body-slide', - title: null, - subtitle: null, backgroundImage: { url: 'https://placekitten.com/1600/900', width: 1600, height: 900, + padding: 0, + offsetX: 0, + offsetY: 0, }, bodies: [ { + images: [], + videos: [], text: { big: false, rawText: '\n', @@ -243,14 +242,11 @@ describe('GenericLayout', () => { }); describe('with inline images', () => { - const requests = []; + const requests: slides_v1.Schema$Request[] = []; before(() => { - const input = { + const input: SlideDefinition = { objectId: 'body-slide', - title: null, - subtitle: null, - backgroundImage: null, tables: [], bodies: [ { @@ -260,8 +256,17 @@ describe('GenericLayout', () => { url: 'https://placekitten.com/350/315', width: 350, height: 315, + padding: 0, + offsetX: 0, + offsetY: 0, }, ], + text: { + rawText: '', + big: false, + listMarkers: [], + textRuns: [], + }, }, ], }; @@ -284,14 +289,11 @@ describe('GenericLayout', () => { }); describe('with video', () => { - const requests = []; + const requests: slides_v1.Schema$Request[] = []; before(() => { - const input = { + const input: SlideDefinition = { objectId: 'body-slide', - title: null, - subtitle: null, - backgroundImage: null, bodies: [ { videos: [ @@ -303,6 +305,12 @@ describe('GenericLayout', () => { }, ], images: [], + text: { + rawText: '', + big: false, + listMarkers: [], + textRuns: [], + }, }, ], tables: [], @@ -324,14 +332,11 @@ describe('GenericLayout', () => { }); describe('with table', () => { - const requests = []; + const requests: slides_v1.Schema$Request[] = []; before(() => { - const input = { + const input: SlideDefinition = { objectId: 'body-slide', - title: null, - subtitle: null, - backgroundImage: null, bodies: [], tables: [ { @@ -450,8 +455,8 @@ describe('GenericLayout', () => { insertText: { text: 'Animal', cellLocation: { - rowIndex: '0', - columnIndex: '0', + rowIndex: 0, + columnIndex: 0, }, }, }, @@ -460,16 +465,15 @@ describe('GenericLayout', () => { }); describe('with formatted text', () => { - const requests = []; + const requests: slides_v1.Schema$Request[] = []; before(() => { - const input = { + const input: SlideDefinition = { objectId: 'body-slide', - title: null, - subtitle: null, - backgroundImage: null, bodies: [ { + images: [], + videos: [], text: { big: false, rawText: 'Item 1\nItem 2\n\tfoo\n\tbar\n\tbaz\nItem 3\n', diff --git a/test/match_layout.spec.ts b/test/match_layout.spec.ts index 62cb037..f685835 100644 --- a/test/match_layout.spec.ts +++ b/test/match_layout.spec.ts @@ -16,97 +16,142 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import matchLayout from '../src/layout/match_layout'; import {SlideDefinition} from '../src/slides'; -import {slides_v1 as SlidesV1} from 'googleapis'; const expect = chai.expect; chai.use(chaiAsPromised); describe('matchLayout', () => { - const tests = [ + const tests: [string, SlideDefinition][] = [ [ 'TITLE', { - title: {rawText: 'title'}, - subtitle: {rawText: 'subtitle'}, + title: {rawText: 'title', listMarkers: [], textRuns: [], big: false}, + subtitle: { + rawText: 'subtitle', + listMarkers: [], + textRuns: [], + big: false, + }, bodies: [], tables: [], - images: [], - videos: [], }, ], [ 'SECTION_HEADER', { - title: {rawText: 'title'}, - subtitle: null, + title: {rawText: 'title', listMarkers: [], textRuns: [], big: false}, bodies: [], tables: [], - images: [], - videos: [], }, ], [ 'MAIN_POINT', { - title: {rawText: 'title', big: true}, - subtitle: null, + title: {rawText: 'title', big: true, listMarkers: [], textRuns: []}, bodies: [], tables: [], - images: [], - videos: [], }, ], [ 'SECTION_TITLE_AND_DESCRIPTION', { - title: {rawText: 'title'}, - subtitle: {rawText: 'subtitle'}, - bodies: [{text: {rawText: 'body'}}], + title: {rawText: 'title', listMarkers: [], textRuns: [], big: false}, + subtitle: { + rawText: 'subtitle', + listMarkers: [], + textRuns: [], + big: false, + }, + bodies: [ + { + text: {rawText: 'body', listMarkers: [], textRuns: [], big: false}, + images: [], + videos: [], + }, + ], tables: [], }, ], [ 'BIG_NUMBER', { - title: {rawText: 'title', big: true}, - subtitle: null, - bodies: [{text: {rawText: 'body'}}], + title: {rawText: 'title', big: true, listMarkers: [], textRuns: []}, + bodies: [ + { + text: {rawText: 'body', listMarkers: [], textRuns: [], big: false}, + images: [], + videos: [], + }, + ], tables: [], }, ], [ 'TITLE_AND_TWO_COLUMNS', { - title: {rawText: 'title'}, - subtitle: null, - bodies: [{text: {rawText: 'column1'}}, {text: {rawText: 'column2'}}], + title: {rawText: 'title', listMarkers: [], textRuns: [], big: false}, + bodies: [ + { + text: { + rawText: 'column1', + listMarkers: [], + textRuns: [], + big: false, + }, + images: [], + videos: [], + }, + { + text: { + rawText: 'column2', + listMarkers: [], + textRuns: [], + big: false, + }, + images: [], + videos: [], + }, + ], tables: [], }, ], [ 'TITLE_AND_BODY', { - title: {rawText: 'title'}, - subtitle: null, - bodies: [{text: {rawText: 'body'}}], + title: {rawText: 'title', listMarkers: [], textRuns: [], big: false}, + bodies: [ + { + text: {rawText: 'body', listMarkers: [], textRuns: [], big: false}, + images: [], + videos: [], + }, + ], tables: [], - images: [], - videos: [], }, ], [ 'TITLE_AND_BODY', { - title: {rawText: 'title'}, - subtitle: null, + title: {rawText: 'title', listMarkers: [], textRuns: [], big: false}, bodies: [ { images: [ { url: 'https://source.unsplash.com/78A265wPiO4/1600x900', padding: 0, + offsetX: 0, + offsetY: 0, + height: 900, + width: 1600, }, ], + videos: [], + text: { + big: false, + textRuns: [], + listMarkers: [], + rawText: '', + }, }, ], tables: [], @@ -115,8 +160,6 @@ describe('matchLayout', () => { [ 'BLANK', { - title: null, - subtitle: null, bodies: [], tables: [], }, @@ -124,27 +167,35 @@ describe('matchLayout', () => { [ 'TITLE_AND_BODY', { - title: null, - subtitle: null, bodies: [ { images: [ { url: 'https://source.unsplash.com/78A265wPiO4/1600x900', padding: 0, + offsetX: 0, + offsetY: 0, + height: 900, + width: 1600, }, ], + videos: [], + text: { + rawText: '', + listMarkers: [], + textRuns: [], + big: false, + }, }, ], tables: [], - videos: [], }, ], ]; for (const test of tests) { it(`should match ${test[0]}`, () => { - const layout = matchLayout(null, test[1] as SlideDefinition); + const layout = matchLayout({}, test[1]); expect(layout.name).to.eql(test[0]); }); } @@ -152,13 +203,9 @@ describe('matchLayout', () => { describe('matchCustomLayout', () => { it('should use a custom layout', () => { - const slide = { - title: null, - subtitle: null, + const slide: SlideDefinition = { bodies: [], tables: [], - images: [], - videos: [], customLayout: 'mylayout', }; @@ -172,10 +219,7 @@ describe('matchCustomLayout', () => { }, ], }; - const layout = matchLayout( - presentation as SlidesV1.Schema$Presentation, - slide - ); + const layout = matchLayout(presentation, slide); expect(layout.name).to.eql('MYLAYOUT'); }); }); diff --git a/test/slide_generator.spec.ts b/test/slide_generator.spec.ts index bb6a53c..2475567 100644 --- a/test/slide_generator.spec.ts +++ b/test/slide_generator.spec.ts @@ -24,7 +24,7 @@ const expect = chai.expect; chai.use(chaiAsPromised); function buildCredentials(): OAuth2Client { - const oauth2Client = new OAuth2Client('test', 'test', null); + const oauth2Client = new OAuth2Client('test', 'test'); oauth2Client.setCredentials({ access_token: 'abc', token_type: ' Bearer', diff --git a/tsconfig.json b/tsconfig.json index 05ef485..b3c5054 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "rootDir": ".", "outDir": "lib", - "esModuleInterop": true + "esModuleInterop": true, + "noEmit": true }, "include": [ "src/**/*.ts", diff --git a/yarn.lock b/yarn.lock index 005cf1d..d8744ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -997,6 +997,25 @@ "resolved" "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz" "version" "0.12.2" +"@types/chai-as-promised@^7.1.4": + "integrity" "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==" + "resolved" "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz" + "version" "7.1.4" + dependencies: + "@types/chai" "*" + +"@types/chai-subset@^1.3.3": + "integrity" "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==" + "resolved" "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz" + "version" "1.3.3" + dependencies: + "@types/chai" "*" + +"@types/chai@*", "@types/chai@^4.2.19": + "integrity" "sha512-jRJgpRBuY+7izT7/WNXP/LsMO9YonsstuL+xuvycDyESpoDoIAsMd7suwpB4h9oEWB+ZlPTqJJ8EHomzNhwTPQ==" + "resolved" "https://registry.npmjs.org/@types/chai/-/chai-4.2.19.tgz" + "version" "4.2.19" + "@types/debug@^4.1.5": "integrity" "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" "resolved" "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz" @@ -1017,6 +1036,13 @@ "resolved" "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz" "version" "7.0.7" +"@types/jsonfile@^6.0.0": + "integrity" "sha512-mUHbRieyluPtL3c466K7oUGua1lAVlz45PV4U3bHs5CXdBlDIeXJI5xQXa6IZYnrgmcJzJp/CiTZB4zfShAi6w==" + "resolved" "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.0.0.tgz" + "version" "6.0.0" + dependencies: + "@types/node" "*" + "@types/linkify-it@*": "integrity" "sha512-pQv3Sygwxxh6jYQzXaiyWDAHevJqWtqDUv6t11Sa9CPGiXny66II7Pl6PR8QO5OVysD6HYOkHMeBgIjLnk9SkQ==" "resolved" "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.1.tgz" @@ -1034,6 +1060,11 @@ dependencies: "@types/lodash" "*" +"@types/lowlight@^0.0.2": + "integrity" "sha512-37DldsUs2l4rXI2YQgVn+NKVEaaUbBIzJg3eYzAXimGrtre8vxqE65wAGqYs9W6IsoOfgj74se/rBc9yoRXOHQ==" + "resolved" "https://registry.npmjs.org/@types/lowlight/-/lowlight-0.0.2.tgz" + "version" "0.0.2" + "@types/markdown-it@12.0.2": "integrity" "sha512-p4DIfLMmGN0iLSbMxknDXeSm8W2ZRqQeN/1EAwVxVqJietzgp3WeP1UQjCKWDXWBcEbUa1ECx8YAfdpQdDQmZQ==" "resolved" "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.0.2.tgz" @@ -1060,6 +1091,18 @@ dependencies: "@types/node" "*" +"@types/mocha@^8.2.2": + "integrity" "sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==" + "resolved" "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.2.tgz" + "version" "8.2.2" + +"@types/mock-fs@^4.13.0": + "integrity" "sha512-FUqxhURwqFtFBCuUj3uQMp7rPSQs//b3O9XecAVxhqS9y4/W8SIJEZFq2mmpnFVZBXwR/2OyPLE97CpyYiB8Mw==" + "resolved" "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.0.tgz" + "version" "4.13.0" + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@15.12.4": "integrity" "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==" "resolved" "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz" @@ -1075,6 +1118,11 @@ "resolved" "https://registry.npmjs.org/@types/parse-color/-/parse-color-1.0.0.tgz" "version" "1.0.0" +"@types/parse5@^6.0.0": + "integrity" "sha512-oPwPSj4a1wu9rsXTEGIJz91ISU725t0BmSnUhb57sI+M8XEmvUop84lzuiYdq0Y5M6xLY8DBPg0C2xEQKLyvBA==" + "resolved" "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.0.tgz" + "version" "6.0.0" + "@types/probe-image-size@^7.0.0": "integrity" "sha512-8PH89IXb4WAOK293gyZvD1bajmIjUqEZ+pwVjY4wzLyUwxaUNbnT4FzNAJP9SW9wbtcc7brbN6q/9kYcDY6RIA==" "resolved" "https://registry.npmjs.org/@types/probe-image-size/-/probe-image-size-7.0.0.tgz" @@ -1118,6 +1166,11 @@ dependencies: "@types/node" "*" +"@types/tmp@^0.2.0": + "integrity" "sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ==" + "resolved" "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz" + "version" "0.2.0" + "@types/tough-cookie@*": "integrity" "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" "resolved" "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz"