Skip to content

Commit ce1661c

Browse files
committed
Fix image/video alignment with two column layouts, add file.io ack for CLI, update readme/changelog
1 parent c2b66a4 commit ce1661c

File tree

14 files changed

+304
-216
lines changed

14 files changed

+304
-216
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
## (pending)
44

55
* Support local image upload (via file.io) and rasterization of SVG and TeX/MathML expressions
6-
* Allow custom layouts via markdown
6+
* Fix image alignment when included in a column
77
* Allow offsets for images
8+
* Allow custom layouts via markdown
89
* Update dependencies
10+
* Migrate code to TypeScript
911

1012
## 0.4
1113

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,17 @@ are supported. For example, to use the github theme
295295
md2gslides slides.md --style github
296296
```
297297

298+
You can also apply additional style changes to the entire block, such as changing
299+
the font size:
300+
301+
<pre>
302+
### Hello World
303+
304+
```javascript
305+
console.log('Hello world');
306+
```{style="font-size: 36pt"}
307+
</pre>
308+
298309
### Tables
299310

300311
Tables are supported via
@@ -316,13 +327,47 @@ Dogs | 75 million
316327
Birds | 16 million
317328
</pre>
318329

330+
### Local images
331+
332+
Images referencing local paths temporarily uploaded and hosted to [file.io](https://file.io)
333+
334+
### Image rasterization
335+
336+
Slides can also include generated images, using `$$$` fenced blocks
337+
for the data. Currently supported generated images are math expression (TeX
338+
and MathML) as well as SVG
339+
340+
Using TeX:
341+
342+
<pre>
343+
# How about some math?
344+
345+
$$$ math
346+
\cos (2\theta) = \cos^2 \theta - \sin^2 \theta
347+
$$$
348+
</pre>
349+
350+
SVG
351+
352+
<pre>
353+
# Or some SVG?
354+
355+
$$$ svg
356+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48"><defs><path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/><path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/><path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/><path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/></svg>
357+
$$$
358+
</pre>
359+
360+
Like local images, generated images are temporarily served via file.io.
361+
362+
Pull requests for other image generators (e.g. mermaid, chartjs, etc.) are welcome!
363+
319364
## Reading from standard input
320365

321366
You can also pipe markdown into the tool by using `STDIN` as the file name.
322367

323368
## Contributing
324369

325-
With the exception of `/bin/md2gslides.js`, ES6 is used throughout and compiled
370+
With the exception of `/bin/md2gslides.js`, TypeScript is used throughout and compiled
326371
with [Babel](https://babeljs.io/). [Mocha](https://mochajs.org/) and [Chai](http://chaijs.com/)
327372
are used for testing.
328373

@@ -338,6 +383,12 @@ To run unit tests:
338383
npm run test
339384
```
340385

386+
To lint/format tests:
387+
388+
```sh
389+
npm run lint
390+
```
391+
341392
See [CONTRIBUTING](CONTRIBUTING.md) for additional terms.
342393

343394
## License

bin/md2gslides.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,17 @@ parser.addArgument(['-c', '--copy'], {
8080
dest: 'copy',
8181
required: false,
8282
});
83+
parser.addArgument(['--use-fileio'], {
84+
help: 'Acknolwedge local and generated images are uploaded to https://file.io',
85+
action: 'storeTrue',
86+
dest: 'useFileio',
87+
require: false,
88+
});
8389

8490
const args = parser.parseArgs();
8591

8692
function handleError(err) {
8793
console.log('Unable to generate slides:', err);
88-
console.log(err.stack);
89-
console.log(JSON.stringify(err, null, 2));
9094
}
9195

