Permalink
Browse files

#3 - Support speaker notes via comment blocks

  • Loading branch information...
1 parent 1f7cd01 commit 4fa1f44c37a6cb236109185fe902a2e7c43b9627 @sqrrrl sqrrrl committed Feb 9, 2017
View
@@ -175,7 +175,7 @@ the end of an image URL.
### Videos
-Includes YouTube videos with a modified image tag.
+Include YouTube videos with a modified image tag.
<pre>
---
@@ -187,6 +187,24 @@ Includes YouTube videos with a modified image tag.
![Slide with video](https://github.com/googlesamples/md2googleslides/raw/master/examples/video_slide.png)
+### Speaker notes
+
+Include speaker notes for a slide using HTML comments. Text inside
+the comments may include markdown for formatting, though only text
+formatting is allowed. Videos, images, and tables are ignored inside
+speaker notes.
+
+<pre>
+ ---
+
+ # Slide title
+
+ ![](https://placekitten.com/1600/900){.background}
+
+ &lt;!--
+ These are speaker notes.
+ --&gt;
+</pre>
### Formatting
View
@@ -36,7 +36,7 @@
"debug": "^2.2.0",
"extend": "^3.0.0",
"google-auth-library": "^0.9.8",
- "googleapis": "^14.2.0",
+ "googleapis": "~16.1",
"highlight.js": "^9.7.0",
"inline-styles-parse": "^1.2.0",
"jsonfile": "^2.4.0",
View
@@ -27,9 +27,22 @@ const parseColor = require('parse-color');
const parse5 = require('parse5');
const inlineStylesParse = require('inline-styles-parse');
const markdownTokenRules = {};
+const inlineTokenRules = {};
const htmlTokenRules = {};
const NO_OP = function() {};
+const mdOptions = {
+ html: true,
+ langPrefix: 'highlight ',
+ linkify: false,
+ breaks: false
+};
+const parser = markdownIt(mdOptions)
+ .use(attrs)
+ .use(lazyHeaders)
+ .use(emoji, {shortcuts: {}})
+ .use(expandTabs, {tabWidth: 4})
+ .use(video, { youtube: { width: 640, height: 390 }});
/**
* Parse the markdown and converts it into a form more suitable
@@ -45,65 +58,60 @@ const NO_OP = function() {};
function extractSlides(markdown, stylesheet) {
let tokens = parseMarkdown(markdown);
let css = nativeCSS.convert(stylesheet);
- let env = {
- slides: [],
- currentSlide: null,
- styles: [{
- bold: undefined,
- italic: undefined,
- fontFamily: undefined,
- foregroundColor: undefined,
- link: undefined,
- backgroundColor: undefined,
- underline: undefined,
- strikethrough: undefined,
- smallCaps: undefined,
- baselineOffset: undefined
- }],
- listDepth: 0,
- css: css,
- inlineHtmlContext: undefined
- };
-
+ let env = newEnv(markdownTokenRules, css);
startSlide(env);
+ processTokens(tokens, env);
+ endSlide(env);
+ debug(JSON.stringify(env.slides, null, 2));
+ return env.slides;
+}
+function processTokens(tokens, env) {
for(let index in tokens) {
let token = tokens[index];
if (token.type == 'hr' && index == 0) {
continue; // Skip leading HR since no previous slide
}
processMarkdownToken(token, env);
}
- endSlide(env);
- debug(JSON.stringify(env.slides, null, 2));
- return env.slides;
}
function parseMarkdown(markdown) {
- const mdOptions = {
- html: true,
- langPrefix: 'highlight ',
- linkify: false,
- breaks: false
- };
- const parser = markdownIt(mdOptions)
- .use(attrs)
- .use(lazyHeaders)
- .use(emoji, {shortcuts: {}})
- .use(expandTabs, {tabWidth: 4})
- .use(video, { youtube: { width: 640, height: 390 }});
return parser.parse(markdown);
}
function processMarkdownToken(token, env) {
- let rule = markdownTokenRules[token.type];
+ let rule = env.rules[token.type];
if (rule) {
rule(token, env);
} else {
debug(`Ignoring token ${token.type}`);
}
}
+function newEnv(rules, css) {
+ return {
+ rules: rules,
+ slides: [],
+ currentSlide: null,
+ styles: [{
+ bold: undefined,
+ italic: undefined,
+ fontFamily: undefined,
+ foregroundColor: undefined,
+ link: undefined,
+ backgroundColor: undefined,
+ underline: undefined,
+ strikethrough: undefined,
+ smallCaps: undefined,
+ baselineOffset: undefined
+ }],
+ listDepth: 0,
+ css: css,
+ inlineHtmlContext: undefined,
+ restrictToInline: false
+ };
+}
function startTextBlock(env) {
env.text = {
rawText: '',
@@ -131,7 +139,12 @@ function startSlide(env) {
bodies: [],
tables: [],
videos: [],
- images: []
+ images: [],
+ notes: {
+ rawText: '',
+ textRuns: [],
+ listMarkers: []
+ }
};
}
@@ -194,6 +207,22 @@ markdownTokenRules['inline'] = function(token, env) {
}
};
+markdownTokenRules['html_block'] = function(token, env) {
+ var re = /<!--([\s\S]*)-->/m;
+ var match = re.exec(token.content);
+ if (match == null) {
+ throw new Error('Unsupported HTML block: ' + token.content);
+ }
+ // Since the notes can contain unparsed markdown, create a new environment
+ // to process it so we don't inadvertently lose state. Just carry
+ // forward the notes from the current slide to append to
+ var subEnv = newEnv(inlineTokenRules, env.css);
+ subEnv.text = env.currentSlide.notes;
+ var tokens = parseMarkdown(match[1]);
+ processTokens(tokens, subEnv);
+};
+
+
markdownTokenRules['html_inline'] = function(token, env) {
const fragment = parse5.parseFragment(token.content, env.inlineHtmlContext);
if(fragment.childNodes.length) {
@@ -473,6 +502,39 @@ markdownTokenRules['list_item_open'] = function(token, env) {
markdownTokenRules['list_item_close'] = NO_OP;
+// These rules are specific to parsing markdown in an inline context.
+
+inlineTokenRules['heading_open'] = function(token, env) {
+ startStyle({bold: true}, env); // TODO - Better style for inline headers
+};
+inlineTokenRules['heading_close'] = function(token, env) {
+ endStyle(env);
+};
+inlineTokenRules['inline'] = markdownTokenRules['inline'];
+inlineTokenRules['html_inline'] = markdownTokenRules['html_inline'];
+inlineTokenRules['text'] = markdownTokenRules['text'];
+inlineTokenRules['paragraph_open'] = markdownTokenRules['paragraph_open'];
+inlineTokenRules['paragraph_close'] = markdownTokenRules['paragraph_close'];
+inlineTokenRules['fence'] = markdownTokenRules['fence'];
+inlineTokenRules['em_open'] = markdownTokenRules['em_open'];
+inlineTokenRules['em_close'] = markdownTokenRules['em_close'];
+inlineTokenRules['s_open'] = markdownTokenRules['s_open'];
+inlineTokenRules['s_close'] = markdownTokenRules['s_close'];
+inlineTokenRules['strong_open'] = markdownTokenRules['strong_open'];
+inlineTokenRules['strong_close'] = markdownTokenRules['strong_close'];
+inlineTokenRules['link_open'] = markdownTokenRules['link_open'];
+inlineTokenRules['link_close'] = markdownTokenRules['link_close'];
+inlineTokenRules['code_inline'] = markdownTokenRules['code_inline'];
+inlineTokenRules['hardbreak'] = markdownTokenRules['hardbreak'];
+inlineTokenRules['softbreak'] = markdownTokenRules['softbreak'];
+inlineTokenRules['blockquote_open'] = markdownTokenRules['blockquote_open'];
+inlineTokenRules['blockquote_close'] = markdownTokenRules['blockquote_close'];
+inlineTokenRules['emoji'] = markdownTokenRules['emoji'];
+inlineTokenRules['bullet_list_open'] = markdownTokenRules['bullet_list_open'];
+inlineTokenRules['bullet_list_close'] = markdownTokenRules['bullet_list_close'];
+inlineTokenRules['blockquote_open'] = markdownTokenRules['blockquote_open'];
+
+
// These rules are specific to parsing syntax-highlighted code
// Currently these are a very small subset of HTML, limited to
// span elements.
View
@@ -70,6 +70,11 @@ class GenericLayout {
this.appendCreateVideoRequests(this.slide.videos, requests);
}
+ if (this.slide.notes && this.slide.notes.rawText.length) {
+ const objectId = this.presentation.findSpeakerNotesObjectId(this.slide.objectId);
+ this.appendInsertTextRequests(this.slide.notes, { objectId: objectId }, requests);
+ }
+
return requests;
}
View
@@ -120,6 +120,14 @@ class Presentation {
}
return null;
}
+
+ findSpeakerNotesObjectId(pageId) {
+ let page = this.findPage(pageId);
+ if (page) {
+ return page.slideProperties.notesPage.notesProperties.speakerNotesObjectId;
+ }
+ return null;
+ }
}
module.exports = Presentation;
@@ -354,4 +354,19 @@ describe('extractSlides', function() {
return expect(slides).to.have.deep.property('[0].bodies[0].textRuns[0].baselineOffset', 'SUPERSCRIPT');
});
});
+
+ describe('with speaker notes', function() {
+ const markdown =
+ '# Title\n' +
+ '<!-- Hello **world** -->\n';
+ const slides = extractSlides(markdown);
+
+ it('should have speaker notes', function() {
+ return expect(slides).to.have.deep.property('[0].notes.rawText', 'Hello world\n');
+ });
+
+ it('should have text runs', function() {
+ return expect(slides).to.have.deep.property('[0].notes.textRuns').length(1);
+ });
+ });
});
@@ -30,7 +30,14 @@
}
}
}
- ]
+ ],
+ "slideProperties": {
+ "notesPage": {
+ "notesProperties": {
+ "speakerNotesObjectId": "speaker-notes-element"
+ }
+ }
+ }
},
{
"objectId": "body-slide",
@@ -51,7 +51,13 @@ describe('GenericLayout', function() {
"bodies": [],
"tables": [],
"videos": [],
- "images": []
+ "images": [],
+ "notes": {
+ "rawText": "Speaker notes here.",
+ "textRuns": [],
+ "listMarkers": []
+ },
+
};
const layout = new GenericLayout('', presentation, input);
layout.appendContentRequests(requests);
@@ -74,6 +80,15 @@ describe('GenericLayout', function() {
}
});
});
+
+ it(`should insert speaker notes`, function() {
+ expect(requests).to.include({
+ "insertText": {
+ "text": "Speaker notes here.",
+ "objectId": "speaker-notes-element"
+ }
+ });
+ });
});
describe('with title & body slide', function() {

0 comments on commit 4fa1f44

Please sign in to comment.