9296
function prompt(url) {
@@ -168,7 +172,10 @@ function generateSlides(slideGenerator) {
168172
const input = fs.readFileSync(file, { encoding: 'UTF-8' });
169173
const css = loadCss(args.style);
170174

171-
return slideGenerator.generateFromMarkdown(input, css);
175+
return slideGenerator.generateFromMarkdown(input, {
176+
css: css,
177+
useFileio: args.useFileio,
178+
});
172179
}
173180

174181
function displayResults(id) {

examples/example.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ This is the *right* column
5757

5858
---
5959

60+
# Columns can have images
61+
62+
This is the *left* column
63+
64+
{.column}
65+
66+
![](https://picsum.photos/g/1600/900)
67+
68+
---
69+
6070
# Slides can have background images
6171

6272
![](https://picsum.photos/g/1600/900){.background}
@@ -139,7 +149,7 @@ Birds | 16 million
139149
Use <span style="color:red">span</span> to color text.
140150

141151
Use <sup>superscript</sup> and <sub>subscript</sub>, <span style="text-decoration: line-through">strikethrough</span>
142-
or <span style="text-decoration: underline">underline</span>, even <span style="font-variant: small-caps">small caps.</span>
152+
or <span style="text-decoration: underline">underline</span>, even <span style="font-variant: small-caps">small <span style="color:green">caps</span>.</span>
143153

144154
---
145155
# How about some math?
@@ -164,3 +174,5 @@ Custom master slides can be selected by adding the attribute `{layout="Title and
164174
the slide layout will be chosen from the available master slides by the name.
165175

166176
This can be used with the flag `--copy=[presentation id]` to copy and use an existing presentation as the source rather than a blank slide.
177+
178+
---

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"exec": "npm run compile && node bin/md2gslides.js",
2020
"test": "mocha --require ./test/register --timeout 5000 \"test/**/*.spec.ts\"",
2121
"test-debug": "mocha --debug-brk --inspect --require ./test/register --timeout 5000 \"test/**/*.spec.ts\"",
22-
"lint": "./node_modules/.bin/eslint --fix \"src/**/*.ts\" bin",
22+
"lint": "./node_modules/.bin/eslint --fix \"src/**/*.ts\" \"test/**/*.ts\" bin",
2323
"eslint-check": "eslint --print-config . | eslint-config-prettier-check"
2424
},
2525
"repository": {

src/layout/generic_layout.ts

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,6 @@ interface BoundingBox {
3030
y: number;
3131
}
3232

33-
function asArrayOrNull<T>(value: T): T[] | null {
34-
if (!value) {
35-
return null;
36-
}
37-
return [value];
38-
}
39-
4033
/**
4134
* Performs most of the work of converting a slide into API requests.
4235
*
@@ -72,10 +65,9 @@ export default class GenericLayout {
7265
}
7366

7467
public appendContentRequests(requests: SlidesV1.Schema$Request[]): SlidesV1.Schema$Request[] {
75-
this.appendFillPlaceholderTextRequest(asArrayOrNull(this.slide.title), 'TITLE', requests);
76-
this.appendFillPlaceholderTextRequest(asArrayOrNull(this.slide.title), 'CENTERED_TITLE', requests);
77-
this.appendFillPlaceholderTextRequest(asArrayOrNull(this.slide.subtitle), 'SUBTITLE', requests);
78-
this.appendFillPlaceholderTextRequest(this.slide.bodies, 'BODY', requests);
68+
this.appendFillPlaceholderTextRequest(this.slide.title, 'TITLE', requests);
69+
this.appendFillPlaceholderTextRequest(this.slide.title, 'CENTERED_TITLE', requests);
70+
this.appendFillPlaceholderTextRequest(this.slide.subtitle, 'SUBTITLE', requests);
7971

8072
if (this.slide.backgroundImage) {
8173
this.appendSetBackgroundImageRequest(this.slide.backgroundImage, requests);
@@ -85,12 +77,18 @@ export default class GenericLayout {
8577
this.appendCreateTableRequests(this.slide.tables, requests);
8678
}
8779

88-
if (this.slide.images.length) {
89-
this.appendCreateImageRequests(this.slide.images, requests);
90-
}
91-
92-
if (this.slide.videos.length) {
93-
this.appendCreateVideoRequests(this.slide.videos, requests);
80+
if (this.slide.bodies) {
81+
const bodyElements = findPlaceholder(this.presentation, this.slide.objectId, 'BODY');
82+
this.slide.bodies.forEach((body, index) => {
83+
let placeholder = bodyElements[index];
84+
this.appendFillPlaceholderTextRequest(body.text, placeholder, requests);
85+
if (body.images && body.images.length) {
86+
this.appendCreateImageRequests(body.images, placeholder, requests);
87+
}
88+
if (body.videos && body.videos.length) {
89+
this.appendCreateVideoRequests(body.videos, placeholder, requests);
90+
}
91+
});
9492
}
9593

9694
if (this.slide.notes) {
@@ -102,28 +100,25 @@ export default class GenericLayout {
102100
}
103101

104102
protected appendFillPlaceholderTextRequest(
105-
values: TextDefinition[],
106-
placeholderName: string,
103+
value: TextDefinition,
104+
placeholder: string | SlidesV1.Schema$PageElement,
107105
requests: SlidesV1.Schema$Request[],
108106
): SlidesV1.Schema$Request[] {
109-
if (!(values && values.length)) {
110-
debug('No text for placeholder %s', placeholderName);
107+
if (!value) {
108+
debug('No text for placeholder %s');
111109
return;
112110
}
113111

114-
const pageElements = findPlaceholder(this.presentation, this.slide.objectId, placeholderName);
115-
if (!pageElements) {
116-
debug('Skipping undefined placeholder %s', placeholderName);
117-
return;
118-
}
119-
120-
for (let i in pageElements) {
121-
if (values[i] != undefined) {
122-
debug('Slide #%d: setting %s[%d] to %s', this.slide.index, placeholderName, i, values[i].rawText);
123-
let id = pageElements[i].objectId;
124-
this.appendInsertTextRequests(values[i], { objectId: id }, requests);
112+
if (typeof placeholder === 'string') {
113+
const pageElements = findPlaceholder(this.presentation, this.slide.objectId, placeholder);
114+
if (!pageElements) {
115+
debug('Skipping undefined placeholder %s', placeholder);
116+
return;
125117
}
118+
placeholder = pageElements[0];
126119
}
120+
121+
this.appendInsertTextRequests(value, { objectId: placeholder.objectId }, requests);
127122
}
128123

129124
protected appendInsertTextRequests(text: TextDefinition, locationProps, requests: SlidesV1.Schema$Request[]): void {
@@ -215,7 +210,7 @@ export default class GenericLayout {
215210
});
216211
}
217212

218-
protected appendCreateImageRequests(images, requests: SlidesV1.Schema$Request[]): void {
213+
protected appendCreateImageRequests(images, placeholder, requests: SlidesV1.Schema$Request[]): void {
219214
// TODO - Fix weird cast
220215
const layer = (Layout as (s: string) => Layout.PackingSmith)('left-right'); // TODO - Configurable?
221216
for (let image of images) {
@@ -227,7 +222,7 @@ export default class GenericLayout {
227222
});
228223
}
229224

230-
const box = this.getBodyBoundingBox(false);
225+
const box = this.getBodyBoundingBox(placeholder);
231226
const computedLayout = layer.export();
232227

233228
let scaleRatio = Math.min(box.width / computedLayout.width, box.height / computedLayout.height);
@@ -277,15 +272,19 @@ export default class GenericLayout {
277272
}
278273
}
279274

280-
protected appendCreateVideoRequests(videos: VideoDefinition[], requests: SlidesV1.Schema$Request[]): void {
275+
protected appendCreateVideoRequests(
276+
videos: VideoDefinition[],
277+
placeholder,
278+
requests: SlidesV1.Schema$Request[],
279+
): void {
281280
if (videos.length > 1) {
282281
throw new Error('Multiple videos per slide are not supported.');
283282
}
284283
const video = videos[0];
285284

286285
debug('Slide #%d: adding video %s', this.slide.index, video.id);
287286

288-
const box = this.getBodyBoundingBox(false);
287+
const box = this.getBodyBoundingBox(placeholder);
289288

290289
const scaleRatio = Math.min(box.width / video.width, box.height / video.height);
291290

@@ -389,10 +388,9 @@ export default class GenericLayout {
389388
};
390389
}
391390

392-
protected getBodyBoundingBox(fullScreen: boolean): BoundingBox {
393-
const body = findPlaceholder(this.presentation, this.slide.objectId, 'BODY');
394-
if (body && !fullScreen) {
395-
return this.calculateBoundingBox(body[0]);
391+
protected getBodyBoundingBox(placeholder): BoundingBox {
392+
if (placeholder) {
393+
return this.calculateBoundingBox(placeholder);
396394
}
397395
return {
398396
width: this.presentation.pageSize.width.magnitude,

src/layout/match_layout.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ function hasTextContent(slide: SlideDefinition): boolean {
6565

6666
// Anything which takes up the main body space
6767
function hasContent(slide: SlideDefinition): boolean {
68-
return (
69-
slide.bodies.length !== 0 || slide.tables.length !== 0 || slide.videos.length !== 0 || slide.images.length !== 0
70-
);
68+
return slide.bodies.length !== 0 || slide.tables.length !== 0;
7169
}
7270

7371
// Define rules for picking slide layouts based on the default

src/parser/env.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { SlideDefinition, TextDefinition, StyleDefinition, TableDefinition, ListDefinition } from '../slides';
1+
import {
2+
SlideDefinition,
3+
TextDefinition,
4+
StyleDefinition,
5+
TableDefinition,
6+
ListDefinition,
7+
ImageDefinition,
8+
VideoDefinition,
9+
} from '../slides';
210
import { uuid } from '../utils';
311
import extend from 'extend';
412
import * as _ from 'lodash';
@@ -16,6 +24,8 @@ export class Context {
1624
public table?: TableDefinition;
1725
public list?: ListDefinition;
1826
public inlineHtmlContext?: object;
27+
public images: ImageDefinition[] = [];
28+
public videos: VideoDefinition[] = [];
1929

2030
public constructor(css: Stylesheet) {
2131
this.css = css;
@@ -41,11 +51,19 @@ export class Context {
4151

4252
public endSlide(): void {
4353
if (this.currentSlide) {
44-
if (this.text && this.text.rawText.trim().length) {
45-
this.currentSlide.bodies.push(this.text);
54+
if (this.images.length || this.videos.length || (this.text && this.text.rawText.trim().length)) {
55+
this.currentSlide.bodies.push({
56+
text: this.text,
57+
images: this.images,
58+
videos: this.videos,
59+
});
60+
this.images = [];
61+
this.videos = [];
4662
}
4763
this.slides.push(this.currentSlide);
4864
}
65+
this.currentSlide = undefined;
66+
this.text = undefined;
4967
}
5068

5169
public startSlide(): void {
@@ -57,8 +75,6 @@ export class Context {
5775
backgroundImage: null,
5876
bodies: [],
5977
tables: [],
60-
videos: [],
61-
images: [],
6278
notes: null,
6379
};
6480
}

0 commit comments

Comments
 (0)