├── out └── .gitignore ├── .gitignore ├── .travis.yml ├── .jshintignore ├── test ├── mocha.opts ├── test_dom.js ├── runner.html └── remark │ ├── converter_test.js │ ├── api_test.js │ ├── models │ ├── slideshow_test.js │ └── slide_test.js │ ├── lexer_test.js │ ├── components │ └── timer_test.js │ ├── views │ ├── slideView_test.js │ └── slideshowView_test.js │ ├── controllers │ └── defaultController_test.js │ └── parser_test.js ├── src ├── templates │ ├── resources.js.template │ ├── highlighter.js.template │ └── boilerplate-single.html.template ├── remark │ ├── controllers │ │ ├── inputs │ │ │ ├── message.js │ │ │ ├── location.js │ │ │ ├── mouse.js │ │ │ ├── touch.js │ │ │ └── keyboard.js │ │ └── defaultController.js │ ├── dom.js │ ├── models │ │ ├── slideshow │ │ │ ├── events.js │ │ │ └── navigation.js │ │ ├── slide.js │ │ └── slideshow.js │ ├── utils.js │ ├── converter.js │ ├── components │ │ └── timer │ │ │ └── timer.js │ ├── scaler.js │ ├── views │ │ ├── notesView.js │ │ ├── slideView.js │ │ └── slideshowView.js │ ├── api.js │ ├── lexer.js │ ├── parser.js │ └── resources.js ├── remark.js ├── polyfills.js ├── remark.html └── remark.less ├── Makefile ├── .jshintrc ├── bower.json ├── package.json ├── LICENCE ├── boilerplate-local.html ├── boilerplate-remote.html ├── README.md ├── make.js └── HISTORY.md /out/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | vendor 3 | *.swp 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | src/remark/highlighter.js 2 | src/remark/resources.js 3 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --require sinon 3 | --require ./test/environment 4 | --require ./src/remark/utils 5 | --reporter dot 6 | --ui bdd 7 | -------------------------------------------------------------------------------- /src/templates/resources.js.template: -------------------------------------------------------------------------------- 1 | /* Automatically generated */ 2 | 3 | module.exports = { 4 | documentStyles: %DOCUMENT_STYLES%, 5 | containerLayout: %CONTAINER_LAYOUT% 6 | }; 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: deps 2 | node make all 3 | 4 | deps: 5 | npm install 6 | 7 | test: 8 | node make test 9 | 10 | bundle: 11 | node make bundle 12 | 13 | highlighter: 14 | node make highlighter 15 | 16 | .PHONY: deps test bundle highlighter 17 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "eqeqeq": true, 3 | "undef": true, 4 | "laxcomma": true, 5 | "browser": true, 6 | 7 | "predef": [ 8 | "require", 9 | "module", 10 | "exports", 11 | "location", 12 | "describe", 13 | "before", 14 | "it", 15 | "alert", 16 | "$" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/templates/highlighter.js.template: -------------------------------------------------------------------------------- 1 | /* Automatically generated */ 2 | 3 | var hljs = new (%HIGHLIGHTER_ENGINE%)() 4 | , languages = [%HIGHLIGHTER_LANGUAGES%] 5 | ; 6 | 7 | for (var i = 0; i < languages.length; ++i) { 8 | hljs.registerLanguage(languages[i].name, languages[i].create); 9 | } 10 | 11 | module.exports = { 12 | styles: %HIGHLIGHTER_STYLES%, 13 | engine: hljs 14 | }; 15 | -------------------------------------------------------------------------------- /src/remark/controllers/inputs/message.js: -------------------------------------------------------------------------------- 1 | exports.register = function (events) { 2 | addMessageEventListeners(events); 3 | }; 4 | 5 | function addMessageEventListeners (events) { 6 | events.on('message', navigateByMessage); 7 | 8 | function navigateByMessage(message) { 9 | var cap; 10 | 11 | if ((cap = /^gotoSlide:(\d+)$/.exec(message.data)) !== null) { 12 | events.emit('gotoSlide', parseInt(cap[1], 10), true); 13 | } 14 | else if (message.data === 'toggleBlackout') { 15 | events.emit('toggleBlackout'); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/remark/dom.js: -------------------------------------------------------------------------------- 1 | module.exports = Dom; 2 | 3 | function Dom () { } 4 | 5 | Dom.prototype.XMLHttpRequest = XMLHttpRequest; 6 | 7 | Dom.prototype.getHTMLElement = function () { 8 | return document.getElementsByTagName('html')[0]; 9 | }; 10 | 11 | Dom.prototype.getBodyElement = function () { 12 | return document.body; 13 | }; 14 | 15 | Dom.prototype.getElementById = function (id) { 16 | return document.getElementById(id); 17 | }; 18 | 19 | Dom.prototype.getLocationHash = function () { 20 | return window.location.hash; 21 | }; 22 | 23 | Dom.prototype.setLocationHash = function (hash) { 24 | window.location.hash = hash; 25 | }; 26 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remark", 3 | "homepage": "http://remarkjs.com/", 4 | "authors": [ 5 | "Ole Petter Bang " 6 | ], 7 | "description": "A simple, in-browser, markdown-driven slideshow tool.", 8 | "main": "out/remark.js", 9 | "keywords": [ 10 | "Markdown", 11 | "Slides" 12 | ], 13 | "license": "MIT", 14 | "ignore": [ 15 | ".gitignore", 16 | ".gitmodules", 17 | ".jshintignore", 18 | ".jshintrc", 19 | "HISTORY.md", 20 | "LICENCE", 21 | "make.js", 22 | "Makefile", 23 | "package.json", 24 | "bower_components", 25 | "node_modules", 26 | "src", 27 | "test", 28 | "vendor", 29 | "out/tests.js" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/remark/models/slideshow/events.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | 3 | module.exports = Events; 4 | 5 | function Events (events) { 6 | var self = this 7 | , externalEvents = new EventEmitter() 8 | ; 9 | 10 | externalEvents.setMaxListeners(0); 11 | 12 | self.on = function () { 13 | externalEvents.on.apply(externalEvents, arguments); 14 | return self; 15 | }; 16 | 17 | ['showSlide', 'hideSlide', 'beforeShowSlide', 'afterShowSlide', 'beforeHideSlide', 'afterHideSlide'].map(function (eventName) { 18 | events.on(eventName, function (slideIndex) { 19 | var slide = self.getSlides()[slideIndex]; 20 | externalEvents.emit(eventName, slide); 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/remark.js: -------------------------------------------------------------------------------- 1 | var Api = require('./remark/api') 2 | , highlighter = require('./remark/highlighter') 3 | , polyfills = require('./polyfills') 4 | , resources = require('./remark/resources') 5 | ; 6 | 7 | // Expose API as `remark` 8 | window.remark = new Api(); 9 | 10 | // Apply polyfills as needed 11 | polyfills.apply(); 12 | 13 | // Apply embedded styles to document 14 | styleDocument(); 15 | 16 | function styleDocument () { 17 | var styleElement = document.createElement('style') 18 | , headElement = document.getElementsByTagName('head')[0] 19 | , style 20 | ; 21 | 22 | styleElement.type = 'text/css'; 23 | headElement.insertBefore(styleElement, headElement.firstChild); 24 | 25 | styleElement.innerHTML = resources.documentStyles; 26 | 27 | for (style in highlighter.styles) { 28 | if (highlighter.styles.hasOwnProperty(style)) { 29 | styleElement.innerHTML = styleElement.innerHTML + 30 | highlighter.styles[style]; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/polyfills.js: -------------------------------------------------------------------------------- 1 | exports.apply = function () { 2 | forEach([Array, window.NodeList, window.HTMLCollection], extend); 3 | }; 4 | 5 | function forEach (list, f) { 6 | var i; 7 | 8 | for (i = 0; i < list.length; ++i) { 9 | f(list[i], i); 10 | } 11 | } 12 | 13 | function extend (object) { 14 | var prototype = object && object.prototype; 15 | 16 | if (!prototype) { 17 | return; 18 | } 19 | 20 | prototype.forEach = prototype.forEach || function (f) { 21 | forEach(this, f); 22 | }; 23 | 24 | prototype.filter = prototype.filter || function (f) { 25 | var result = []; 26 | 27 | this.forEach(function (element) { 28 | if (f(element, result.length)) { 29 | result.push(element); 30 | } 31 | }); 32 | 33 | return result; 34 | }; 35 | 36 | prototype.map = prototype.map || function (f) { 37 | var result = []; 38 | 39 | this.forEach(function (element) { 40 | result.push(f(element, result.length)); 41 | }); 42 | 43 | return result; 44 | }; 45 | } -------------------------------------------------------------------------------- /test/test_dom.js: -------------------------------------------------------------------------------- 1 | module.exports = TestDom; 2 | 3 | function TestDom () { 4 | this.html = document.createElement('html'); 5 | this.body = document.createElement('body'); 6 | } 7 | 8 | TestDom.prototype.XMLHttpRequest = FakeXMLHttpRequest; 9 | 10 | function FakeXMLHttpRequest () { 11 | this._opened = false; 12 | this._sent = false; 13 | }; 14 | 15 | FakeXMLHttpRequest.prototype.open = function () { 16 | this._opened = true; 17 | }; 18 | 19 | FakeXMLHttpRequest.prototype.send = function () { 20 | this._sent = true; 21 | 22 | if (this._opened) { 23 | this.responseText = FakeXMLHttpRequest.responseText; 24 | } 25 | }; 26 | 27 | TestDom.prototype.getHTMLElement = function () { 28 | return this.html; 29 | }; 30 | 31 | TestDom.prototype.getBodyElement = function () { 32 | return this.body; 33 | }; 34 | 35 | TestDom.prototype.getElementById = function () {} 36 | TestDom.prototype.getLocationHash = function () {}; 37 | TestDom.prototype.setLocationHash = function (hash) {}; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remark" 3 | , "version" : "0.6.5" 4 | , "dependencies": { 5 | "marked": "0.3.1" 6 | } 7 | , "repository": { 8 | "type": "git", 9 | "url": "https://github.com/gnab/remark.git" 10 | } 11 | , "devDependencies": { 12 | "browserify": "2.14.2" 13 | , "uglify-js": "2.3.5" 14 | , "less": "1.3.3" 15 | , "mocha": "1.17.0" 16 | , "mocha-phantomjs": "3.3.1" 17 | , "phantomjs": "1.9.2-6" 18 | , "should": "1.2.2" 19 | , "sinon": "1.8.2" 20 | , "jshint": "2.1.2" 21 | , "shelljs": "0.1.4" 22 | } 23 | , "scripts": { 24 | "test": "node make test" 25 | } 26 | , "config": { 27 | "highlighter": { 28 | "languages": [ 29 | "javascript" 30 | , "ruby" 31 | , "python" 32 | , "bash" 33 | , "java" 34 | , "php" 35 | , "perl" 36 | , "cpp" 37 | , "objectivec" 38 | , "cs" 39 | , "sql" 40 | , "xml" 41 | , "css" 42 | , "scala" 43 | , "coffeescript" 44 | , "lisp" 45 | , "clojure" 46 | , "http" 47 | , "haskell" 48 | ] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/remark/controllers/inputs/location.js: -------------------------------------------------------------------------------- 1 | exports.register = function (events, dom, slideshowView) { 2 | addLocationEventListeners(events, dom, slideshowView); 3 | }; 4 | 5 | function addLocationEventListeners (events, dom, slideshowView) { 6 | // If slideshow is embedded into custom DOM element, we don't 7 | // hook up to location hash changes, so just go to first slide. 8 | if (slideshowView.isEmbedded()) { 9 | events.emit('gotoSlide', 1); 10 | } 11 | // When slideshow is not embedded into custom DOM element, but 12 | // rather hosted directly inside document.body, we hook up to 13 | // location hash changes, and trigger initial navigation. 14 | else { 15 | events.on('hashchange', navigateByHash); 16 | events.on('slideChanged', updateHash); 17 | 18 | navigateByHash(); 19 | } 20 | 21 | function navigateByHash () { 22 | var slideNoOrName = (dom.getLocationHash() || '').substr(1); 23 | events.emit('gotoSlide', slideNoOrName); 24 | } 25 | 26 | function updateHash (slideNoOrName) { 27 | dom.setLocationHash('#' + slideNoOrName); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2013 Ole Petter Bang 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/remark/controllers/defaultController.js: -------------------------------------------------------------------------------- 1 | // Allow override of global `location` 2 | /* global location:true */ 3 | 4 | module.exports = Controller; 5 | 6 | var keyboard = require('./inputs/keyboard') 7 | , mouse = require('./inputs/mouse') 8 | , touch = require('./inputs/touch') 9 | , message = require('./inputs/message') 10 | , location = require('./inputs/location') 11 | ; 12 | 13 | function Controller (events, dom, slideshowView, options) { 14 | options = options || {}; 15 | 16 | message.register(events); 17 | location.register(events, dom, slideshowView); 18 | keyboard.register(events); 19 | mouse.register(events, options); 20 | touch.register(events, options); 21 | 22 | addApiEventListeners(events, slideshowView, options); 23 | } 24 | 25 | function addApiEventListeners(events, slideshowView, options) { 26 | events.on('pause', function(event) { 27 | keyboard.unregister(events); 28 | mouse.unregister(events); 29 | touch.unregister(events); 30 | }); 31 | 32 | events.on('resume', function(event) { 33 | keyboard.register(events); 34 | mouse.register(events, options); 35 | touch.register(events, options); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/remark/controllers/inputs/mouse.js: -------------------------------------------------------------------------------- 1 | exports.register = function (events, options) { 2 | addMouseEventListeners(events, options); 3 | }; 4 | 5 | exports.unregister = function (events) { 6 | removeMouseEventListeners(events); 7 | }; 8 | 9 | function addMouseEventListeners (events, options) { 10 | if (options.click) { 11 | events.on('click', function (event) { 12 | if (event.button === 0) { 13 | events.emit('gotoNextSlide'); 14 | } 15 | }); 16 | events.on('contextmenu', function (event) { 17 | event.preventDefault(); 18 | events.emit('gotoPreviousSlide'); 19 | }); 20 | } 21 | 22 | if (options.scroll !== false) { 23 | events.on('mousewheel', function (event) { 24 | if (event.wheelDeltaY > 0) { 25 | events.emit('gotoPreviousSlide'); 26 | } 27 | else if (event.wheelDeltaY < 0) { 28 | events.emit('gotoNextSlide'); 29 | } 30 | }); 31 | } 32 | } 33 | 34 | function removeMouseEventListeners(events) { 35 | events.removeAllListeners('click'); 36 | events.removeAllListeners('contextmenu'); 37 | events.removeAllListeners('mousewheel'); 38 | } 39 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 13 | 14 | 15 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/remark/converter_test.js: -------------------------------------------------------------------------------- 1 | var converter = require('../../src/remark/converter'); 2 | 3 | describe('Converter', function () { 4 | it('should convert empty content', function () { 5 | var content = ['']; 6 | converter.convertMarkdown(content).should.equal(''); 7 | }); 8 | 9 | it('should convert paragraph', function () { 10 | var content = ['paragraph']; 11 | converter.convertMarkdown(content).should.equal('

paragraph

'); 12 | }); 13 | 14 | it('should convert paragraph with inline content class', function () { 15 | var content = [ 16 | 'before ', 17 | { block: false, class: 'whatever', content: ['some _fancy_ content'] }, 18 | ' after' 19 | ]; 20 | converter.convertMarkdown(content).should.equal( 21 | '

before some fancy content after

'); 22 | }); 23 | 24 | it('should convert reference-style link', function () { 25 | var content = ['[link][id]'], 26 | links = { id: { href: 'url', title: 'title'} }; 27 | 28 | converter.convertMarkdown(content, links).should.equal( 29 | '

link

'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /boilerplate-local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My Awesome Presentation 5 | 6 | 18 | 19 | 20 | 40 | 42 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/remark/utils.js: -------------------------------------------------------------------------------- 1 | exports.addClass = function (element, className) { 2 | element.className = exports.getClasses(element) 3 | .concat([className]) 4 | .join(' '); 5 | }; 6 | 7 | exports.removeClass = function (element, className) { 8 | element.className = exports.getClasses(element) 9 | .filter(function (klass) { return klass !== className; }) 10 | .join(' '); 11 | }; 12 | 13 | exports.toggleClass = function (element, className) { 14 | var classes = exports.getClasses(element), 15 | index = classes.indexOf(className); 16 | 17 | if (index !== -1) { 18 | classes.splice(index, 1); 19 | } 20 | else { 21 | classes.push(className); 22 | } 23 | 24 | element.className = classes.join(' '); 25 | }; 26 | 27 | exports.getClasses = function (element) { 28 | return element.className 29 | .split(' ') 30 | .filter(function (s) { return s !== ''; }); 31 | }; 32 | 33 | exports.hasClass = function (element, className) { 34 | return exports.getClasses(element).indexOf(className) !== -1; 35 | }; 36 | 37 | exports.getPrefixedProperty = function (element, propertyName) { 38 | var capitalizedPropertName = propertyName[0].toUpperCase() + 39 | propertyName.slice(1); 40 | 41 | return element[propertyName] || element['moz' + capitalizedPropertName] || 42 | element['webkit' + capitalizedPropertName]; 43 | }; 44 | -------------------------------------------------------------------------------- /boilerplate-remote.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My Awesome Presentation 5 | 6 | 18 | 19 | 20 | 40 | 42 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/templates/boilerplate-single.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My Awesome Presentation 5 | 6 | 18 | 19 | 20 | 40 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/remark/controllers/inputs/touch.js: -------------------------------------------------------------------------------- 1 | exports.register = function (events, options) { 2 | addTouchEventListeners(events, options); 3 | }; 4 | 5 | exports.unregister = function (events) { 6 | removeTouchEventListeners(events); 7 | }; 8 | 9 | function addTouchEventListeners (events, options) { 10 | var touch 11 | , startX 12 | , endX 13 | ; 14 | 15 | if (options.touch === false) { 16 | return; 17 | } 18 | 19 | var isTap = function () { 20 | return Math.abs(startX - endX) < 10; 21 | }; 22 | 23 | var handleTap = function () { 24 | events.emit('tap', endX); 25 | }; 26 | 27 | var handleSwipe = function () { 28 | if (startX > endX) { 29 | events.emit('gotoNextSlide'); 30 | } 31 | else { 32 | events.emit('gotoPreviousSlide'); 33 | } 34 | }; 35 | 36 | events.on('touchstart', function (event) { 37 | touch = event.touches[0]; 38 | startX = touch.clientX; 39 | }); 40 | 41 | events.on('touchend', function (event) { 42 | if (event.target.nodeName.toUpperCase() === 'A') { 43 | return; 44 | } 45 | 46 | touch = event.changedTouches[0]; 47 | endX = touch.clientX; 48 | 49 | if (isTap()) { 50 | handleTap(); 51 | } 52 | else { 53 | handleSwipe(); 54 | } 55 | }); 56 | 57 | events.on('touchmove', function (event) { 58 | event.preventDefault(); 59 | }); 60 | } 61 | 62 | function removeTouchEventListeners(events) { 63 | events.removeAllListeners("touchstart"); 64 | events.removeAllListeners("touchend"); 65 | events.removeAllListeners("touchmove"); 66 | } 67 | -------------------------------------------------------------------------------- /src/remark/converter.js: -------------------------------------------------------------------------------- 1 | var marked = require('marked') 2 | , converter = module.exports = {} 3 | , element = document.createElement('div') 4 | ; 5 | 6 | marked.setOptions({ 7 | gfm: true, 8 | tables: true, 9 | breaks: false, 10 | 11 | // Without this set to true, converting something like 12 | //

*

*

will become

13 | pedantic: true, 14 | 15 | sanitize: false, 16 | smartLists: true, 17 | langPrefix: '' 18 | }); 19 | 20 | converter.convertMarkdown = function (content, links, inline) { 21 | element.innerHTML = convertMarkdown(content, links || {}, inline); 22 | element.innerHTML = element.innerHTML.replace(/

\s*<\/p>/g, ''); 23 | return element.innerHTML.replace(/\n\r?$/, ''); 24 | }; 25 | 26 | function convertMarkdown (content, links, insideContentClass) { 27 | var i, tag, markdown = '', html; 28 | 29 | for (i = 0; i < content.length; ++i) { 30 | if (typeof content[i] === 'string') { 31 | markdown += content[i]; 32 | } 33 | else { 34 | tag = content[i].block ? 'div' : 'span'; 35 | markdown += '<' + tag + ' class="' + content[i].class + '">'; 36 | markdown += convertMarkdown(content[i].content, links, true); 37 | markdown += ''; 38 | } 39 | } 40 | 41 | var tokens = marked.Lexer.lex(markdown.replace(/^\s+/, '')); 42 | tokens.links = links; 43 | html = marked.Parser.parse(tokens); 44 | 45 | if (insideContentClass) { 46 | element.innerHTML = html; 47 | if (element.children.length === 1 && element.children[0].tagName === 'P') { 48 | html = element.children[0].innerHTML; 49 | } 50 | } 51 | 52 | return html; 53 | } 54 | -------------------------------------------------------------------------------- /src/remark/components/timer/timer.js: -------------------------------------------------------------------------------- 1 | var utils = require('../../utils'); 2 | 3 | module.exports = TimerViewModel; 4 | 5 | function TimerViewModel (events, element) { 6 | var self = this; 7 | 8 | self.events = events; 9 | self.element = element; 10 | 11 | self.startTime = null; 12 | self.pauseStart = null; 13 | self.pauseLength = 0; 14 | 15 | element.innerHTML = '0:00:00'; 16 | 17 | setInterval(function() { 18 | self.updateTimer(); 19 | }, 100); 20 | 21 | events.on('start', function () { 22 | // When we do the first slide change, start the clock. 23 | self.startTime = new Date(); 24 | }); 25 | 26 | events.on('resetTimer', function () { 27 | // If we reset the timer, clear everything. 28 | self.startTime = null; 29 | self.pauseStart = null; 30 | self.pauseLength = 0; 31 | self.element.innerHTML = '0:00:00'; 32 | }); 33 | 34 | events.on('pause', function () { 35 | self.pauseStart = new Date(); 36 | }); 37 | 38 | events.on('resume', function () { 39 | self.pauseLength += new Date() - self.pauseStart; 40 | self.pauseStart = null; 41 | }); 42 | } 43 | 44 | TimerViewModel.prototype.updateTimer = function () { 45 | var self = this; 46 | 47 | if (self.startTime) { 48 | var millis; 49 | // If we're currently paused, measure elapsed time from the pauseStart. 50 | // Otherwise, use "now". 51 | if (self.pauseStart) { 52 | millis = self.pauseStart - self.startTime - self.pauseLength; 53 | } else { 54 | millis = new Date() - self.startTime - self.pauseLength; 55 | } 56 | 57 | var seconds = Math.floor(millis / 1000) % 60; 58 | var minutes = Math.floor(millis / 60000) % 60; 59 | var hours = Math.floor(millis / 3600000); 60 | 61 | self.element.innerHTML = hours + (minutes > 9 ? ':' : ':0') + minutes + (seconds > 9 ? ':' : ':0') + seconds; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/remark/controllers/inputs/keyboard.js: -------------------------------------------------------------------------------- 1 | exports.register = function (events) { 2 | addKeyboardEventListeners(events); 3 | }; 4 | 5 | exports.unregister = function (events) { 6 | removeKeyboardEventListeners(events); 7 | }; 8 | 9 | function addKeyboardEventListeners (events) { 10 | events.on('keydown', function (event) { 11 | switch (event.keyCode) { 12 | case 33: // Page up 13 | case 37: // Left 14 | case 38: // Up 15 | events.emit('gotoPreviousSlide'); 16 | break; 17 | case 32: // Space 18 | case 34: // Page down 19 | case 39: // Right 20 | case 40: // Down 21 | events.emit('gotoNextSlide'); 22 | break; 23 | case 36: // Home 24 | events.emit('gotoFirstSlide'); 25 | break; 26 | case 35: // End 27 | events.emit('gotoLastSlide'); 28 | break; 29 | case 27: // Escape 30 | events.emit('hideOverlay'); 31 | break; 32 | } 33 | }); 34 | 35 | events.on('keypress', function (event) { 36 | if (event.metaKey || event.ctrlKey) { 37 | // Bail out if meta or ctrl key was pressed 38 | return; 39 | } 40 | 41 | switch (String.fromCharCode(event.which)) { 42 | case 'j': 43 | events.emit('gotoNextSlide'); 44 | break; 45 | case 'k': 46 | events.emit('gotoPreviousSlide'); 47 | break; 48 | case 'b': 49 | events.emit('toggleBlackout'); 50 | break; 51 | case 'c': 52 | events.emit('createClone'); 53 | break; 54 | case 'p': 55 | events.emit('togglePresenterMode'); 56 | break; 57 | case 'f': 58 | events.emit('toggleFullscreen'); 59 | break; 60 | case 't': 61 | events.emit('resetTimer'); 62 | break; 63 | case 'h': 64 | case '?': 65 | events.emit('toggleHelp'); 66 | break; 67 | } 68 | }); 69 | } 70 | 71 | function removeKeyboardEventListeners(events) { 72 | events.removeAllListeners("keydown"); 73 | events.removeAllListeners("keypress"); 74 | } 75 | -------------------------------------------------------------------------------- /src/remark/scaler.js: -------------------------------------------------------------------------------- 1 | var referenceWidth = 908 2 | , referenceHeight = 681 3 | , referenceRatio = referenceWidth / referenceHeight 4 | ; 5 | 6 | module.exports = Scaler; 7 | 8 | function Scaler (events, slideshow) { 9 | var self = this; 10 | 11 | self.events = events; 12 | self.slideshow = slideshow; 13 | self.ratio = getRatio(slideshow); 14 | self.dimensions = getDimensions(self.ratio); 15 | 16 | self.events.on('propertiesChanged', function (changes) { 17 | if (changes.hasOwnProperty('ratio')) { 18 | self.ratio = getRatio(slideshow); 19 | self.dimensions = getDimensions(self.ratio); 20 | } 21 | }); 22 | } 23 | 24 | Scaler.prototype.scaleToFit = function (element, container) { 25 | var self = this 26 | , containerHeight = container.clientHeight 27 | , containerWidth = container.clientWidth 28 | , scale 29 | , scaledWidth 30 | , scaledHeight 31 | , ratio = self.ratio 32 | , dimensions = self.dimensions 33 | , direction 34 | , left 35 | , top 36 | ; 37 | 38 | if (containerWidth / ratio.width > containerHeight / ratio.height) { 39 | scale = containerHeight / dimensions.height; 40 | } 41 | else { 42 | scale = containerWidth / dimensions.width; 43 | } 44 | 45 | scaledWidth = dimensions.width * scale; 46 | scaledHeight = dimensions.height * scale; 47 | 48 | left = (containerWidth - scaledWidth) / 2; 49 | top = (containerHeight - scaledHeight) / 2; 50 | 51 | element.style['-webkit-transform'] = 'scale(' + scale + ')'; 52 | element.style.MozTransform = 'scale(' + scale + ')'; 53 | element.style.left = Math.max(left, 0) + 'px'; 54 | element.style.top = Math.max(top, 0) + 'px'; 55 | }; 56 | 57 | function getRatio (slideshow) { 58 | var ratioComponents = slideshow.getRatio().split(':') 59 | , ratio 60 | ; 61 | 62 | ratio = { 63 | width: parseInt(ratioComponents[0], 10) 64 | , height: parseInt(ratioComponents[1], 10) 65 | }; 66 | 67 | ratio.ratio = ratio.width / ratio.height; 68 | 69 | return ratio; 70 | } 71 | 72 | function getDimensions (ratio) { 73 | return { 74 | width: Math.floor(referenceWidth / referenceRatio * ratio.ratio) 75 | , height: referenceHeight 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /test/remark/api_test.js: -------------------------------------------------------------------------------- 1 | var Api = require('../../src/remark/api') 2 | , TestDom = require('../test_dom') 3 | , highlighter = require('../../src/remark/highlighter') 4 | , Slideshow = require('../../src/remark/models/slideshow') 5 | ; 6 | 7 | describe('API', function () { 8 | var api, 9 | dom; 10 | 11 | beforeEach(function () { 12 | dom = new TestDom(); 13 | api = new Api(dom); 14 | }); 15 | 16 | it('should be exposed', function () { 17 | window.should.have.property('remark'); 18 | }); 19 | 20 | it('should expose highlighter', function () { 21 | api.highlighter.should.equal(highlighter); 22 | }); 23 | 24 | it('should allow creating slideshow', function () { 25 | api.create().should.be.an.instanceOf(Slideshow); 26 | }); 27 | 28 | it('should allow creating slideshow with source directly', function () { 29 | var slides = api.create({ source: '1\n---\n2' }).getSlides(); 30 | slides.length.should.eql(2); 31 | slides[0].content.should.eql([ '1' ]); 32 | slides[1].content.should.eql([ '2' ]); 33 | }); 34 | 35 | it('should allow creating slideshow from source textarea', function () { 36 | var source = document.createElement('textarea'); 37 | source.id = 'source'; 38 | source.textContent = '3\n---\n4'; 39 | dom.getElementById = function () { return source; }; 40 | 41 | var slides = api.create().getSlides(); 42 | slides.length.should.eql(2); 43 | slides[0].content.should.eql(['3']); 44 | slides[1].content.should.eql(['4']); 45 | }); 46 | 47 | it('should allow creating slideshow from source url with linux newlines', function () { 48 | dom.XMLHttpRequest.responseText = '5\n---\n6'; 49 | var slides = api.create({ sourceUrl: 'some-file-with-linux-newlines.txt' }).getSlides(); 50 | slides.length.should.eql(2); 51 | slides[0].content.should.eql(['5']); 52 | slides[1].content.should.eql(['6']); 53 | }); 54 | 55 | it('should allow creating slideshow from source url with windows newlines', function () { 56 | dom.XMLHttpRequest.responseText = '7\r\n---\r\n8'; 57 | var slides = api.create({ sourceUrl: 'some-file-with-windows-newlines.txt' }).getSlides(); 58 | slides.length.should.eql(2); 59 | slides[0].content.should.eql(['7']); 60 | slides[1].content.should.eql(['8']); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/remark/views/notesView.js: -------------------------------------------------------------------------------- 1 | var converter = require('../converter'); 2 | 3 | module.exports = NotesView; 4 | 5 | function NotesView (events, element, slideViewsAccessor) { 6 | var self = this; 7 | 8 | self.events = events; 9 | self.element = element; 10 | self.slideViewsAccessor = slideViewsAccessor; 11 | 12 | self.configureElements(); 13 | 14 | events.on('showSlide', function (slideIndex) { 15 | self.showSlide(slideIndex); 16 | }); 17 | } 18 | 19 | NotesView.prototype.showSlide = function (slideIndex) { 20 | var self = this 21 | , slideViews = self.slideViewsAccessor() 22 | , slideView = slideViews[slideIndex] 23 | , nextSlideView = slideViews[slideIndex + 1] 24 | ; 25 | 26 | self.notesElement.innerHTML = slideView.notesElement.innerHTML; 27 | 28 | if (nextSlideView) { 29 | self.notesPreviewElement.innerHTML = nextSlideView.notesElement.innerHTML; 30 | } 31 | else { 32 | self.notesPreviewElement.innerHTML = ''; 33 | } 34 | }; 35 | 36 | NotesView.prototype.configureElements = function () { 37 | var self = this; 38 | 39 | self.notesElement = self.element.getElementsByClassName('remark-notes')[0]; 40 | self.notesPreviewElement = self.element.getElementsByClassName('remark-notes-preview')[0]; 41 | 42 | self.notesElement.addEventListener('mousewheel', function (event) { 43 | event.stopPropagation(); 44 | }); 45 | 46 | self.notesPreviewElement.addEventListener('mousewheel', function (event) { 47 | event.stopPropagation(); 48 | }); 49 | 50 | self.toolbarElement = self.element.getElementsByClassName('remark-toolbar')[0]; 51 | 52 | var commands = { 53 | increase: function () { 54 | self.notesElement.style.fontSize = (parseFloat(self.notesElement.style.fontSize) || 1) + 0.1 + 'em'; 55 | self.notesPreviewElement.style.fontsize = self.notesElement.style.fontSize; 56 | }, 57 | decrease: function () { 58 | self.notesElement.style.fontSize = (parseFloat(self.notesElement.style.fontSize) || 1) - 0.1 + 'em'; 59 | self.notesPreviewElement.style.fontsize = self.notesElement.style.fontSize; 60 | } 61 | }; 62 | 63 | self.toolbarElement.getElementsByTagName('a').forEach(function (link) { 64 | link.addEventListener('click', function (e) { 65 | var command = e.target.hash.substr(1); 66 | commands[command](); 67 | e.preventDefault(); 68 | }); 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /src/remark/api.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter 2 | , highlighter = require('./highlighter') 3 | , converter = require('./converter') 4 | , Parser = require('./parser') 5 | , Slideshow = require('./models/slideshow') 6 | , SlideshowView = require('./views/slideshowView') 7 | , DefaultController = require('./controllers/defaultController') 8 | , Dom = require('./dom') 9 | ; 10 | 11 | module.exports = Api; 12 | 13 | function Api (dom) { 14 | this.dom = dom || new Dom(); 15 | } 16 | 17 | // Expose highlighter to allow enumerating available styles and 18 | // including external language grammars 19 | Api.prototype.highlighter = highlighter; 20 | 21 | Api.prototype.convert = function (markdown) { 22 | var parser = new Parser() 23 | , content = parser.parse(markdown || '')[0].content 24 | ; 25 | 26 | return converter.convertMarkdown(content, {}, true); 27 | }; 28 | 29 | // Creates slideshow initialized from options 30 | Api.prototype.create = function (options) { 31 | var events 32 | , slideshow 33 | , slideshowView 34 | , controller 35 | ; 36 | 37 | options = applyDefaults(this.dom, options); 38 | 39 | events = new EventEmitter(); 40 | events.setMaxListeners(0); 41 | 42 | slideshow = new Slideshow(events, options); 43 | slideshowView = new SlideshowView(events, this.dom, options.container, slideshow); 44 | controller = options.controller || new DefaultController(events, this.dom, slideshowView, options.navigation); 45 | 46 | return slideshow; 47 | }; 48 | 49 | function applyDefaults (dom, options) { 50 | var sourceElement; 51 | 52 | options = options || {}; 53 | 54 | if (options.hasOwnProperty('sourceUrl')) { 55 | var req = new dom.XMLHttpRequest(); 56 | req.open('GET', options.sourceUrl, false); 57 | req.send(); 58 | options.source = req.responseText.replace(/\r\n/g, '\n'); 59 | } 60 | else if (!options.hasOwnProperty('source')) { 61 | sourceElement = dom.getElementById('source'); 62 | if (sourceElement) { 63 | options.source = unescape(sourceElement.innerHTML); 64 | sourceElement.style.display = 'none'; 65 | } 66 | } 67 | 68 | if (!(options.container instanceof window.HTMLElement)) { 69 | options.container = dom.getBodyElement(); 70 | } 71 | 72 | return options; 73 | } 74 | 75 | function unescape (source) { 76 | source = source.replace(/&[l|g]t;/g, 77 | function (match) { 78 | return match === '<' ? '<' : '>'; 79 | }); 80 | 81 | source = source.replace(/&/g, '&'); 82 | source = source.replace(/"/g, '"'); 83 | 84 | return source; 85 | } 86 | -------------------------------------------------------------------------------- /test/remark/models/slideshow_test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter 2 | , Slideshow = require('../../../src/remark/models/slideshow') 3 | , Slide = require('../../../src/remark/models/slide') 4 | ; 5 | 6 | describe('Slideshow', function () { 7 | var events 8 | , slideshow 9 | ; 10 | 11 | beforeEach(function () { 12 | events = new EventEmitter(); 13 | slideshow = new Slideshow(events); 14 | }); 15 | 16 | describe('loading from source', function () { 17 | it('should create slides', function () { 18 | slideshow.loadFromString('a\n---\nb'); 19 | slideshow.getSlides().length.should.equal(2); 20 | }); 21 | 22 | it('should replace slides', function () { 23 | slideshow.loadFromString('a\n---\nb\n---\nc'); 24 | slideshow.getSlides().length.should.equal(3); 25 | }); 26 | }); 27 | 28 | describe('continued slides', function () { 29 | it('should be created when using only two dashes', function () { 30 | slideshow.loadFromString('a\n--\nb'); 31 | 32 | slideshow.getSlides()[1].properties.should.have.property('continued', 'true'); 33 | }); 34 | }); 35 | 36 | describe('name mapping', function () { 37 | it('should map named slide', function () { 38 | slideshow.loadFromString('name: a\n---\nno name\n---\nname: b'); 39 | slideshow.getSlideByName('a').should.exist; 40 | slideshow.getSlideByName('b').should.exist; 41 | }); 42 | }); 43 | 44 | describe('templates', function () { 45 | it('should have properties inherited by referenced slide', function () { 46 | slideshow.loadFromString('name: a\na\n---\ntemplate: a\nb'); 47 | slideshow.getSlides()[1].content.should.eql(['\na', '\nb']); 48 | }); 49 | 50 | it('should have content inherited by referenced slide', function () { 51 | slideshow.loadFromString('name: a\na\n---\ntemplate: a\nb'); 52 | slideshow.getSlides()[1].content.should.eql(['\na', '\nb']); 53 | }); 54 | }); 55 | 56 | describe('layout slides', function () { 57 | it('should be default template for subsequent slides', function () { 58 | slideshow.loadFromString('layout: true\na\n---\nb'); 59 | slideshow.getSlides()[0].content.should.eql(['\na', 'b']); 60 | }); 61 | 62 | it('should not be default template for subsequent layout slide', function () { 63 | slideshow.loadFromString('layout: true\na\n---\nlayout: true\nb\n---\nc'); 64 | slideshow.getSlides()[0].content.should.eql(['\nb', 'c']); 65 | }); 66 | 67 | it('should be omitted from list of slides', function () { 68 | slideshow.loadFromString('name: a\nlayout: true\n---\nname: b'); 69 | slideshow.getSlides().length.should.equal(1); 70 | }); 71 | }); 72 | 73 | describe('events', function () { 74 | it('should emit slidesChanged event', function (done) { 75 | events.on('slidesChanged', function () { 76 | done(); 77 | }); 78 | 79 | slideshow.loadFromString('a\n---\nb'); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/remark/models/slide.js: -------------------------------------------------------------------------------- 1 | module.exports = Slide; 2 | 3 | function Slide (slideNo, slide, template) { 4 | var self = this; 5 | 6 | self.properties = slide.properties || {}; 7 | self.links = slide.links || {}; 8 | self.content = slide.content || []; 9 | self.notes = slide.notes || ''; 10 | self.number = slideNo; 11 | 12 | self.getSlideNo = function () { return slideNo; }; 13 | 14 | if (template) { 15 | inherit(self, template); 16 | } 17 | } 18 | 19 | function inherit (slide, template) { 20 | inheritProperties(slide, template); 21 | inheritContent(slide, template); 22 | inheritNotes(slide, template); 23 | } 24 | 25 | function inheritProperties (slide, template) { 26 | var property 27 | , value 28 | ; 29 | 30 | for (property in template.properties) { 31 | if (!template.properties.hasOwnProperty(property) || 32 | ignoreProperty(property)) { 33 | continue; 34 | } 35 | 36 | value = [template.properties[property]]; 37 | 38 | if (property === 'class' && slide.properties[property]) { 39 | value.push(slide.properties[property]); 40 | } 41 | 42 | if (property === 'class' || slide.properties[property] === undefined) { 43 | slide.properties[property] = value.join(', '); 44 | } 45 | } 46 | } 47 | 48 | function ignoreProperty (property) { 49 | return property === 'name' || 50 | property === 'layout'; 51 | } 52 | 53 | function inheritContent (slide, template) { 54 | var expandedVariables; 55 | 56 | slide.properties.content = slide.content.slice(); 57 | slide.content = template.content.slice(); 58 | 59 | expandedVariables = slide.expandVariables(/* contentOnly: */ true); 60 | 61 | if (expandedVariables.content === undefined) { 62 | slide.content = slide.content.concat(slide.properties.content); 63 | } 64 | 65 | delete slide.properties.content; 66 | } 67 | 68 | function inheritNotes (slide, template) { 69 | if (template.notes) { 70 | slide.notes = template.notes + '\n\n' + slide.notes; 71 | } 72 | } 73 | 74 | Slide.prototype.expandVariables = function (contentOnly, content, expandResult) { 75 | var properties = this.properties 76 | , i 77 | ; 78 | 79 | content = content !== undefined ? content : this.content; 80 | expandResult = expandResult || {}; 81 | 82 | for (i = 0; i < content.length; ++i) { 83 | if (typeof content[i] === 'string') { 84 | content[i] = content[i].replace(/(\\)?(\{\{([^\}\n]+)\}\})/g, expand); 85 | } 86 | else { 87 | this.expandVariables(contentOnly, content[i].content, expandResult); 88 | } 89 | } 90 | 91 | function expand (match, escaped, unescapedMatch, property) { 92 | var propertyName = property.trim() 93 | , propertyValue 94 | ; 95 | 96 | if (escaped) { 97 | return contentOnly ? match[0] : unescapedMatch; 98 | } 99 | 100 | if (contentOnly && propertyName !== 'content') { 101 | return match; 102 | } 103 | 104 | propertyValue = properties[propertyName]; 105 | 106 | if (propertyValue !== undefined) { 107 | expandResult[propertyName] = propertyValue; 108 | return propertyValue; 109 | } 110 | 111 | return propertyName === 'content' ? '' : unescapedMatch; 112 | } 113 | 114 | return expandResult; 115 | }; 116 | -------------------------------------------------------------------------------- /src/remark.html: -------------------------------------------------------------------------------- 1 |

2 |
3 |
4 | + 5 | - 6 | 7 |
8 |
9 |
10 |
11 |
Notes for current slide
12 |
13 |
14 |
15 |
Notes for next slide
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Paused 26 |
27 |
28 |
29 |
30 |

Help

31 |

Keyboard shortcuts

32 | 33 | 34 | 40 | 41 | 42 | 43 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 92 | 93 | 94 | 95 | 98 | 99 | 100 | 101 | 105 | 106 | 107 |
35 | , 36 | , 37 | Pg Up, 38 | k 39 | Go to previous slide
44 | , 45 | , 46 | Pg Dn, 47 | Space, 48 | j 49 | Go to next slide
54 | Home 55 | Go to first slide
60 | End 61 | Go to last slide
66 | b 67 | Toggle blackout mode
72 | f 73 | Toggle fullscreen mode
78 | c 79 | Clone slideshow
84 | p 85 | Toggle presenter mode
90 | w 91 | Pause/Resume the presentation
96 | t 97 | Restart the presentation timer
102 | ?, 103 | h 104 | Toggle this help
108 |
109 |
110 | 111 | 112 | 115 | 116 | 117 |
113 | Esc 114 | Back to slideshow
118 |
119 |
120 | -------------------------------------------------------------------------------- /src/remark/lexer.js: -------------------------------------------------------------------------------- 1 | module.exports = Lexer; 2 | 3 | var CODE = 1, 4 | CONTENT = 2, 5 | FENCES = 3, 6 | DEF = 4, 7 | DEF_HREF = 5, 8 | DEF_TITLE = 6, 9 | SEPARATOR = 7, 10 | NOTES_SEPARATOR = 8; 11 | 12 | var regexByName = { 13 | CODE: /(?:^|\n)( {4}[^\n]+\n*)+/, 14 | CONTENT: /(?:\\)?((?:\.[a-zA-Z_\-][a-zA-Z\-_0-9]*)+)\[/, 15 | FENCES: /(?:^|\n) *(`{3,}|~{3,}) *(?:\S+)? *\n(?:[\s\S]+?)\s*\3 *(?:\n+|$)/, 16 | DEF: /(?:^|\n) *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, 17 | SEPARATOR: /(?:^|\n)(---?)(?:\n|$)/, 18 | NOTES_SEPARATOR: /(?:^|\n)(\?{3})(?:\n|$)/ 19 | }; 20 | 21 | var block = replace(/CODE|CONTENT|FENCES|DEF|SEPARATOR|NOTES_SEPARATOR/, regexByName), 22 | inline = replace(/CODE|CONTENT|FENCES/, regexByName); 23 | 24 | function Lexer () { } 25 | 26 | Lexer.prototype.lex = function (src) { 27 | var tokens = lex(src, block), 28 | i; 29 | 30 | for (i = tokens.length - 2; i >= 0; i--) { 31 | if (tokens[i].type === 'text' && tokens[i+1].type === 'text') { 32 | tokens[i].text += tokens[i+1].text; 33 | tokens.splice(i+1, 1); 34 | } 35 | } 36 | 37 | return tokens; 38 | }; 39 | 40 | function lex (src, regex, tokens) { 41 | var cap, text; 42 | 43 | tokens = tokens || []; 44 | 45 | while ((cap = regex.exec(src)) !== null) { 46 | if (cap.index > 0) { 47 | tokens.push({ 48 | type: 'text', 49 | text: src.substring(0, cap.index) 50 | }); 51 | } 52 | 53 | if (cap[CODE]) { 54 | tokens.push({ 55 | type: 'code', 56 | text: cap[0] 57 | }); 58 | } 59 | else if (cap[FENCES]) { 60 | tokens.push({ 61 | type: 'fences', 62 | text: cap[0] 63 | }); 64 | } 65 | else if (cap[DEF]) { 66 | tokens.push({ 67 | type: 'def', 68 | id: cap[DEF], 69 | href: cap[DEF_HREF], 70 | title: cap[DEF_TITLE] 71 | }); 72 | } 73 | else if (cap[SEPARATOR]) { 74 | tokens.push({ 75 | type: 'separator', 76 | text: cap[SEPARATOR] 77 | }); 78 | } 79 | else if (cap[NOTES_SEPARATOR]) { 80 | tokens.push({ 81 | type: 'notes_separator', 82 | text: cap[NOTES_SEPARATOR] 83 | }); 84 | } 85 | else if (cap[CONTENT]) { 86 | text = getTextInBrackets(src, cap.index + cap[0].length); 87 | if (text !== undefined) { 88 | src = src.substring(text.length + 1); 89 | tokens.push({ 90 | type: 'content_start', 91 | classes: cap[CONTENT].substring(1).split('.'), 92 | block: text.indexOf('\n') !== -1 93 | }); 94 | lex(text, inline, tokens); 95 | tokens.push({ 96 | type: 'content_end', 97 | block: text.indexOf('\n') !== -1 98 | }); 99 | } 100 | else { 101 | tokens.push({ 102 | type: 'text', 103 | text: cap[0] 104 | }); 105 | } 106 | } 107 | 108 | src = src.substring(cap.index + cap[0].length); 109 | } 110 | 111 | if (src || (!src && tokens.length === 0)) { 112 | tokens.push({ 113 | type: 'text', 114 | text: src 115 | }); 116 | } 117 | 118 | return tokens; 119 | } 120 | 121 | function replace (regex, replacements) { 122 | return new RegExp(regex.source.replace(/\w{2,}/g, function (key) { 123 | return replacements[key].source; 124 | })); 125 | } 126 | 127 | function getTextInBrackets (src, offset) { 128 | var depth = 1, 129 | pos = offset, 130 | chr; 131 | 132 | while (depth > 0 && pos < src.length) { 133 | chr = src[pos++]; 134 | depth += (chr === '[' && 1) || (chr === ']' && -1) || 0; 135 | } 136 | 137 | if (depth === 0) { 138 | src = src.substr(offset, pos - offset - 1); 139 | return src; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/remark/models/slideshow/navigation.js: -------------------------------------------------------------------------------- 1 | module.exports = Navigation; 2 | 3 | function Navigation (events) { 4 | var self = this 5 | , currentSlideNo = 0 6 | , started = null 7 | ; 8 | 9 | self.getCurrentSlideNo = getCurrentSlideNo; 10 | self.gotoSlide = gotoSlide; 11 | self.gotoPreviousSlide = gotoPreviousSlide; 12 | self.gotoNextSlide = gotoNextSlide; 13 | self.gotoFirstSlide = gotoFirstSlide; 14 | self.gotoLastSlide = gotoLastSlide; 15 | self.pause = pause; 16 | self.resume = resume; 17 | 18 | events.on('gotoSlide', gotoSlide); 19 | events.on('gotoPreviousSlide', gotoPreviousSlide); 20 | events.on('gotoNextSlide', gotoNextSlide); 21 | events.on('gotoFirstSlide', gotoFirstSlide); 22 | events.on('gotoLastSlide', gotoLastSlide); 23 | 24 | events.on('slidesChanged', function () { 25 | if (currentSlideNo > self.getSlideCount()) { 26 | currentSlideNo = self.getSlideCount(); 27 | } 28 | }); 29 | 30 | events.on('createClone', function () { 31 | if (!self.clone || self.clone.closed) { 32 | self.clone = window.open(location.href, '_blank', 'location=no'); 33 | } 34 | else { 35 | self.clone.focus(); 36 | } 37 | }); 38 | 39 | events.on('resetTimer', function() { 40 | started = false; 41 | }); 42 | 43 | function pause () { 44 | events.emit('pause'); 45 | } 46 | 47 | function resume () { 48 | events.emit('resume'); 49 | } 50 | 51 | function getCurrentSlideNo () { 52 | return currentSlideNo; 53 | } 54 | 55 | function gotoSlide (slideNoOrName, noMessage) { 56 | var slideNo = getSlideNo(slideNoOrName) 57 | , alreadyOnSlide = slideNo === currentSlideNo 58 | , slideOutOfRange = slideNo < 1 || slideNo > self.getSlideCount() 59 | ; 60 | if (noMessage === undefined) noMessage = false; 61 | 62 | if (alreadyOnSlide || slideOutOfRange) { 63 | return; 64 | } 65 | 66 | if (currentSlideNo !== 0) { 67 | events.emit('hideSlide', currentSlideNo - 1, false); 68 | } 69 | 70 | // Use some tri-state logic here. 71 | // null = We haven't shown the first slide yet. 72 | // false = We've shown the initial slide, but we haven't progressed beyond that. 73 | // true = We've issued the first slide change command. 74 | if (started === null) { 75 | started = false; 76 | } else if (started === false) { 77 | // We've shown the initial slide previously - that means this is a 78 | // genuine move to a new slide. 79 | events.emit('start'); 80 | started = true; 81 | } 82 | 83 | events.emit('showSlide', slideNo - 1); 84 | 85 | currentSlideNo = slideNo; 86 | 87 | events.emit('slideChanged', slideNoOrName || slideNo); 88 | 89 | if (!noMessage) { 90 | if (self.clone && !self.clone.closed) { 91 | self.clone.postMessage('gotoSlide:' + currentSlideNo, '*'); 92 | } 93 | 94 | if (window.opener) { 95 | window.opener.postMessage('gotoSlide:' + currentSlideNo, '*'); 96 | } 97 | } 98 | } 99 | 100 | function gotoPreviousSlide() { 101 | self.gotoSlide(currentSlideNo - 1); 102 | } 103 | 104 | function gotoNextSlide() { 105 | self.gotoSlide(currentSlideNo + 1); 106 | } 107 | 108 | function gotoFirstSlide () { 109 | self.gotoSlide(1); 110 | } 111 | 112 | function gotoLastSlide () { 113 | self.gotoSlide(self.getSlideCount()); 114 | } 115 | 116 | function getSlideNo (slideNoOrName) { 117 | var slideNo 118 | , slide 119 | ; 120 | 121 | if (typeof slideNoOrName === 'number') { 122 | return slideNoOrName; 123 | } 124 | 125 | slideNo = parseInt(slideNoOrName, 10); 126 | if (slideNo.toString() === slideNoOrName) { 127 | return slideNo; 128 | } 129 | 130 | slide = self.getSlideByName(slideNoOrName); 131 | if (slide) { 132 | return slide.getSlideNo(); 133 | } 134 | 135 | return 1; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/remark/models/slide_test.js: -------------------------------------------------------------------------------- 1 | var Slide = require('../../../src/remark/models/slide'); 2 | 3 | describe('Slide', function () { 4 | describe('properties', function () { 5 | it('should be extracted', function () { 6 | var slide = new Slide(1, { 7 | content: [''], 8 | properties: {a: 'b', c: 'd'} 9 | }); 10 | slide.properties.should.have.property('a', 'b'); 11 | slide.properties.should.have.property('c', 'd'); 12 | slide.content.should.eql(['']); 13 | }); 14 | }); 15 | 16 | describe('inheritance', function () { 17 | it('should inherit properties, content and notes', function () { 18 | var template = new Slide(1, { 19 | content: ['Some content.'], 20 | properties: {prop1: 'val1'}, 21 | notes: 'template notes' 22 | }) 23 | , slide = new Slide(2, { 24 | content: ['More content.'], 25 | properties: {prop2: 'val2'}, 26 | notes: 'slide notes' 27 | }, template); 28 | 29 | slide.properties.should.have.property('prop1', 'val1'); 30 | slide.properties.should.have.property('prop2', 'val2'); 31 | slide.content.should.eql(['Some content.', 'More content.']); 32 | slide.notes.should.equal('template notes\n\nslide notes'); 33 | }); 34 | 35 | it('should not inherit name property', function () { 36 | var template = new Slide(1, { 37 | content: ['Some content.'], 38 | properties: {name: 'name'} 39 | }) 40 | , slide = new Slide(1, {content: ['More content.']}, template); 41 | 42 | slide.properties.should.not.have.property('name'); 43 | }); 44 | 45 | it('should not inherit layout property', function () { 46 | var template = new Slide(1, { 47 | content: ['Some content.'], 48 | properties: {layout: true} 49 | }) 50 | , slide = new Slide(1, {content: ['More content.']}, template); 51 | 52 | slide.properties.should.not.have.property('layout'); 53 | }); 54 | 55 | it('should aggregate class property value', function () { 56 | var template = new Slide(1, { 57 | content: ['Some content.'], 58 | properties: {'class': 'a'} 59 | }) 60 | , slide = new Slide(1, { 61 | content: ['More content.'], 62 | properties: {'class': 'b'} 63 | }, template); 64 | 65 | slide.properties.should.have.property('class', 'a, b'); 66 | }); 67 | 68 | it('should not expand regular properties when inheriting template', function () { 69 | var template = new Slide(1, { 70 | content: ['{{name}}'], 71 | properties: {name: 'a'} 72 | }) 73 | , slide = new Slide(1, { 74 | content: [''], 75 | properites: {name: 'b'} 76 | }, template); 77 | 78 | slide.content.should.eql(['{{name}}', '']); 79 | }); 80 | }); 81 | 82 | describe('variables', function () { 83 | it('should be expanded to matching properties', function () { 84 | var slide = new Slide(1, { 85 | content: ['prop1 = {{ prop1 }}'], 86 | properties: {prop1: 'val1'} 87 | }); 88 | 89 | slide.expandVariables(); 90 | 91 | slide.content.should.eql(['prop1 = val1']); 92 | }); 93 | 94 | it('should ignore escaped variables', function () { 95 | var slide = new Slide(1, { 96 | content: ['prop1 = \\{{ prop1 }}'], 97 | properties: {prop1: 'val1'} 98 | }); 99 | 100 | slide.expandVariables(); 101 | 102 | slide.content.should.eql(['prop1 = {{ prop1 }}']); 103 | }); 104 | 105 | it('should ignore undefined variables', function () { 106 | var slide = new Slide(1, {content: ['prop1 = {{ prop1 }}']}); 107 | 108 | slide.expandVariables(); 109 | 110 | slide.content.should.eql(['prop1 = {{ prop1 }}']); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /test/remark/lexer_test.js: -------------------------------------------------------------------------------- 1 | var Lexer = require('../../src/remark/lexer'); 2 | 3 | describe('Lexer', function () { 4 | 5 | describe('identifying tokens', function () { 6 | it('should recognize text', function () { 7 | lexer.lex('1').should.eql([ 8 | {type: 'text', text: '1'} 9 | ]); 10 | }); 11 | 12 | it('should treat empty source as empty text token', function () { 13 | lexer.lex('').should.eql([ 14 | {type: 'text', text: ''} 15 | ]); 16 | }); 17 | 18 | it('should recognize normal separator', function () { 19 | lexer.lex('\n---\n').should.eql([ 20 | {type: 'separator', text: '---'} 21 | ]); 22 | }); 23 | 24 | it('should recognize continued separators', function () { 25 | lexer.lex('\n--\n').should.eql([ 26 | {type: 'separator', text: '--'} 27 | ]); 28 | }); 29 | 30 | it('should recognize notes separator', function () { 31 | lexer.lex('\n???\n').should.eql([ 32 | {type: 'notes_separator', text: '???'} 33 | ]); 34 | }); 35 | 36 | it('should recognize code', function () { 37 | lexer.lex(' code').should.eql([ 38 | {type: 'code', text: ' code'} 39 | ]); 40 | }); 41 | 42 | it('should recognize fences', function () { 43 | lexer.lex('```\ncode```').should.eql([ 44 | {type: 'fences', text: '```\ncode```'} 45 | ]); 46 | }); 47 | 48 | it('should recognize content class', function () { 49 | lexer.lex('.classA[content]').should.eql([ 50 | {type: 'content_start', classes: ['classA'], block: false}, 51 | {type: 'text', text: 'content'}, 52 | {type: 'content_end', block: false} 53 | ]); 54 | }); 55 | 56 | it('should recignize multiple content classes', function () { 57 | lexer.lex('.c1.c2[content]').should.eql([ 58 | {type: 'content_start', classes: ['c1', 'c2'], block: false}, 59 | {type: 'text', text: 'content'}, 60 | {type: 'content_end', block: false} 61 | ]); 62 | }); 63 | 64 | it('should treat unclosed content class as text', function () { 65 | lexer.lex('text .class[content').should.eql([ 66 | {type: 'text', text: 'text .class[content'} 67 | ]); 68 | }); 69 | 70 | it('should leave separator inside fences as-is', function () { 71 | lexer.lex('```\n---\n```').should.eql([ 72 | {type: 'fences', text: '```\n---\n```'} 73 | ]); 74 | }); 75 | 76 | it('should leave separator inside content class as-is', function () { 77 | lexer.lex('.class[\n---\n]').should.eql([ 78 | {type: 'content_start', classes: ['class'], block: true}, 79 | {type: 'text', text: '\n---\n'}, 80 | {type: 'content_end', block: true} 81 | ]); 82 | }); 83 | 84 | it('should leave content class inside code as-is', function () { 85 | lexer.lex(' .class[x]').should.eql([ 86 | {type: 'code', text: ' .class[x]'} 87 | ]); 88 | }); 89 | 90 | it('should leave content class inside fences as-is', function () { 91 | lexer.lex('```\n.class[x]\n```').should.eql([ 92 | {type: 'fences', text: '```\n.class[x]\n```'} 93 | ]); 94 | }); 95 | 96 | it('should lex content classes recursively', function () { 97 | lexer.lex('.c1[.c2[x]]').should.eql([ 98 | {type: 'content_start', classes: ['c1'], block: false}, 99 | {type: 'content_start', classes: ['c2'], block: false}, 100 | {type: 'text', text: 'x'}, 101 | {type: 'content_end', block: false}, 102 | {type: 'content_end', block: false} 103 | ]); 104 | }); 105 | 106 | it('should recognize link definition', function () { 107 | lexer.lex('[id]: http://url.com "website"').should.eql([ 108 | { 109 | type: 'def', 110 | id: 'id', 111 | href: 'http://url.com', 112 | title: 'website' 113 | } 114 | ]); 115 | }); 116 | }); 117 | 118 | var lexer; 119 | 120 | beforeEach(function () { 121 | lexer = new Lexer(); 122 | }); 123 | 124 | }); 125 | -------------------------------------------------------------------------------- /test/remark/components/timer_test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter 2 | , Timer = require('components/timer') 3 | ; 4 | 5 | describe('Timer', function () { 6 | var events 7 | , element 8 | , timer 9 | ; 10 | 11 | beforeEach(function () { 12 | events = new EventEmitter(); 13 | element = document.createElement('div'); 14 | }); 15 | 16 | describe('timer updates', function () { 17 | beforeEach(function () { 18 | timer = new Timer(events, element); 19 | }); 20 | 21 | it('should do nothing if the timer has not started', function () { 22 | timer.element.innerHTML.should.equal('0:00:00'); 23 | }); 24 | 25 | it('should show progress time if the slideshow has started', function () { 26 | // Force a specific start time and update 27 | timer.startTime = new Date() - (2*3600000 + 34 * 60000 + 56 * 1000); 28 | timer.updateTimer(); 29 | // Timer output should match forced time 30 | timer.element.innerHTML.should.equal('2:34:56'); 31 | }); 32 | 33 | it('should compensate for a pause in progress', function () { 34 | // Force a specific start time and update, including an in-progress pause 35 | timer.startTime = new Date() - (2*3600000 + 34 * 60000 + 56 * 1000); 36 | timer.pauseStart = new Date() - (1*3600000 + 23 * 60000 + 45 * 1000); 37 | timer.updateTimer(); 38 | // Timer output should match forced time 39 | timer.element.innerHTML.should.equal('1:11:11'); 40 | }); 41 | 42 | it('should compensate for paused time', function () { 43 | // Force a specific start time and update, including a recorded pause 44 | timer.startTime = new Date() - (2*3600000 + 34 * 60000 + 56 * 1000); 45 | timer.pauseLength = (5 * 60000 + 6 * 1000); 46 | timer.updateTimer(); 47 | // Timer output should match forced time 48 | timer.element.innerHTML.should.equal('2:29:50'); 49 | }); 50 | 51 | 52 | it('should compensate for a pause in progress in addition to previous pauses', function () { 53 | // Force a specific start time and update, including a recorded pause 54 | // and an in-progress pause 55 | timer.startTime = new Date() - (2*3600000 + 34 * 60000 + 56 * 1000); 56 | timer.pauseLength = (5 * 60000 + 6 * 1000); 57 | timer.pauseStart = new Date() - (1*3600000 + 23 * 60000 + 45 * 1000); 58 | timer.updateTimer(); 59 | // Timer output should match forced time 60 | timer.element.innerHTML.should.equal('1:06:05'); 61 | }); 62 | 63 | }); 64 | 65 | describe('timer events', function () { 66 | beforeEach(function () { 67 | timer = new Timer(events, element); 68 | }); 69 | 70 | it('should respond to a start event', function () { 71 | events.emit('start'); 72 | timer.startTime.should.not.equal(null); 73 | }); 74 | 75 | it('should reset on demand', function () { 76 | timer.startTime = new Date() - (2*3600000 + 34 * 60000 + 56 * 1000); 77 | events.emit('resetTimer'); 78 | timer.element.innerHTML.should.equal('0:00:00'); 79 | // BDD seems to make this really easy test impossible... 80 | // timer.startTime.should.equal(null); 81 | // timer.pauseStart.should.equal(null); 82 | timer.pauseLength.should.equal(0); 83 | }); 84 | 85 | it('should track pause start end time', function () { 86 | timer.startTime = new Date() - (2*3600000 + 34 * 60000 + 56 * 1000); 87 | 88 | events.emit('pause'); 89 | timer.pauseStart.should.not.equal(null); 90 | timer.pauseLength.should.equal(0); 91 | }); 92 | 93 | it('should accumulate pause duration at pause end', function () { 94 | timer.startTime = new Date() - (2*3600000 + 34 * 60000 + 56 * 1000); 95 | timer.pauseStart = new Date() - (12 * 1000); 96 | timer.pauseLength = 100000; 97 | 98 | events.emit('resume'); 99 | // BDD seems to make this really easy test impossible... 100 | //timer.pauseStart.should.equal(null); 101 | // Microsecond accuracy is a possible problem here, so 102 | // allow a 5 microsecond window just in case. 103 | timer.pauseLength.should.be.approximately(112000, 5); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remark 2 | 3 | [![](https://api.travis-ci.org/gnab/remark.svg?branch=master)](https://travis-ci.org/gnab/remark) 4 | 5 | A simple, in-browser, markdown-driven slideshow tool targeted at people who know their way around HTML and CSS, featuring: 6 | 7 | - Markdown formatting, with smart extensions 8 | - Presenter mode, with cloned slideshow view 9 | - Syntax highlighting, supporting a range of languages 10 | - Slide scaling, thus similar appearance on all devices / resolutions 11 | - Touch support for smart phones and pads, i.e. swipe to navigate slides 12 | 13 | Check out [this remark slideshow](http://gnab.github.com/remark) for a brief introduction. 14 | 15 | ### Getting Started 16 | 17 | It takes only a few, simple steps to get up and running with remark: 18 | 19 | 1. Create a HTML file to contain your slideshow (see below) 20 | 2. Open the HTML file in a decent browser 21 | 3. Edit the Markdown and/or CSS styles as needed, save and refresh! 22 | 23 | Below is a boilerplate HTML file to get you started: 24 | 25 | ```html 26 | 27 | 28 | 29 | Title 30 | 31 | 43 | 44 | 45 | 64 | 66 | 69 | 70 | 71 | ``` 72 | 73 | ### Moving On 74 | 75 | For more information on using remark, please check out the [wiki](http://github.com/gnab/remark/wiki) pages. 76 | 77 | ### Real-world remark slideshows 78 | 79 | On using remark: 80 | 81 | - [The Official remark Slideshow](http://gnab.github.com/remark) 82 | - [Coloured Terminal Listings in remark](http://joshbode.github.com/remark/ansi.html) by [joshbode](https://github.com/joshbode) 83 | 84 | Other interesting stuff: 85 | 86 | - [gnab.github.com/editorjs](http://gnab.github.com/editorjs) 87 | - [judoole.github.com/GroovyBDD](http://judoole.github.com/GroovyBDD) 88 | - [kjbekkelund.github.com/nith-coffeescript](http://kjbekkelund.github.com/nith-coffeescript) 89 | - [kjbekkelund.github.com/js-architecture-backbone](http://kjbekkelund.github.com/js-architecture-backbone) 90 | - [bekkopen.github.com/infrastruktur-som-kode](http://bekkopen.github.com/infrastruktur-som-kode) 91 | - [ivarconr.github.com/Test-Driven-Web-Development/slides](http://ivarconr.github.com/Test-Driven-Web-Development/slides) 92 | - [havard.github.com/node.js-intro-norwegian](http://havard.github.com/node.js-intro-norwegian) 93 | - [mobmad.github.com/js-tdd-erfaringer](http://mobmad.github.com/js-tdd-erfaringer) 94 | - [torgeir.github.com/busterjs-lightning-talk](http://torgeir.github.com/busterjs-lightning-talk) 95 | - [roberto.github.com/ruby-sinform-2012](http://roberto.github.com/ruby-sinform-2012) 96 | - [http://asmeurer.github.io/python3-presentation/slides.html](http://asmeurer.github.io/python3-presentation/slides.html) 97 | 98 | ### Other systems integrating with remark 99 | 100 | - [http://markdowner.com](http://markdowner.com) 101 | - [http://remarks.sinaapp.com](http://remarks.sinaapp.com/) 102 | 103 | ### Credits 104 | 105 | - [torgeir](http://github.com/torgeir), for invaluable advice and feedback. 106 | - [kjbekkelund](https://github.com/kjbekkelund), for numerous pull requests. 107 | - [gureckis](https://github.com/gureckis), for several pull requests. 108 | - [freakboy3742](https://github.com/freakboy3742), for several pull requests. 109 | 110 | ### License 111 | 112 | remark is licensed under the MIT license. See LICENCE for further 113 | details. 114 | -------------------------------------------------------------------------------- /test/remark/views/slideView_test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter 2 | , Slide = require('../../../src/remark/models/slide') 3 | , SlideView = require('../../../src/remark/views/slideView') 4 | , utils = require('../../../src/remark/utils') 5 | ; 6 | 7 | describe('SlideView', function () { 8 | var slideshow = { 9 | getHighlightStyle: function () { return 'default'; } 10 | , getSlides: function () { return []; } 11 | , getLinks: function () { return {}; } 12 | , getHighlightLanguage: function () { } 13 | } 14 | , scaler = { 15 | dimensions: {width: 10, height: 10} 16 | } 17 | ; 18 | 19 | describe('background', function () { 20 | it('should be set from background slide property', function () { 21 | var slide = new Slide(1, { 22 | source: '', 23 | properties: {'background-image': 'url(image.jpg)'} 24 | }) 25 | , slideView = new SlideView(new EventEmitter(), slideshow, scaler, slide) 26 | ; 27 | 28 | slideView.contentElement.style.backgroundImage.should.match(/^url\(.*image\.jpg\)$/); 29 | }); 30 | }); 31 | 32 | describe('classes', function () { 33 | it('should contain "content" class by default', function () { 34 | var slide = new Slide(1, {source: ''}) 35 | , slideView = new SlideView(new EventEmitter(), slideshow, scaler, slide) 36 | , classes = utils.getClasses(slideView.contentElement) 37 | ; 38 | 39 | classes.should.include('remark-slide-content'); 40 | }); 41 | 42 | it('should contain additional classes from slide properties', function () { 43 | var slide = new Slide(1, { 44 | source: '', 45 | properties: {'class': 'middle, center'} 46 | }) 47 | , slideView = new SlideView(new EventEmitter(), slideshow, scaler, slide) 48 | , classes = utils.getClasses(slideView.contentElement) 49 | ; 50 | 51 | classes.should.include('remark-slide-content'); 52 | classes.should.include('middle'); 53 | classes.should.include('center'); 54 | }); 55 | }); 56 | 57 | describe('empty paragraph removal', function () { 58 | it('should have empty paragraphs removed', function () { 59 | var slide = new Slide(1, {source: '<p> </p>'}) 60 | , slideView = new SlideView(new EventEmitter(), slideshow, scaler, slide); 61 | 62 | slideView.contentElement.innerHTML.should.not.include('

'); 63 | }); 64 | }); 65 | 66 | describe('show slide', function () { 67 | it('should set the slide visible', function () { 68 | var slide = new Slide(1, {source: ''}) 69 | , slideView = new SlideView(new EventEmitter(), slideshow, scaler, slide) 70 | ; 71 | 72 | slideView.show(); 73 | 74 | var classes = utils.getClasses(slideView.containerElement); 75 | classes.should.include('remark-visible'); 76 | classes.should.not.include('remark-fading'); 77 | }); 78 | 79 | it('should remove any fading element', function () { 80 | var slide = new Slide(1, {source: ''}) 81 | , slideView = new SlideView(new EventEmitter(), slideshow, scaler, slide) 82 | ; 83 | utils.addClass(slideView.containerElement, 'remark-fading'); 84 | 85 | slideView.show(); 86 | 87 | var classes = utils.getClasses(slideView.containerElement); 88 | classes.should.include('remark-visible'); 89 | classes.should.not.include('remark-fading'); 90 | }); 91 | }); 92 | 93 | describe('hide slide', function () { 94 | it('should mark the slide as fading', function () { 95 | var slide = new Slide(1, {source: ''}) 96 | , slideView = new SlideView(new EventEmitter(), slideshow, scaler, slide) 97 | ; 98 | utils.addClass(slideView.containerElement, 'remark-visible'); 99 | 100 | slideView.hide(); 101 | 102 | var classes = utils.getClasses(slideView.containerElement); 103 | classes.should.not.include('remark-visible'); 104 | classes.should.include('remark-fading'); 105 | }); 106 | }); 107 | 108 | describe('code line highlighting', function () { 109 | it('should add class to prefixed lines', function () { 110 | var slide = new Slide(1, { content: ['```\nline 1\n* line 2\nline 3\n```'] }) 111 | , slideView = new SlideView(new EventEmitter(), slideshow, scaler, slide) 112 | ; 113 | 114 | var lines = slideView.element.getElementsByClassName('remark-code-line-highlighted'); 115 | 116 | lines.length.should.equal(1); 117 | lines[0].innerHTML.should.equal(' line 2'); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/remark/models/slideshow.js: -------------------------------------------------------------------------------- 1 | var Navigation = require('./slideshow/navigation') 2 | , Events = require('./slideshow/events') 3 | , utils = require('../utils') 4 | , Slide = require('./slide') 5 | , Parser = require('../parser') 6 | ; 7 | 8 | module.exports = Slideshow; 9 | 10 | function Slideshow (events, options) { 11 | var self = this 12 | , slides = [] 13 | , links = {} 14 | ; 15 | 16 | options = options || {}; 17 | 18 | // Extend slideshow functionality 19 | Events.call(self, events); 20 | Navigation.call(self, events); 21 | 22 | self.loadFromString = loadFromString; 23 | self.update = update; 24 | self.getLinks = getLinks; 25 | self.getSlides = getSlides; 26 | self.getSlideCount = getSlideCount; 27 | self.getSlideByName = getSlideByName; 28 | 29 | self.togglePresenterMode = togglePresenterMode; 30 | self.toggleHelp = toggleHelp; 31 | self.toggleBlackout = toggleBlackout; 32 | self.toggleFullscreen = toggleFullscreen; 33 | self.createClone = createClone; 34 | 35 | self.resetTimer = resetTimer; 36 | 37 | self.getRatio = getOrDefault('ratio', '4:3'); 38 | self.getHighlightStyle = getOrDefault('highlightStyle', 'default'); 39 | self.getHighlightLanguage = getOrDefault('highlightLanguage', ''); 40 | 41 | loadFromString(options.source); 42 | 43 | events.on('toggleBlackout', function () { 44 | if (self.clone && !self.clone.closed) { 45 | self.clone.postMessage('toggleBlackout', '*'); 46 | } 47 | }); 48 | 49 | function loadFromString (source) { 50 | source = source || ''; 51 | 52 | slides = createSlides(source); 53 | expandVariables(slides); 54 | 55 | links = {}; 56 | slides.forEach(function (slide) { 57 | for (var id in slide.links) { 58 | if (slide.links.hasOwnProperty(id)) { 59 | links[id] = slide.links[id]; 60 | } 61 | } 62 | }); 63 | 64 | events.emit('slidesChanged'); 65 | } 66 | 67 | function update () { 68 | events.emit('resize'); 69 | } 70 | 71 | function getLinks () { 72 | return links; 73 | } 74 | 75 | function getSlides () { 76 | return slides.map(function (slide) { return slide; }); 77 | } 78 | 79 | function getSlideCount () { 80 | return slides.length; 81 | } 82 | 83 | function getSlideByName (name) { 84 | return slides.byName[name]; 85 | } 86 | 87 | function togglePresenterMode () { 88 | events.emit('togglePresenterMode'); 89 | } 90 | 91 | function toggleHelp () { 92 | events.emit('toggleHelp'); 93 | } 94 | 95 | function toggleBlackout () { 96 | events.emit('toggleBlackout'); 97 | } 98 | 99 | function toggleFullscreen () { 100 | events.emit('toggleFullscreen'); 101 | } 102 | 103 | function createClone () { 104 | events.emit('createClone'); 105 | } 106 | 107 | function resetTimer () { 108 | events.emit('resetTimer'); 109 | } 110 | 111 | function getOrDefault (key, defaultValue) { 112 | return function () { 113 | if (options[key] === undefined) { 114 | return defaultValue; 115 | } 116 | 117 | return options[key]; 118 | }; 119 | } 120 | } 121 | 122 | function createSlides (slideshowSource) { 123 | var parser = new Parser() 124 | , parsedSlides = parser.parse(slideshowSource) 125 | , slides = [] 126 | , byName = {} 127 | , layoutSlide 128 | ; 129 | 130 | slides.byName = {}; 131 | 132 | parsedSlides.forEach(function (slide, i) { 133 | var template, slideViewModel; 134 | 135 | if (slide.properties.continued === 'true' && i > 0) { 136 | template = slides[slides.length - 1]; 137 | } 138 | else if (byName[slide.properties.template]) { 139 | template = byName[slide.properties.template]; 140 | } 141 | else if (slide.properties.layout === 'false') { 142 | layoutSlide = undefined; 143 | } 144 | else if (layoutSlide && slide.properties.layout !== 'true') { 145 | template = layoutSlide; 146 | } 147 | 148 | slideViewModel = new Slide(slides.length + 1, slide, template); 149 | 150 | if (slide.properties.layout === 'true') { 151 | layoutSlide = slideViewModel; 152 | } 153 | 154 | if (slide.properties.name) { 155 | byName[slide.properties.name] = slideViewModel; 156 | } 157 | 158 | if (slide.properties.layout !== 'true') { 159 | slides.push(slideViewModel); 160 | if (slide.properties.name) { 161 | slides.byName[slide.properties.name] = slideViewModel; 162 | } 163 | } 164 | }); 165 | 166 | return slides; 167 | } 168 | 169 | function expandVariables (slides) { 170 | slides.forEach(function (slide) { 171 | slide.expandVariables(); 172 | }); 173 | } 174 | -------------------------------------------------------------------------------- /test/remark/controllers/defaultController_test.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon') 2 | , EventEmitter = require('events').EventEmitter 3 | , TestDom = require('../../test_dom') 4 | , Controller = require('../../../src/remark/controllers/defaultController') 5 | ; 6 | 7 | describe('Controller', function () { 8 | describe('initial navigation', function () { 9 | it('should naviate to first slide when slideshow is embedded ', function () { 10 | createController({embedded: true}); 11 | 12 | events.emit.should.be.calledWithExactly('gotoSlide', 1); 13 | }); 14 | 15 | it('should naviate by hash when slideshow is not embedded', function () { 16 | dom.getLocationHash = function () { return '#2'; }; 17 | 18 | createController({embedded: false}); 19 | 20 | events.emit.should.be.calledWithExactly('gotoSlide', '2'); 21 | }); 22 | }); 23 | 24 | describe('hash change', function () { 25 | it('should not navigate by hash when slideshow is embedded', function () { 26 | createController({embedded: true}); 27 | 28 | dom.getLocationHash = function () { return '#3'; }; 29 | events.emit('hashchange'); 30 | 31 | events.emit.should.not.be.calledWithExactly('gotoSlide', '3'); 32 | }); 33 | 34 | it('should navigate by hash when slideshow is not embedded', function () { 35 | createController({embedded: false}); 36 | 37 | dom.getLocationHash = function () { return '#3'; }; 38 | events.emit('hashchange'); 39 | 40 | events.emit.should.be.calledWithExactly('gotoSlide', '3'); 41 | }); 42 | }); 43 | 44 | describe('keyboard navigation', function () { 45 | it('should navigate to previous slide when pressing page up', function () { 46 | events.emit('keydown', {keyCode: 33}); 47 | 48 | events.emit.should.be.calledWithExactly('gotoPreviousSlide'); 49 | }); 50 | 51 | it('should navigate to previous slide when pressing arrow left', function () { 52 | events.emit('keydown', {keyCode: 37}); 53 | 54 | events.emit.should.be.calledWithExactly('gotoPreviousSlide'); 55 | }); 56 | 57 | it('should navigate to previous slide when pressing arrow up', function () { 58 | events.emit('keydown', {keyCode: 38}); 59 | 60 | events.emit.should.be.calledWithExactly('gotoPreviousSlide'); 61 | }); 62 | 63 | it('should navigate to next slide when pressing space', function () { 64 | events.emit('keydown', {keyCode: 32}); 65 | 66 | events.emit.should.be.calledWithExactly('gotoNextSlide'); 67 | }); 68 | 69 | it('should navigate to next slide when pressing page down', function () { 70 | events.emit('keydown', {keyCode: 34}); 71 | 72 | events.emit.should.be.calledWithExactly('gotoNextSlide'); 73 | }); 74 | 75 | it('should navigate to next slide when pressing arrow right', function () { 76 | events.emit('keydown', {keyCode: 39}); 77 | 78 | events.emit.should.be.calledWithExactly('gotoNextSlide'); 79 | }); 80 | 81 | it('should navigate to next slide when pressing arrow down', function () { 82 | events.emit('keydown', {keyCode: 39}); 83 | 84 | events.emit.should.be.calledWithExactly('gotoNextSlide'); 85 | }); 86 | 87 | it('should navigate to first slide when pressing home', function () { 88 | events.emit('keydown', {keyCode: 36}); 89 | 90 | events.emit.should.be.calledWithExactly('gotoFirstSlide'); 91 | }); 92 | 93 | it('should navigate to last slide when pressing end', function () { 94 | events.emit('keydown', {keyCode: 35}); 95 | 96 | events.emit.should.be.calledWithExactly('gotoLastSlide'); 97 | }); 98 | 99 | beforeEach(function () { 100 | createController(); 101 | }); 102 | }); 103 | 104 | describe('commands', function () { 105 | it('should toggle blackout mode when pressing "b"', function () { 106 | events.emit('keypress', {which: 98}); 107 | events.emit.should.be.calledWithExactly('toggleBlackout'); 108 | }); 109 | 110 | beforeEach(function () { 111 | createController(); 112 | }); 113 | }); 114 | 115 | describe('custom controller', function () { 116 | it('should do nothing when pressing page up', function () { 117 | events.emit('keydown', {keyCode: 33}); 118 | 119 | events.emit.should.not.be.calledWithExactly('gotoPreviousSlide'); 120 | }); 121 | 122 | beforeEach(function () { 123 | controller = function() {}; 124 | }); 125 | }); 126 | 127 | var events 128 | , dom 129 | , controller 130 | ; 131 | 132 | function createController (options) { 133 | options = options || {embedded: false}; 134 | 135 | controller = new Controller(events, dom, { 136 | isEmbedded: function () { return options.embedded; } 137 | }); 138 | } 139 | 140 | beforeEach(function () { 141 | events = new EventEmitter(); 142 | sinon.spy(events, 'emit'); 143 | 144 | dom = new TestDom(); 145 | }); 146 | 147 | afterEach(function () { 148 | events.emit.restore(); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /make.js: -------------------------------------------------------------------------------- 1 | require('shelljs/make'); 2 | require('shelljs/global'); 3 | 4 | // Targets 5 | 6 | target.all = function () { 7 | target.test(); 8 | target.minify(); 9 | target.boilerplate(); 10 | }; 11 | 12 | target.highlighter = function () { 13 | console.log('Bundling highlighter...'); 14 | 15 | rm('-rf', 'vendor/highlight.js'); 16 | mkdir('-p', 'vendor'); 17 | pushd('vendor'); 18 | exec('git clone https://github.com/isagalaev/highlight.js.git'); 19 | pushd('highlight.js'); 20 | exec('git checkout tags/8.0'); 21 | popd(); 22 | popd(); 23 | 24 | bundleHighlighter('src/remark/highlighter.js'); 25 | }; 26 | 27 | target.test = function () { 28 | target['lint'](); 29 | target['bundle'](); 30 | target['test-bundle'](); 31 | 32 | console.log('Running tests...'); 33 | run('mocha-phantomjs test/runner.html'); 34 | }; 35 | 36 | target.lint = function () { 37 | console.log('Linting...'); 38 | run('jshint src', {silent: true}); 39 | }; 40 | 41 | target.bundle = function () { 42 | console.log('Bundling...'); 43 | bundleResources('src/remark/resources.js'); 44 | 45 | run('browserify ' + components() + ' src/remark.js', 46 | {silent: true}).output.to('out/remark.js'); 47 | }; 48 | 49 | function components () { 50 | var componentsPath = './src/remark/components'; 51 | 52 | return ls(componentsPath) 53 | .map(function (component) { 54 | return '-r ' + componentsPath + '/' + component + '/' + component + 55 | '.js:' + 'components/' + component; 56 | }) 57 | .join(' '); 58 | } 59 | 60 | target['test-bundle'] = function () { 61 | console.log('Bundling tests...'); 62 | 63 | [ 64 | "require('should');", 65 | "require('sinon');" 66 | ] 67 | .concat(find('./test') 68 | .filter(function(file) { return file.match(/\.js$/); }) 69 | .map(function (file) { return "require('./" + file + "');" }) 70 | ) 71 | .join('\n') 72 | .to('_tests.js'); 73 | 74 | run('browserify ' + components() + ' _tests.js', 75 | {silent: true}).output.to('out/tests.js'); 76 | rm('_tests.js'); 77 | }; 78 | 79 | target.boilerplate = function () { 80 | console.log('Generating boilerplate...'); 81 | generateBoilerplateSingle("boilerplate-single.html"); 82 | }; 83 | 84 | target.minify = function () { 85 | console.log('Minifying...'); 86 | run('uglifyjs out/remark.js', {silent: true}).output.to('out/remark.min.js'); 87 | }; 88 | 89 | // Helper functions 90 | 91 | var path = require('path') 92 | , config = require('./package.json').config 93 | , ignoredStyles = ['brown_paper', 'school_book', 'pojoaque'] 94 | ; 95 | 96 | function bundleResources (target) { 97 | var resources = { 98 | DOCUMENT_STYLES: JSON.stringify( 99 | less('src/remark.less')) 100 | , CONTAINER_LAYOUT: JSON.stringify( 101 | cat('src/remark.html')) 102 | }; 103 | 104 | cat('src/templates/resources.js.template') 105 | .replace(/%(\w+)%/g, function (match, key) { 106 | return resources[key]; 107 | }) 108 | .to(target); 109 | } 110 | 111 | function bundleHighlighter (target) { 112 | var highlightjs = 'vendor/highlight.js/src/' 113 | , resources = { 114 | HIGHLIGHTER_STYLES: JSON.stringify( 115 | ls(highlightjs + 'styles/*.css').reduce(mapStyle, {})) 116 | , HIGHLIGHTER_ENGINE: 117 | cat(highlightjs + 'highlight.js') 118 | , HIGHLIGHTER_LANGUAGES: 119 | config.highlighter.languages.map(function (language) { 120 | return '{name:"' + language + '",create:' + 121 | cat(highlightjs + 'languages/' + language + '.js') + '}'; 122 | }).join(',') 123 | }; 124 | 125 | cat('src/templates/highlighter.js.template') 126 | .replace(/%(\w+)%/g, function (match, key) { 127 | return resources[key]; 128 | }) 129 | .to(target); 130 | } 131 | 132 | function generateBoilerplateSingle(target) { 133 | var resources = { 134 | REMARK_MINJS: escape(cat('out/remark.min.js') 135 | // highlighter has a ending script tag as a string literal, and 136 | // that causes early termination of escaped script. Split that literal. 137 | .replace('""', '""')) 138 | }; 139 | 140 | cat('src/templates/boilerplate-single.html.template') 141 | .replace(/%(\w+)%/g, function (match, key) { 142 | return resources[key]; 143 | }) 144 | .to(target); 145 | } 146 | 147 | function mapStyle (map, file) { 148 | var key = path.basename(file, path.extname(file)) 149 | , tmpfile = path.join(tempdir(), 'remark.tmp') 150 | ; 151 | 152 | if (ignoredStyles.indexOf(key) === -1) { 153 | ('.hljs-' + key + ' {\n' + cat(file) + '\n}').to(tmpfile); 154 | map[key] = less(tmpfile); 155 | rm(tmpfile); 156 | } 157 | 158 | return map; 159 | } 160 | 161 | function less (file) { 162 | return run('lessc -x ' + file, {silent: true}).output.replace(/\n/g, ''); 163 | } 164 | 165 | function run (command, options) { 166 | var result = exec(pwd() + '/node_modules/.bin/' + command, options); 167 | 168 | if (result.code !== 0) { 169 | if (!options || options.silent) { 170 | console.error(result.output); 171 | } 172 | exit(1); 173 | } 174 | 175 | return result; 176 | } 177 | -------------------------------------------------------------------------------- /src/remark/parser.js: -------------------------------------------------------------------------------- 1 | var Lexer = require('./lexer'); 2 | 3 | module.exports = Parser; 4 | 5 | function Parser () { } 6 | 7 | /* 8 | * Parses source string into list of slides. 9 | * 10 | * Output format: 11 | * 12 | * [ 13 | * // Per slide 14 | * { 15 | * // Properties 16 | * properties: { 17 | * name: 'value' 18 | * }, 19 | * // Notes (optional, same format as content list) 20 | * notes: [...], 21 | * // Link definitions 22 | * links: { 23 | * id: { href: 'url', title: 'optional title' }, 24 | * ... 25 | * ], 26 | * content: [ 27 | * // Any content but content classes are represented as strings 28 | * 'plain text ', 29 | * // Content classes are represented as objects 30 | * { block: false, class: 'the-class', content: [...] }, 31 | * { block: true, class: 'the-class', content: [...] }, 32 | * ... 33 | * ] 34 | * }, 35 | * ... 36 | * ] 37 | */ 38 | Parser.prototype.parse = function (src) { 39 | var lexer = new Lexer(), 40 | tokens = lexer.lex(cleanInput(src)), 41 | slides = [], 42 | 43 | // The last item on the stack contains the current slide or 44 | // content class we're currently appending content to. 45 | stack = [createSlide()]; 46 | 47 | tokens.forEach(function (token) { 48 | switch (token.type) { 49 | case 'text': 50 | case 'code': 51 | case 'fences': 52 | // Text, code and fenced code tokens are appended to their 53 | // respective parents as string literals, and are only included 54 | // in the parse process in order to reason about structure 55 | // (like ignoring a slide separator inside fenced code). 56 | appendTo(stack[stack.length - 1], token.text); 57 | break; 58 | case 'def': 59 | // Link definition 60 | stack[0].links[token.id] = { 61 | href: token.href, 62 | title: token.title 63 | }; 64 | break; 65 | case 'content_start': 66 | // Entering content class, so create stack entry for appending 67 | // upcoming content to. 68 | // 69 | // Lexer handles open/close bracket balance, so there's no need 70 | // to worry about there being a matching closing bracket. 71 | stack.push(createContentClass(token)); 72 | break; 73 | case 'content_end': 74 | // Exiting content class, so remove entry from stack and 75 | // append to previous item (outer content class or slide). 76 | appendTo(stack[stack.length - 2], stack[stack.length - 1]); 77 | stack.pop(); 78 | break; 79 | case 'separator': 80 | // Slide separator (--- or --), so add current slide to list of 81 | // slides and re-initialize stack with new, blank slide. 82 | slides.push(stack[0]); 83 | stack = [createSlide()]; 84 | // Tag the new slide as a continued slide if the separator 85 | // used was -- instead of --- (2 vs. 3 dashes). 86 | stack[0].properties.continued = (token.text === '--').toString(); 87 | break; 88 | case 'notes_separator': 89 | // Notes separator (???), so create empty content list on slide 90 | // in which all remaining slide content will be put. 91 | stack[0].notes = []; 92 | break; 93 | } 94 | }); 95 | 96 | // Push current slide to list of slides. 97 | slides.push(stack[0]); 98 | 99 | slides.forEach(function (slide) { 100 | slide.content[0] = extractProperties(slide.content[0] || '', slide.properties); 101 | }); 102 | 103 | return slides; 104 | }; 105 | 106 | function createSlide () { 107 | return { 108 | content: [], 109 | properties: { 110 | continued: 'false' 111 | }, 112 | links: {} 113 | }; 114 | } 115 | 116 | function createContentClass (token) { 117 | return { 118 | class: token.classes.join(' '), 119 | block: token.block, 120 | content: [] 121 | }; 122 | } 123 | 124 | function appendTo (element, content) { 125 | var target = element.content; 126 | 127 | if (element.notes !== undefined) { 128 | target = element.notes; 129 | } 130 | 131 | // If two string are added after one another, we can just as well 132 | // go ahead and concatenate them into a single string. 133 | var lastIdx = target.length - 1; 134 | if (typeof target[lastIdx] === 'string' && typeof content === 'string') { 135 | target[lastIdx] += content; 136 | } 137 | else { 138 | target.push(content); 139 | } 140 | } 141 | 142 | function extractProperties (source, properties) { 143 | var propertyFinder = /^\n*([-\w]+):([^$\n]*)/i 144 | , match 145 | ; 146 | 147 | while ((match = propertyFinder.exec(source)) !== null) { 148 | source = source.substr(0, match.index) + 149 | source.substr(match.index + match[0].length); 150 | 151 | properties[match[1].trim()] = match[2].trim(); 152 | 153 | propertyFinder.lastIndex = match.index; 154 | } 155 | 156 | return source; 157 | } 158 | 159 | function cleanInput(source) { 160 | // If all lines are indented, we should trim them all to the same point so that code doesn't 161 | // need to start at column 0 in the source (see GitHub Issue #105) 162 | 163 | // Helper to extract captures from the regex 164 | var getMatchCaptures = function (source, pattern) { 165 | var results = [], match; 166 | while ((match = pattern.exec(source)) !== null) 167 | results.push(match[1]); 168 | return results; 169 | }; 170 | 171 | // Calculate the minimum leading whitespace 172 | // Ensure there's at least one char that's not newline nor whitespace to ignore empty and blank lines 173 | var leadingWhitespacePattern = /^([ \t]*)[^ \t\n]/gm; 174 | var whitespace = getMatchCaptures(source, leadingWhitespacePattern).map(function (s) { return s.length; }); 175 | var minWhitespace = Math.min.apply(Math, whitespace); 176 | 177 | // Trim off the exact amount of whitespace, or less for blank lines (non-empty) 178 | var trimWhitespacePattern = new RegExp('^[ \\t]{0,' + minWhitespace + '}', 'gm'); 179 | return source.replace(trimWhitespacePattern, ''); 180 | } 181 | -------------------------------------------------------------------------------- /test/remark/views/slideshowView_test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter 2 | , TestDom = require('../../test_dom') 3 | , SlideshowView = require('../../../src/remark/views/slideshowView') 4 | , Slideshow = require('../../../src/remark/models/slideshow') 5 | , utils = require('../../../src/remark/utils') 6 | ; 7 | 8 | describe('SlideshowView', function () { 9 | var events 10 | , dom 11 | , model 12 | , containerElement 13 | , view 14 | ; 15 | 16 | beforeEach(function () { 17 | events = new EventEmitter(); 18 | dom = new TestDom(); 19 | model = new Slideshow(events); 20 | containerElement = document.createElement('div'); 21 | }); 22 | 23 | describe('container element configuration', function () { 24 | beforeEach(function () { 25 | view = new SlideshowView(events, dom, containerElement, model); 26 | }); 27 | 28 | it('should style element', function () { 29 | containerElement.className.should.include('remark-container'); 30 | }); 31 | 32 | it('should position element', function () { 33 | containerElement.style.position.should.equal('absolute'); 34 | }); 35 | 36 | it('should make element focusable', function () { 37 | containerElement.tabIndex.should.equal(-1); 38 | }); 39 | 40 | describe('proxying of element events', function () { 41 | it('should proxy keydown event', function (done) { 42 | events.on('keydown', function () { 43 | done(); 44 | }); 45 | 46 | triggerEvent(containerElement, 'keydown'); 47 | }); 48 | 49 | it('should proxy keypress event', function (done) { 50 | events.on('keypress', function () { 51 | done(); 52 | }); 53 | 54 | triggerEvent(containerElement, 'keypress'); 55 | }); 56 | 57 | it('should proxy mousewheel event', function (done) { 58 | events.on('mousewheel', function () { 59 | done(); 60 | }); 61 | 62 | triggerEvent(containerElement, 'mousewheel'); 63 | }); 64 | 65 | it('should proxy touchstart event', function (done) { 66 | events.on('touchstart', function () { 67 | done(); 68 | }); 69 | 70 | triggerEvent(containerElement, 'touchstart'); 71 | }); 72 | 73 | it('should proxy touchmove event', function (done) { 74 | events.on('touchmove', function () { 75 | done(); 76 | }); 77 | 78 | triggerEvent(containerElement, 'touchmove'); 79 | }); 80 | 81 | it('should proxy touchend event', function (done) { 82 | events.on('touchend', function () { 83 | done(); 84 | }); 85 | 86 | triggerEvent(containerElement, 'touchend'); 87 | }); 88 | }); 89 | }); 90 | 91 | describe('document.body container element configuration', function () { 92 | var body; 93 | 94 | beforeEach(function () { 95 | body = dom.getBodyElement(); 96 | containerElement = body; 97 | view = new SlideshowView(events, dom, containerElement, model); 98 | }); 99 | 100 | it('should style HTML element', function () { 101 | dom.getHTMLElement().className.should.include('remark-container'); 102 | }); 103 | 104 | it('should not position element', function () { 105 | containerElement.style.position.should.not.equal('absolute'); 106 | }); 107 | 108 | describe('proxying of element events', function () { 109 | it('should proxy resize event', function (done) { 110 | events.on('resize', function () { 111 | done(); 112 | }); 113 | 114 | triggerEvent(window, 'resize'); 115 | }); 116 | 117 | it('should proxy hashchange event', function (done) { 118 | events.on('hashchange', function () { 119 | done(); 120 | }); 121 | 122 | triggerEvent(window, 'hashchange'); 123 | }); 124 | 125 | it('should proxy keydown event', function (done) { 126 | events.on('keydown', function () { 127 | done(); 128 | }); 129 | 130 | triggerEvent(window, 'keydown'); 131 | }); 132 | 133 | it('should proxy keypress event', function (done) { 134 | events.on('keypress', function () { 135 | done(); 136 | }); 137 | 138 | triggerEvent(window, 'keypress'); 139 | }); 140 | 141 | it('should proxy mousewheel event', function (done) { 142 | events.on('mousewheel', function () { 143 | done(); 144 | }); 145 | 146 | triggerEvent(window, 'mousewheel'); 147 | }); 148 | 149 | it('should proxy touchstart event', function (done) { 150 | events.on('touchstart', function () { 151 | done(); 152 | }); 153 | 154 | triggerEvent(body, 'touchstart'); 155 | }); 156 | 157 | it('should proxy touchmove event', function (done) { 158 | events.on('touchmove', function () { 159 | done(); 160 | }); 161 | 162 | triggerEvent(body, 'touchmove'); 163 | }); 164 | 165 | it('should proxy touchend event', function (done) { 166 | events.on('touchend', function () { 167 | done(); 168 | }); 169 | 170 | triggerEvent(body, 'touchend'); 171 | }); 172 | }); 173 | }); 174 | 175 | describe('ratio calculation', function () { 176 | it('should calculate element size for 4:3', function () { 177 | model = new Slideshow(events, {ratio: '4:3'}); 178 | 179 | view = new SlideshowView(events, dom, containerElement, model); 180 | 181 | view.slideViews[0].scalingElement.style.width.should.equal('908px'); 182 | view.slideViews[0].scalingElement.style.height.should.equal('681px'); 183 | }); 184 | 185 | it('should calculate element size for 16:9', function () { 186 | model = new Slideshow(events, {ratio: '16:9'}); 187 | 188 | view = new SlideshowView(events, dom, containerElement, model); 189 | 190 | view.slideViews[0].scalingElement.style.width.should.equal('1210px'); 191 | view.slideViews[0].scalingElement.style.height.should.equal('681px'); 192 | }); 193 | }); 194 | 195 | describe('model synchronization', function () { 196 | beforeEach(function () { 197 | view = new SlideshowView(events, dom, containerElement, model); 198 | }); 199 | 200 | it('should create initial slide views', function () { 201 | view.slideViews.length.should.equal(1); 202 | }); 203 | 204 | it('should replace slide views on slideshow update', function () { 205 | model.loadFromString('a\n---\nb'); 206 | 207 | view.slideViews.length.should.equal(2); 208 | }); 209 | }); 210 | 211 | describe('modes', function () { 212 | beforeEach(function () { 213 | view = new SlideshowView(events, dom, containerElement, model); 214 | }); 215 | 216 | it('should toggle blackout on event', function () { 217 | events.emit('toggleBlackout'); 218 | 219 | utils.hasClass(containerElement, 'remark-blackout-mode').should.equal(true); 220 | }); 221 | 222 | it('should leave blackout mode on event', function () { 223 | utils.addClass(containerElement, 'remark-blackout-mode'); 224 | events.emit('hideOverlay'); 225 | 226 | utils.hasClass(containerElement, 'remark-blackout-mode').should.equal(false); 227 | }); 228 | }); 229 | 230 | function triggerEvent(element, eventName) { 231 | var event = document.createEvent('HTMLEvents'); 232 | event.initEvent(eventName, true, true); 233 | element.dispatchEvent(event); 234 | } 235 | }); 236 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ### 0.6.5 2 | * #115: Highlight *-prefixed code block lines. 3 | * #110: Enable click navigation when configured. 4 | * #108: Add `sourceUrl` configuration option ([DanTup](https://github.com/DanTup)). 5 | * #107: Add blackout mode. 6 | * #104: Increase default font sizes. 7 | * #102: Add default fonts to templates. 8 | 9 | ### 0.6.4 10 | * #105/106: Support indented source code ([DanTup](https://github.com/DanTup)). 11 | 12 | ### 0.6.3 13 | * #101: Make navigation using scroll configurable. 14 | 15 | ### 0.6.2 16 | * #77: Enable Matjax for slide notes by keeping notes HTML in DOM. 17 | * #82: Hide help screen when hitting Escape. 18 | * #85, #87: No longer operate on escaped HTML. 19 | * #98: Flatten CSS hierarchy for `remark-slide-content` to ease styling. 20 | 21 | ### 0.6.1 22 | * #81: Introduce boilerplate HTML files ([gurjeet](https://github.com/gurjeet)). 23 | * #83: Always include background colors and images. 24 | * #91: Bundle Haskell syntax highlighting ([sol](https://github.com/sol)). 25 | * #92: Use official highlight.js ([nanoant](https://github.com/nanoant)). 26 | * #96: Add Bower integration ([trumbitta](https://github.com/trumbitta)). 27 | * Run tests using PhantomJS, which enables running tests on Windows. 28 | 29 | ### 0.6.0 30 | * #73: Fix infinite loop issue for cloned views ([peter50216](https://github.com/peter50216)). 31 | * #71: Make `img { max-with: 100%}` work in Firefox ([obfusk](https://github.com/obfusk)). 32 | * #69: Assign `remark-fading` class to slide being hidden to allow animated transitions ([freakboy3742](https://github.com/freakboy3742)). 33 | * #68: Add overlay in presenter mode to indicate paused state ([freakboy3742](https://github.com/freakboy3742)). 34 | * #67: Make slideshow controller customizable ([freakboy3742](https://github.com/freakboy3742)). 35 | * #66: Add timer for presentation view ([freakboy3742](https://github.com/freakboy3742)). 36 | * #64: Expose API endpoints for display functions ([freakboy3742](https://github.com/freakboy3742)). 37 | 38 | ### 0.5.9 39 | * #62: Inherit presenter notes from template slide. 40 | 41 | ### 0.5.8 42 | * #61: Only handle shortcut keys when meta/ctrl key is not pressed. 43 | 44 | ### 0.5.7 45 | * Hardcode paper dimensions to make slides fit perfectly when printing / exporting to PDF. 46 | 47 | ### 0.5.6 48 | * #50: Support printing / export to PDF via Save as PDF in Chrome. 49 | * Extend API: ([gureckis](https://github.com/gureckis)) 50 | * Add `slideshow.pause()` and `slideshow.resume()` for bypassing keyboard navigation. 51 | * Add `[before|after][Show|Hide]Slide` events. 52 | 53 | ### 0.5.5 54 | * #53: Use highlight.js fork that fixes Scala multiline string issue. 55 | * #54: Expose slide object in showSlide and hideSlide events. 56 | * Add fullscreen mode toggle. 57 | 58 | ### 0.5.4 59 | * Fix content class issue (#52) by allowing capital letters. 60 | 61 | ### 0.5.3 62 | * Fix Firefox issue (#47) by handling quoted CSS URLs. 63 | 64 | ### 0.5.2 65 | * Add presenter mode and support functionality for cloning slideshow. 66 | 67 | ### 0.5.1 68 | * Fix Firefox issue (#47) by extending HTMLCollection with forEach. 69 | * Fix empty paragraphs regression. 70 | * Flatten CSS class hierarchy to ease styling. 71 | * Remove default font size and family styles. 72 | 73 | ### 0.5.0 74 | * Update API to allow creating and embedding multiple slideshows. 75 | * Prefix CSS class names with `remark-` to avoid collisions. 76 | * Add highlight-style slide property for setting highlight.js style. 77 | * Highlighting language is no longer automatically determined. 78 | * Must either be configured for entire slideshow or specified per code block. 79 | * Code classes are DEPRECATED, use GFM fenced code blocks instead. 80 | * Fix content classes being expanded inside code blocks bug. 81 | 82 | ### 0.4.6 83 | * Add background-image slide property. 84 | * Make slide backgrounds centered, not repeated, and, if needed, down-scaled to fit slide. 85 | * Make remark.config.set and .get functions for accessing configuration. 86 | * Update highlighting styles when highlightStyle property is reconfigured. 87 | * Update slideshow display ratio when ratio property is reconfigured. 88 | 89 | ### 0.4.5 90 | * Fix multiple block quotes bug. 91 | * Add HTTP language highlighting support. 92 | * Add HOME and END shortcut keys for navigation to first and last slide. 93 | * Add help overlay triggered by pressing ?. 94 | * Add API methods: 95 | * `remark.loadFromString('markdown string')` 96 | * `remark.gotoFirstSlide()` 97 | * `remark.gotoLastSlide()` 98 | * `remark.gotoNextSlide()` 99 | * `remark.gotoPreviousSlide()` 100 | * `remark.gotoSlide(slideNoOrName)` 101 | * Add `ratio` configuration option. 102 | 103 | ### 0.4.4 104 | * Fix missing Markdown conversion of content inside HTML blocks. 105 | 106 | ### 0.4.3 107 | * Fix .left CSS class (via @lionel-m). 108 | * Fix support for block-quotes (via @joshbode). 109 | * Update dependencies to support node v0.8.x. 110 | 111 | ### 0.4.2 112 | * Emit 'ready' event. 113 | * Upgrade marked. 114 | * Enable Github Flavored Markdown (GFM). 115 | 116 | ### 0.4.1 117 | * Perform regular property expansion after inheriting templates. 118 | * Exclude highlight.js styles depending on background images. 119 | 120 | ### 0.4.0 121 | * Slide classes are DEPRECATED, use slide `class` property instead. 122 | * Slide properties: 123 | * name 124 | * class 125 | * continued 126 | * template 127 | * layout 128 | * Expand `{{ property }}` to corresponding property value. 129 | * Access slides by name in URL fragment. 130 | * Upgrade highlight.js. 131 | 132 | ### 0.3.6 133 | * Upgrade highlight.js. 134 | * Upgrade marked. 135 | * Configure embedded languages for build in package.json. 136 | * Update embedded languages: 137 | * javascript 138 | * ruby 139 | * python 140 | * bash 141 | * java 142 | * php 143 | * perl 144 | * cpp 145 | * objectivec 146 | * cs 147 | * sql 148 | * xml 149 | * css 150 | * scala 151 | * coffeescript 152 | * lisp 153 | 154 | ### 0.3.5 155 | * Convert slide attributes, i.e. .attribute=value. 156 | * Fix slide content overflow issue. 157 | * Embed more slide and content classes; `.left`, `.center`, `.right`, `.top`, `.middle` and `.bottom`. 158 | 159 | ### 0.3.4 160 | * Upgrade marked. 161 | * Disable Github Flavored Markdown (GFM) to prevent autolinks, i.e. src attributes for img or iframe tags turning into links. 162 | 163 | ### 0.3.3 164 | * Expose `config` function. 165 | * Add support for `highlightLanguage` configuration option. 166 | * Add support for `highlightInline` configuration option. 167 | 168 | ### 0.3.2 169 | * Expose highlighter engine ([kjbekkelund](https://github.com/kjbekkelund)). 170 | * Handle 0 to 3 spaces before # in headings ([kjbekkelund](https://github.com/kjbekkelund)). 171 | * Support headings inside DIVs ([kjbekkelund](https://github.com/kjbekkelund)). 172 | * Use marked instead of Showdown ([kjbekkelund](https://github.com/kjbekkelund)). 173 | * Build remark using Node.js instead of Ruby. 174 | * Run tests using Buster.js instead of Jasmine. 175 | 176 | ### 0.3.1 177 | * Initial event support ([kjbekkelund](https://github.com/kjbekkelund)). 178 | * Made remark.config a function accepting configuration options. 179 | * Added support for multiple content classes on a single line. 180 | 181 | ### 0.3.0 182 | 183 | * Input Markdown source element should now be of type TEXTAREA instead of PRE. 184 | * Added proper escaping of in-code HTML. 185 | * Made highlight.js styles work on inline code as well as block code. 186 | 187 | ### 0.2.4 188 | 189 | * Made highlight style configurable through `highlightStyle` option. 190 | * Added current slide number to slides. 191 | * Disabled highlighting of inline code without language hinting. 192 | 193 | ### 0.2.3 194 | 195 | * Added full highlight.js supporting a whole bunch of languages. 196 | 197 | ### 0.2.2 198 | 199 | * Simple handling of swiping, e.g. for iPhones ([kjbekkelund](https://github.com/kjbekkelund)). 200 | 201 | ### 0.2.1 202 | 203 | * Fixed non-working links via touch events. 204 | * Fixed non-working resize ([kjbekkelund](https://github.com/kjbekkelund)). 205 | 206 | ### 0.2.0 207 | 208 | * Added slide navigation using page up/down keys and mouse wheel. 209 | * Added touch events in order to support mobile phones ([kjbekkelund](https://github.com/kjbekkelund)). 210 | * Go to the next slide when pressing Space ([kjbekkelund](https://github.com/kjbekkelund)). 211 | 212 | ### 0.1.2 213 | 214 | * Prepending instead of appending default styles to <head> ([kjbekkelund](https://github.com/kjbekkelund)). 215 | 216 | ### 0.1.1 217 | 218 | * Fixed bug with markdown contained in content classes, i.e. `.class[![image](img.jpg)]`. 219 | 220 | ### 0.1.0 221 | 222 | * Initial version. 223 | -------------------------------------------------------------------------------- /test/remark/parser_test.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../../src/remark/parser'); 2 | 3 | describe('Parser', function () { 4 | 5 | describe('splitting source into correct number of slides', function () { 6 | it('should handle single slide', function () { 7 | parser.parse('1').length.should.equal(1); 8 | }); 9 | 10 | it('should handle multiple slides', function () { 11 | parser.parse('1\n---\n2\n---\n3').length.should.equal(3); 12 | }); 13 | 14 | it('should treat empty source as single slide', function () { 15 | parser.parse('').length.should.equal(1); 16 | }); 17 | 18 | it('should ignore slide separator inside fences', function () { 19 | parser.parse('1\n---\n```\n---\n```\n---\n3').length.should.equal(3); 20 | }); 21 | 22 | it('should ignore slide separator inside content class', function () { 23 | parser.parse('1\n---\n2\n.class[\n---\n]\n---\n3').length.should.equal(3); 24 | }); 25 | }); 26 | 27 | describe('mapping source to corresponding slides', function () { 28 | it('should handle single slide', function () { 29 | parser.parse('1')[0].content.should.eql(['1']); 30 | }); 31 | 32 | it('should handle multiple slides', function () { 33 | var slides = parser.parse('1\n---\n2\n---\n3'); 34 | 35 | slides[0].content.should.eql(['1']); 36 | slides[1].content.should.eql(['2']); 37 | slides[2].content.should.eql(['3']); 38 | }); 39 | 40 | it('should handle empty source', function () { 41 | parser.parse('')[0].content.should.eql(['']); 42 | }); 43 | }); 44 | 45 | describe('parsing notes', function () { 46 | it('should map notes', function () { 47 | parser.parse('content\n???\nnotes')[0].notes.should.eql(['notes']); 48 | }); 49 | 50 | it('should extract notes from source', function () { 51 | parser.parse('content\n???\nnotes')[0].content.should.eql(['content']); 52 | }); 53 | }); 54 | 55 | describe('parsing code', function () { 56 | it('should include code', function () { 57 | var slides = parser.parse('1\n code\n2\n---\n3\n code\n4'); 58 | 59 | slides[0].content.should.eql(['1\n code\n2']); 60 | slides[1].content.should.eql(['3\n code\n4']); 61 | }); 62 | 63 | it('should ignore content class inside code', function () { 64 | parser.parse('some code\n .class[x]')[0].content.should.eql(['some code\n .class[x]']); 65 | }); 66 | }); 67 | 68 | describe('parsing fences', function () { 69 | it('should include fences', function () { 70 | var slides = parser.parse('1\n```\n\n```\n2\n---\n3\n```\n\n```\n4'); 71 | 72 | slides[0].content.should.eql(['1\n```\n\n```\n2']); 73 | slides[1].content.should.eql(['3\n```\n\n```\n4']); 74 | }); 75 | 76 | it('should ignore content class inside fences', function () { 77 | parser.parse('```\n.class[x]\n```')[0].content 78 | .should.eql(['```\n.class[x]\n```']); 79 | }); 80 | }); 81 | 82 | describe('parsing link definitions', function () { 83 | it('should extract link definitions', function () { 84 | parser.parse('[id]: http://url.com "title"')[0].links.id 85 | .should.eql({ href: 'http://url.com', title: 'title' }); 86 | }); 87 | }); 88 | 89 | describe('parsing content classes', function () { 90 | it('should convert block content classes', function () { 91 | parser.parse('1 .class[\nx\n] 2')[0].content 92 | .should.eql([ 93 | '1 ', 94 | { class: 'class', block: true, content: ['\nx\n'] }, 95 | ' 2' 96 | ]); 97 | }); 98 | 99 | it('should convert inline content classes', function () { 100 | parser.parse('1 .class[x] 2')[0].content 101 | .should.eql([ 102 | '1 ', 103 | { class: 'class', block: false, content: ['x'] }, 104 | ' 2' 105 | ]); 106 | }); 107 | 108 | it('should convert multiple classes', function () { 109 | parser.parse('1 .c1.c2[x]')[0].content 110 | .should.eql([ 111 | '1 ', 112 | { class: 'c1 c2', block: false, content: ['x'] } 113 | ]); 114 | }); 115 | 116 | it('should ignore unclosed inline content classes', function () { 117 | parser.parse('1 .class[x 2')[0].content.should.eql(['1 .class[x 2']); 118 | }); 119 | 120 | it('should ignore unclosed block content classes', function () { 121 | parser.parse('1 .class[\n2')[0].content.should.eql(['1 .class[\n2']); 122 | }); 123 | 124 | it('should parse source in content classes', function () { 125 | parser.parse('.c1[.c2[x]]')[0].content 126 | .should.eql([ 127 | { class: 'c1', block: false, content: 128 | [{ class: 'c2', block: false, content: ['x'] }] 129 | } 130 | ]); 131 | }); 132 | }); 133 | 134 | describe('identifying continued slides', function () { 135 | it('should not identify normal, preceding slide as continued', function () { 136 | parser.parse('1\n--\n2\n---\n3')[0].properties.continued.should.equal('false'); 137 | }); 138 | 139 | it('should identify continued slide as continued', function () { 140 | parser.parse('1\n--\n2\n---\n3')[1].properties.continued.should.equal('true'); 141 | }); 142 | 143 | it('should not identify normal, succeeding slide as continued', function () { 144 | parser.parse('1\n--\n2\n---\n3')[2].properties.continued.should.equal('false'); 145 | }); 146 | }); 147 | 148 | describe('parsing slide properties', function () { 149 | it('should map single property', function () { 150 | parser.parse('name: a\n1')[0].properties.name.should.equal('a'); 151 | }); 152 | 153 | it('should map multiple properties', function () { 154 | var slides = parser.parse('name: a\nclass:b\n1'); 155 | 156 | slides[0].properties.name.should.equal('a'); 157 | slides[0].properties['class'].should.equal('b'); 158 | }); 159 | 160 | it('should allow properties with no value', function () { 161 | var slides = parser.parse('a: \n\nContent.'); 162 | slides[0].properties.should.have.property('a', ''); 163 | }); 164 | 165 | it('should extract properties from source', function () { 166 | parser.parse('name: a\nclass:b\n1')[0].content.should.eql(['\n1']); 167 | }); 168 | }); 169 | 170 | describe('parsing content that is indented', function () { 171 | it('should handle leading whitespace on all lines', function () { 172 | var slides = parser.parse(' 1\n ---\n 2\n ---\n 3'); 173 | 174 | slides[0].content.should.eql(['1']); 175 | slides[1].content.should.eql(['2']); 176 | slides[2].content.should.eql(['3']); 177 | }); 178 | 179 | it('should ignore empty lines when calculating whitespace to trim', function () { 180 | var slides = parser.parse(' 1\n\n 1\n ---\n 2\n ---\n 3'); 181 | 182 | slides[0].content.should.eql(['1\n\n1']); 183 | slides[1].content.should.eql(['2']); 184 | slides[2].content.should.eql(['3']); 185 | }); 186 | 187 | it('should ignore blank lines when calculating whitespace to trim', function () { 188 | var slides = parser.parse(' 1\n \n 1\n ---\n 2\n ---\n 3'); 189 | 190 | slides[0].content.should.eql(['1\n\n1']); 191 | slides[1].content.should.eql(['2']); 192 | slides[2].content.should.eql(['3']); 193 | }); 194 | 195 | it('should preserve leading whitespace that goes beyond the minimum whitespace on inner lines', function () { 196 | var slides = parser.parse(' 1\n ---\n 2\n ---\n 3'); 197 | 198 | slides[0].content.should.eql(['1']); 199 | slides[1].content.should.eql([' 2\n']); // Note: lexer includes trailing newines in code blocks 200 | slides[2].content.should.eql(['3']); 201 | }); 202 | 203 | it('should preserve leading whitespace that goes beyond the minimum whitespace on the first line', function () { 204 | var slides = parser.parse(' 1\n ---\n 2\n ---\n 3'); 205 | 206 | slides[0].content.should.eql([' 1\n']); // Note: lexer includes trailing newines in code blocks 207 | slides[1].content.should.eql(['2']); 208 | slides[2].content.should.eql(['3']); 209 | }); 210 | 211 | it('should preserve leading whitespace that goes beyond the minimum whitespace on the last line', function () { 212 | var slides = parser.parse(' 1\n ---\n 2\n ---\n 3'); 213 | 214 | slides[0].content.should.eql(['1']); 215 | slides[1].content.should.eql(['2']); 216 | slides[2].content.should.eql([' 3']); 217 | }); 218 | }); 219 | 220 | var parser; 221 | 222 | beforeEach(function () { 223 | parser = new Parser(); 224 | }); 225 | 226 | }); 227 | -------------------------------------------------------------------------------- /src/remark/views/slideView.js: -------------------------------------------------------------------------------- 1 | var converter = require('../converter') 2 | , highlighter = require('../highlighter') 3 | , utils = require('../utils') 4 | ; 5 | 6 | module.exports = SlideView; 7 | 8 | function SlideView (events, slideshow, scaler, slide) { 9 | var self = this; 10 | 11 | self.events = events; 12 | self.slideshow = slideshow; 13 | self.scaler = scaler; 14 | self.slide = slide; 15 | 16 | self.configureElements(); 17 | self.updateDimensions(); 18 | 19 | self.events.on('propertiesChanged', function (changes) { 20 | if (changes.hasOwnProperty('ratio')) { 21 | self.updateDimensions(); 22 | } 23 | }); 24 | } 25 | 26 | SlideView.prototype.updateDimensions = function () { 27 | var self = this 28 | , dimensions = self.scaler.dimensions 29 | ; 30 | 31 | self.scalingElement.style.width = dimensions.width + 'px'; 32 | self.scalingElement.style.height = dimensions.height + 'px'; 33 | }; 34 | 35 | SlideView.prototype.scale = function (containerElement) { 36 | var self = this; 37 | 38 | self.scaler.scaleToFit(self.scalingElement, containerElement); 39 | }; 40 | 41 | SlideView.prototype.show = function () { 42 | utils.addClass(this.containerElement, 'remark-visible'); 43 | utils.removeClass(this.containerElement, 'remark-fading'); 44 | }; 45 | 46 | SlideView.prototype.hide = function () { 47 | var self = this; 48 | utils.removeClass(this.containerElement, 'remark-visible'); 49 | // Don't just disappear the slide. Mark it as fading, which 50 | // keeps it on the screen, but at a reduced z-index. 51 | // Then set a timer to remove the fading state in 1s. 52 | utils.addClass(this.containerElement, 'remark-fading'); 53 | setTimeout(function(){ 54 | utils.removeClass(self.containerElement, 'remark-fading'); 55 | }, 1000); 56 | }; 57 | 58 | SlideView.prototype.configureElements = function () { 59 | var self = this; 60 | 61 | self.containerElement = document.createElement('div'); 62 | self.containerElement.className = 'remark-slide-container'; 63 | 64 | self.scalingElement = document.createElement('div'); 65 | self.scalingElement.className = 'remark-slide-scaler'; 66 | 67 | self.element = document.createElement('div'); 68 | self.element.className = 'remark-slide'; 69 | 70 | self.contentElement = createContentElement(self.events, self.slideshow, self.slide); 71 | self.notesElement = createNotesElement(self.slideshow, self.slide.notes); 72 | 73 | self.numberElement = document.createElement('div'); 74 | self.numberElement.className = 'remark-slide-number'; 75 | self.numberElement.innerHTML = self.slide.number + ' / ' + self.slideshow.getSlides().length; 76 | 77 | self.contentElement.appendChild(self.numberElement); 78 | self.element.appendChild(self.contentElement); 79 | self.element.appendChild(self.notesElement); 80 | self.scalingElement.appendChild(self.element); 81 | self.containerElement.appendChild(self.scalingElement); 82 | }; 83 | 84 | SlideView.prototype.scaleBackgroundImage = function (dimensions) { 85 | var self = this 86 | , styles = window.getComputedStyle(this.contentElement) 87 | , backgroundImage = styles.backgroundImage 88 | , match 89 | , image 90 | , scale 91 | ; 92 | 93 | if ((match = /^url\(("?)([^\)]+?)\1\)/.exec(backgroundImage)) !== null) { 94 | image = new Image(); 95 | image.onload = function () { 96 | if (image.width > dimensions.width || 97 | image.height > dimensions.height) { 98 | // Background image is larger than slide 99 | if (!self.originalBackgroundSize) { 100 | // No custom background size has been set 101 | self.originalBackgroundSize = self.contentElement.style.backgroundSize; 102 | self.originalBackgroundPosition = self.contentElement.style.backgroundPosition; 103 | self.backgroundSizeSet = true; 104 | 105 | if (dimensions.width / image.width < dimensions.height / image.height) { 106 | scale = dimensions.width / image.width; 107 | } 108 | else { 109 | scale = dimensions.height / image.height; 110 | } 111 | 112 | self.contentElement.style.backgroundSize = image.width * scale + 113 | 'px ' + image.height * scale + 'px'; 114 | self.contentElement.style.backgroundPosition = '50% ' + 115 | ((dimensions.height - (image.height * scale)) / 2) + 'px'; 116 | } 117 | } 118 | else { 119 | // Revert to previous background size setting 120 | if (self.backgroundSizeSet) { 121 | self.contentElement.style.backgroundSize = self.originalBackgroundSize; 122 | self.contentElement.style.backgroundPosition = self.originalBackgroundPosition; 123 | self.backgroundSizeSet = false; 124 | } 125 | } 126 | }; 127 | image.src = match[2]; 128 | } 129 | }; 130 | 131 | function createContentElement (events, slideshow, slide) { 132 | var element = document.createElement('div'); 133 | 134 | if (slide.properties.name) { 135 | element.id = 'slide-' + slide.properties.name; 136 | } 137 | 138 | styleContentElement(slideshow, element, slide.properties); 139 | 140 | element.innerHTML = converter.convertMarkdown(slide.content, slideshow.getLinks()); 141 | 142 | highlightCodeBlocks(element, slideshow); 143 | 144 | return element; 145 | } 146 | 147 | function styleContentElement (slideshow, element, properties) { 148 | element.className = ''; 149 | 150 | setClassFromProperties(element, properties); 151 | setHighlightStyleFromProperties(element, properties, slideshow); 152 | setBackgroundFromProperties(element, properties); 153 | } 154 | 155 | function createNotesElement (slideshow, notes) { 156 | var element = document.createElement('div'); 157 | 158 | element.style.display = 'none'; 159 | 160 | element.innerHTML = converter.convertMarkdown(notes); 161 | 162 | highlightCodeBlocks(element, slideshow); 163 | 164 | return element; 165 | } 166 | 167 | function setBackgroundFromProperties (element, properties) { 168 | var backgroundImage = properties['background-image']; 169 | 170 | if (backgroundImage) { 171 | element.style.backgroundImage = backgroundImage; 172 | } 173 | } 174 | 175 | function setHighlightStyleFromProperties (element, properties, slideshow) { 176 | var highlightStyle = properties['highlight-style'] || 177 | slideshow.getHighlightStyle(); 178 | 179 | if (highlightStyle) { 180 | utils.addClass(element, 'hljs-' + highlightStyle); 181 | } 182 | } 183 | 184 | function setClassFromProperties (element, properties) { 185 | utils.addClass(element, 'remark-slide-content'); 186 | 187 | (properties['class'] || '').split(/,| /) 188 | .filter(function (s) { return s !== ''; }) 189 | .forEach(function (c) { utils.addClass(element, c); }); 190 | } 191 | 192 | function highlightCodeBlocks (content, slideshow) { 193 | var codeBlocks = content.getElementsByTagName('code') 194 | ; 195 | 196 | codeBlocks.forEach(function (block) { 197 | if (block.parentElement.tagName !== 'PRE') { 198 | utils.addClass(block, 'remark-inline-code'); 199 | return; 200 | } 201 | 202 | if (block.className === '') { 203 | block.className = slideshow.getHighlightLanguage(); 204 | } 205 | 206 | var meta = extractMetadata(block); 207 | 208 | if (block.className !== '') { 209 | highlighter.engine.highlightBlock(block, ' '); 210 | } 211 | 212 | wrapLines(block); 213 | highlightBlockLines(block, meta.highlightedLines); 214 | highlightBlockSpans(block); 215 | 216 | utils.addClass(block, 'remark-code'); 217 | }); 218 | } 219 | 220 | function extractMetadata (block) { 221 | var highlightedLines = []; 222 | 223 | block.innerHTML = block.innerHTML.split(/\r?\n/).map(function (line, i) { 224 | if (line.indexOf('*') === 0) { 225 | highlightedLines.push(i); 226 | return line.replace(/^\*( )?/, '$1$1'); 227 | } 228 | 229 | return line; 230 | }).join('\n'); 231 | 232 | return { 233 | highlightedLines: highlightedLines 234 | }; 235 | } 236 | 237 | function wrapLines (block) { 238 | var lines = block.innerHTML.split(/\r?\n/).map(function (line) { 239 | return '
' + line + '
'; 240 | }); 241 | 242 | // Remove empty last line (due to last \n) 243 | if (lines.length && lines[lines.length - 1].indexOf('><') !== -1) { 244 | lines.pop(); 245 | } 246 | 247 | block.innerHTML = lines.join(''); 248 | } 249 | 250 | function highlightBlockLines (block, lines) { 251 | lines.forEach(function (i) { 252 | utils.addClass(block.childNodes[i], 'remark-code-line-highlighted'); 253 | }); 254 | } 255 | 256 | function highlightBlockSpans (block) { 257 | var pattern = /([^\\`])`([^`]+?)`/g 258 | , replacement = '$1$2' 259 | ; 260 | 261 | block.childNodes.forEach(function (element) { 262 | element.innerHTML = element.innerHTML.replace(pattern, replacement); 263 | }); 264 | } 265 | -------------------------------------------------------------------------------- /src/remark/resources.js: -------------------------------------------------------------------------------- 1 | /* Automatically generated */ 2 | 3 | module.exports = { 4 | documentStyles: "html.remark-container,body.remark-container{height:100%;width:100%;-webkit-print-color-adjust:exact;}.remark-container{background:#d7d8d2;margin:0;overflow:hidden;}.remark-container:focus{outline-style:solid;outline-width:1px;}:-webkit-full-screen .remark-container{width:100%;height:100%;}.remark-slides-area{position:relative;height:100%;width:100%;}.remark-slide-container{display:none;position:absolute;height:100%;width:100%;page-break-after:always;}.remark-slide-scaler{background-color:transparent;overflow:hidden;position:absolute;-webkit-transform-origin:top left;-moz-transform-origin:top left;transform-origin:top-left;-moz-box-shadow:0 0 30px #888;-webkit-box-shadow:0 0 30px #888;box-shadow:0 0 30px #888;}.remark-slide{height:100%;width:100%;display:table;table-layout:fixed;}.remark-slide>.left{text-align:left;}.remark-slide>.center{text-align:center;}.remark-slide>.right{text-align:right;}.remark-slide>.top{vertical-align:top;}.remark-slide>.middle{vertical-align:middle;}.remark-slide>.bottom{vertical-align:bottom;}.remark-slide-content{background-color:#fff;background-position:center;background-repeat:no-repeat;display:table-cell;font-size:20px;padding:1em 4em 1em 4em;}.remark-slide-content h1{font-size:55px;}.remark-slide-content h2{font-size:45px;}.remark-slide-content h3{font-size:35px;}.remark-slide-content .left{display:block;text-align:left;}.remark-slide-content .center{display:block;text-align:center;}.remark-slide-content .right{display:block;text-align:right;}.remark-slide-number{bottom:12px;opacity:0.5;position:absolute;right:20px;}.remark-code{font-size:18px;}.remark-code-line{min-height:1em;}.remark-code-line-highlighted{background-color:rgba(255, 255, 0, 0.5);}.remark-code-span-highlighted{background-color:rgba(255, 255, 0, 0.5);padding:1px 2px 2px 2px;}.remark-visible{display:block;z-index:2;}.remark-fading{display:block;z-index:1;}.remark-fading .remark-slide-scaler{-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none;}.remark-backdrop{position:absolute;top:0;bottom:0;left:0;right:0;display:none;background:#000;z-index:2;}.remark-pause{bottom:0;top:0;right:0;left:0;display:none;position:absolute;z-index:1000;}.remark-pause .remark-pause-lozenge{margin-top:30%;text-align:center;}.remark-pause .remark-pause-lozenge span{color:white;background:black;border:2px solid black;border-radius:20px;padding:20px 30px;font-family:Helvetica,arial,freesans,clean,sans-serif;font-size:42pt;font-weight:bold;}.remark-container.remark-presenter-mode.remark-pause-mode .remark-pause{display:block;}.remark-container.remark-presenter-mode.remark-pause-mode .remark-backdrop{display:block;opacity:0.5;}.remark-help{bottom:0;top:0;right:0;left:0;display:none;position:absolute;z-index:1000;-webkit-transform-origin:top left;-moz-transform-origin:top left;transform-origin:top-left;}.remark-help .remark-help-content{color:white;font-family:Helvetica,arial,freesans,clean,sans-serif;font-size:12pt;position:absolute;top:5%;bottom:10%;height:10%;left:5%;width:90%;}.remark-help .remark-help-content h1{font-size:36px;}.remark-help .remark-help-content td{color:white;font-size:12pt;padding:10px;}.remark-help .remark-help-content td:first-child{padding-left:0;}.remark-help .remark-help-content .key{background:white;color:black;min-width:1em;display:inline-block;padding:3px 6px;text-align:center;border-radius:4px;font-size:14px;}.remark-help .dismiss{top:85%;}.remark-container.remark-help-mode .remark-help{display:block;}.remark-container.remark-help-mode .remark-backdrop{display:block;opacity:0.95;}.remark-preview-area{bottom:2%;left:2%;display:none;opacity:0.5;position:absolute;height:47.25%;width:48%;}.remark-preview-area .remark-slide-container{display:block;}.remark-notes-area{background:#e7e8e2;bottom:0;display:none;left:52%;overflow:hidden;position:absolute;right:0;top:0;}.remark-notes-area .remark-top-area{height:50px;left:20px;position:absolute;right:10px;top:10px;}.remark-notes-area .remark-bottom-area{position:absolute;top:75px;bottom:10px;left:20px;right:10px;}.remark-notes-area .remark-bottom-area .remark-toggle{display:block;text-decoration:none;font-family:Helvetica,arial,freesans,clean,sans-serif;border-bottom:1px solid #ccc;height:21px;font-size:0.75em;font-weight:bold;text-transform:uppercase;color:#666;text-shadow:#f5f5f5 1px 1px 1px;}.remark-notes-area .remark-bottom-area .remark-notes-current-area{height:70%;position:relative;}.remark-notes-area .remark-bottom-area .remark-notes-current-area .remark-notes{clear:both;border-top:1px solid #f5f5f5;position:absolute;top:22px;bottom:0px;left:0px;right:0px;overflow-y:auto;margin-bottom:20px;}.remark-notes-area .remark-bottom-area .remark-notes-preview-area{height:30%;position:relative;}.remark-notes-area .remark-bottom-area .remark-notes-preview-area .remark-notes-preview{border-top:1px solid #f5f5f5;position:absolute;top:22px;bottom:0px;left:0px;right:0px;overflow-y:auto;}.remark-notes-area .remark-bottom-area .remark-notes>*:first-child,.remark-notes-area .remark-bottom-area .remark-notes-preview>*:first-child{margin-top:5px;}.remark-notes-area .remark-bottom-area .remark-notes>*:last-child,.remark-notes-area .remark-bottom-area .remark-notes-preview>*:last-child{margin-bottom:0;}.remark-toolbar{color:#979892;vertical-align:middle;}.remark-toolbar .remark-toolbar-link{border:2px solid #d7d8d2;color:#979892;display:inline-block;padding:2px 2px;text-decoration:none;text-align:center;min-width:20px;}.remark-toolbar .remark-toolbar-link:hover{border-color:#979892;color:#676862;}.remark-toolbar .remark-toolbar-timer{border:2px solid black;border-radius:10px;background:black;color:white;display:inline-block;float:right;padding:5px 10px;font-family:sans-serif;font-weight:bold;font-size:175%;text-decoration:none;text-align:center;}.remark-container.remark-presenter-mode .remark-slides-area{top:2%;left:2%;height:47.25%;width:48%;}.remark-container.remark-presenter-mode .remark-preview-area{display:block;}.remark-container.remark-presenter-mode .remark-notes-area{display:block;}.remark-container.remark-blackout-mode:not(.remark-presenter-mode) .remark-backdrop{display:block;opacity:0.99;}@media print{.remark-container{overflow:visible;background-color:#fff;} .remark-slide-container{display:block;position:relative;} .remark-slide-scaler{-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none;}}@page {size:908px 681px;margin:0;}", 5 | containerLayout: "
\n
\n
\n +\n -\n \n
\n
\n
\n
\n
Notes for current slide
\n
\n
\n
\n
Notes for next slide
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n Paused\n
\n
\n
\n
\n

Help

\n

Keyboard shortcuts

\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n ,\n ,\n Pg Up,\n k\n Go to previous slide
\n ,\n ,\n Pg Dn,\n Space,\n j\n Go to next slide
\n Home\n Go to first slide
\n End\n Go to last slide
\n b\n Toggle blackout mode
\n f\n Toggle fullscreen mode
\n c\n Clone slideshow
\n p\n Toggle presenter mode
\n w\n Pause/Resume the presentation
\n t\n Restart the presentation timer
\n ?,\n h\n Toggle this help
\n
\n
\n \n \n \n \n \n
\n Esc\n Back to slideshow
\n
\n
\n" 6 | }; 7 | -------------------------------------------------------------------------------- /src/remark/views/slideshowView.js: -------------------------------------------------------------------------------- 1 | var SlideView = require('./slideView') 2 | , Timer = require('components/timer') 3 | , NotesView = require('./notesView') 4 | , Scaler = require('../scaler') 5 | , resources = require('../resources') 6 | , utils = require('../utils') 7 | ; 8 | 9 | module.exports = SlideshowView; 10 | 11 | function SlideshowView (events, dom, containerElement, slideshow) { 12 | var self = this; 13 | 14 | self.events = events; 15 | self.dom = dom; 16 | self.slideshow = slideshow; 17 | self.scaler = new Scaler(events, slideshow); 18 | self.slideViews = []; 19 | 20 | self.configureContainerElement(containerElement); 21 | self.configureChildElements(); 22 | 23 | self.updateDimensions(); 24 | self.scaleElements(); 25 | self.updateSlideViews(); 26 | 27 | self.timer = new Timer(events, self.timerElement); 28 | 29 | events.on('slidesChanged', function () { 30 | self.updateSlideViews(); 31 | }); 32 | 33 | events.on('hideSlide', function (slideIndex) { 34 | // To make sure that there is only one element fading at a time, 35 | // remove the fading class from all slides before hiding 36 | // the new slide. 37 | self.elementArea.getElementsByClassName('remark-fading').forEach(function (slide) { 38 | utils.removeClass(slide, 'remark-fading'); 39 | }); 40 | self.hideSlide(slideIndex); 41 | }); 42 | 43 | events.on('showSlide', function (slideIndex) { 44 | self.showSlide(slideIndex); 45 | }); 46 | 47 | events.on('togglePresenterMode', function () { 48 | utils.toggleClass(self.containerElement, 'remark-presenter-mode'); 49 | self.scaleElements(); 50 | }); 51 | 52 | events.on('toggleHelp', function () { 53 | utils.toggleClass(self.containerElement, 'remark-help-mode'); 54 | }); 55 | 56 | events.on('toggleBlackout', function () { 57 | utils.toggleClass(self.containerElement, 'remark-blackout-mode'); 58 | }); 59 | 60 | events.on('hideOverlay', function () { 61 | utils.removeClass(self.containerElement, 'remark-blackout-mode'); 62 | utils.removeClass(self.containerElement, 'remark-help-mode'); 63 | }); 64 | 65 | events.on('pause', function () { 66 | utils.toggleClass(self.containerElement, 'remark-pause-mode'); 67 | }); 68 | 69 | events.on('resume', function () { 70 | utils.toggleClass(self.containerElement, 'remark-pause-mode'); 71 | }); 72 | 73 | handleFullscreen(self); 74 | } 75 | 76 | function handleFullscreen(self) { 77 | var requestFullscreen = utils.getPrefixedProperty(self.containerElement, 'requestFullScreen') 78 | , cancelFullscreen = utils.getPrefixedProperty(document, 'cancelFullScreen') 79 | ; 80 | 81 | self.events.on('toggleFullscreen', function () { 82 | var fullscreenElement = utils.getPrefixedProperty(document, 'fullscreenElement') || 83 | utils.getPrefixedProperty(document, 'fullScreenElement'); 84 | 85 | if (!fullscreenElement && requestFullscreen) { 86 | requestFullscreen.call(self.containerElement, Element.ALLOW_KEYBOARD_INPUT); 87 | } 88 | else if (cancelFullscreen) { 89 | cancelFullscreen.call(document); 90 | } 91 | self.scaleElements(); 92 | }); 93 | } 94 | 95 | SlideshowView.prototype.isEmbedded = function () { 96 | return this.containerElement !== this.dom.getBodyElement(); 97 | }; 98 | 99 | SlideshowView.prototype.configureContainerElement = function (element) { 100 | var self = this; 101 | 102 | self.containerElement = element; 103 | 104 | utils.addClass(element, 'remark-container'); 105 | 106 | if (element === self.dom.getBodyElement()) { 107 | utils.addClass(self.dom.getHTMLElement(), 'remark-container'); 108 | 109 | forwardEvents(self.events, window, [ 110 | 'hashchange', 'resize', 'keydown', 'keypress', 'mousewheel', 'message' 111 | ]); 112 | forwardEvents(self.events, self.containerElement, [ 113 | 'touchstart', 'touchmove', 'touchend', 'click', 'contextmenu' 114 | ]); 115 | } 116 | else { 117 | element.style.position = 'absolute'; 118 | element.tabIndex = -1; 119 | 120 | forwardEvents(self.events, window, ['resize']); 121 | forwardEvents(self.events, element, [ 122 | 'keydown', 'keypress', 'mousewheel', 123 | 'touchstart', 'touchmove', 'touchend' 124 | ]); 125 | } 126 | 127 | // Tap event is handled in slideshow view 128 | // rather than controller as knowledge of 129 | // container width is needed to determine 130 | // whether to move backwards or forwards 131 | self.events.on('tap', function (endX) { 132 | if (endX < self.getContainerWidth() / 2) { 133 | self.slideshow.gotoPreviousSlide(); 134 | } 135 | else { 136 | self.slideshow.gotoNextSlide(); 137 | } 138 | }); 139 | }; 140 | 141 | function forwardEvents (target, source, events) { 142 | events.forEach(function (eventName) { 143 | source.addEventListener(eventName, function () { 144 | var args = Array.prototype.slice.call(arguments); 145 | target.emit.apply(target, [eventName].concat(args)); 146 | }); 147 | }); 148 | } 149 | 150 | SlideshowView.prototype.configureChildElements = function () { 151 | var self = this; 152 | 153 | self.containerElement.innerHTML += resources.containerLayout; 154 | 155 | self.elementArea = self.containerElement.getElementsByClassName('remark-slides-area')[0]; 156 | self.previewArea = self.containerElement.getElementsByClassName('remark-preview-area')[0]; 157 | self.notesArea = self.containerElement.getElementsByClassName('remark-notes-area')[0]; 158 | 159 | self.notesView = new NotesView (self.events, self.notesArea, function () { 160 | return self.slideViews; 161 | }); 162 | 163 | self.backdropElement = self.containerElement.getElementsByClassName('remark-backdrop')[0]; 164 | self.helpElement = self.containerElement.getElementsByClassName('remark-help')[0]; 165 | 166 | self.timerElement = self.notesArea.getElementsByClassName('remark-toolbar-timer')[0]; 167 | self.pauseElement = self.containerElement.getElementsByClassName('remark-pause')[0]; 168 | 169 | self.events.on('propertiesChanged', function (changes) { 170 | if (changes.hasOwnProperty('ratio')) { 171 | self.updateDimensions(); 172 | } 173 | }); 174 | 175 | self.events.on('resize', onResize); 176 | 177 | if (window.matchMedia) { 178 | window.matchMedia('print').addListener(function (e) { 179 | if (e.matches) { 180 | self.slideViews.forEach(function (slideView) { 181 | slideView.scale({ 182 | clientWidth: 908, 183 | clientHeight: 681 184 | }); 185 | }); 186 | } 187 | }); 188 | } 189 | 190 | function onResize () { 191 | self.scaleElements(); 192 | } 193 | }; 194 | 195 | SlideshowView.prototype.updateSlideViews = function () { 196 | var self = this; 197 | 198 | self.slideViews.forEach(function (slideView) { 199 | self.elementArea.removeChild(slideView.containerElement); 200 | }); 201 | 202 | self.slideViews = self.slideshow.getSlides().map(function (slide) { 203 | return new SlideView(self.events, self.slideshow, self.scaler, slide); 204 | }); 205 | 206 | self.slideViews.forEach(function (slideView) { 207 | self.elementArea.appendChild(slideView.containerElement); 208 | }); 209 | 210 | self.updateDimensions(); 211 | 212 | if (self.slideshow.getCurrentSlideNo() > 0) { 213 | self.showSlide(self.slideshow.getCurrentSlideNo() - 1); 214 | } 215 | }; 216 | 217 | SlideshowView.prototype.scaleSlideBackgroundImages = function (dimensions) { 218 | var self = this; 219 | 220 | self.slideViews.forEach(function (slideView) { 221 | slideView.scaleBackgroundImage(dimensions); 222 | }); 223 | }; 224 | 225 | SlideshowView.prototype.showSlide = function (slideIndex) { 226 | var self = this 227 | , slideView = self.slideViews[slideIndex] 228 | , nextSlideView = self.slideViews[slideIndex + 1] 229 | ; 230 | 231 | self.events.emit("beforeShowSlide", slideIndex); 232 | 233 | slideView.show(); 234 | 235 | if (nextSlideView) { 236 | self.previewArea.innerHTML = nextSlideView.containerElement.outerHTML; 237 | } 238 | else { 239 | self.previewArea.innerHTML = ''; 240 | } 241 | 242 | self.events.emit("afterShowSlide", slideIndex); 243 | }; 244 | 245 | SlideshowView.prototype.hideSlide = function (slideIndex) { 246 | var self = this 247 | , slideView = self.slideViews[slideIndex] 248 | ; 249 | 250 | self.events.emit("beforeHideSlide", slideIndex); 251 | slideView.hide(); 252 | self.events.emit("afterHideSlide", slideIndex); 253 | 254 | }; 255 | 256 | SlideshowView.prototype.updateDimensions = function () { 257 | var self = this 258 | , dimensions = self.scaler.dimensions 259 | ; 260 | 261 | self.helpElement.style.width = dimensions.width + 'px'; 262 | self.helpElement.style.height = dimensions.height + 'px'; 263 | 264 | self.scaleSlideBackgroundImages(dimensions); 265 | self.scaleElements(); 266 | }; 267 | 268 | SlideshowView.prototype.scaleElements = function () { 269 | var self = this; 270 | 271 | self.slideViews.forEach(function (slideView) { 272 | slideView.scale(self.elementArea); 273 | }); 274 | 275 | if (self.previewArea.children.length) { 276 | self.scaler.scaleToFit(self.previewArea.children[0].children[0], self.previewArea); 277 | } 278 | self.scaler.scaleToFit(self.helpElement, self.containerElement); 279 | self.scaler.scaleToFit(self.pauseElement, self.containerElement); 280 | }; 281 | -------------------------------------------------------------------------------- /src/remark.less: -------------------------------------------------------------------------------- 1 | /*************/ 2 | /* Container */ 3 | /*************/ 4 | 5 | html.remark-container, body.remark-container { 6 | height: 100%; 7 | width: 100%; 8 | -webkit-print-color-adjust: exact; 9 | } 10 | .remark-container { 11 | background: #d7d8d2; 12 | margin: 0; 13 | overflow: hidden; 14 | } 15 | .remark-container:focus { 16 | outline-style: solid; 17 | outline-width: 1px; 18 | } 19 | :-webkit-full-screen .remark-container { 20 | width: 100%; 21 | height: 100%; 22 | } 23 | 24 | /**********/ 25 | /* Slides */ 26 | /**********/ 27 | 28 | .remark-slides-area { 29 | position: relative; 30 | height: 100%; 31 | width: 100%; 32 | } 33 | .remark-slide-container { 34 | display: none; 35 | position: absolute; 36 | height: 100%; 37 | width: 100%; 38 | page-break-after: always; 39 | } 40 | .remark-slide-scaler { 41 | background-color: transparent; 42 | overflow: hidden; 43 | position: absolute; 44 | -webkit-transform-origin: top left; 45 | -moz-transform-origin: top left; 46 | transform-origin: top-left; 47 | -moz-box-shadow: 0 0 30px #888; 48 | -webkit-box-shadow: 0 0 30px #888; 49 | box-shadow: 0 0 30px #888; 50 | } 51 | .remark-slide { 52 | height: 100%; 53 | width: 100%; 54 | display: table; 55 | table-layout: fixed; 56 | 57 | > .left { 58 | text-align: left; 59 | } 60 | 61 | > .center { 62 | text-align: center; 63 | } 64 | 65 | > .right { 66 | text-align: right; 67 | } 68 | 69 | > .top { 70 | vertical-align: top; 71 | } 72 | 73 | > .middle { 74 | vertical-align: middle; 75 | } 76 | 77 | > .bottom { 78 | vertical-align: bottom; 79 | } 80 | } 81 | 82 | .remark-slide-content { 83 | background-color: #fff; 84 | background-position: center; 85 | background-repeat: no-repeat; 86 | display: table-cell; 87 | font-size: 20px; 88 | padding: 1em 4em 1em 4em; 89 | 90 | h1 { font-size: 55px; } 91 | h2 { font-size: 45px; } 92 | h3 { font-size: 35px; } 93 | 94 | .left { 95 | display: block; 96 | text-align: left; 97 | } 98 | 99 | .center { 100 | display: block; 101 | text-align: center; 102 | } 103 | 104 | .right { 105 | display: block; 106 | text-align: right; 107 | } 108 | } 109 | 110 | .remark-slide-number { 111 | bottom: 12px; 112 | opacity: 0.5; 113 | position: absolute; 114 | right: 20px; 115 | } 116 | 117 | .remark-code { 118 | font-size: 18px; 119 | } 120 | .remark-code-line { 121 | min-height: 1em; 122 | } 123 | .remark-code-line-highlighted { 124 | background-color: rgba(255, 255, 0, 0.5); 125 | } 126 | .remark-code-span-highlighted { 127 | background-color: rgba(255, 255, 0, 0.5); 128 | padding: 1px 2px 2px 2px; 129 | } 130 | 131 | .remark-visible { 132 | display: block; 133 | z-index: 2; 134 | } 135 | .remark-fading { 136 | display: block; 137 | z-index: 1; 138 | .remark-slide-scaler { 139 | -moz-box-shadow: none; 140 | -webkit-box-shadow: none; 141 | box-shadow: none; 142 | } 143 | } 144 | 145 | /************/ 146 | /* Backdrop */ 147 | /************/ 148 | 149 | .remark-backdrop { 150 | position: absolute; 151 | top: 0; 152 | bottom: 0; 153 | left: 0; 154 | right: 0; 155 | display: none; 156 | background: #000; 157 | z-index: 2; 158 | } 159 | 160 | /*****************/ 161 | /* Pause overlay */ 162 | /*****************/ 163 | 164 | .remark-pause { 165 | bottom: 0; 166 | top: 0; 167 | right: 0; 168 | left: 0; 169 | display: none; 170 | position: absolute; 171 | z-index: 1000; 172 | 173 | .remark-pause-lozenge { 174 | margin-top: 30%; 175 | text-align: center; 176 | 177 | span { 178 | color: white; 179 | background: black; 180 | border: 2px solid black; 181 | border-radius: 20px; 182 | padding: 20px 30px; 183 | font-family: Helvetica, arial, freesans, clean, sans-serif; 184 | font-size: 42pt; 185 | font-weight: bold; 186 | } 187 | } 188 | } 189 | 190 | .remark-container.remark-presenter-mode.remark-pause-mode { 191 | .remark-pause { 192 | display: block; 193 | } 194 | .remark-backdrop { 195 | display: block; 196 | opacity: 0.5; 197 | } 198 | } 199 | 200 | /********/ 201 | /* Help */ 202 | /********/ 203 | 204 | .remark-help { 205 | bottom: 0; 206 | top: 0; 207 | right: 0; 208 | left: 0; 209 | display: none; 210 | position: absolute; 211 | z-index: 1000; 212 | -webkit-transform-origin: top left; 213 | -moz-transform-origin: top left; 214 | transform-origin: top-left; 215 | 216 | .remark-help-content { 217 | color: white; 218 | font-family: Helvetica, arial, freesans, clean, sans-serif; 219 | font-size: 12pt; 220 | position: absolute; 221 | top: 5%; 222 | bottom: 10%; 223 | height: 10%; 224 | left: 5%; 225 | width: 90%; 226 | 227 | h1 { 228 | font-size: 36px; 229 | } 230 | 231 | td { 232 | color: white; 233 | font-size: 12pt; 234 | padding: 10px; 235 | } 236 | td:first-child { 237 | padding-left: 0; 238 | } 239 | .key { 240 | background: white; 241 | color: black; 242 | min-width: 1em; 243 | display: inline-block; 244 | padding: 3px 6px; 245 | text-align: center; 246 | border-radius: 4px; 247 | font-size: 14px, 248 | } 249 | } 250 | 251 | .dismiss { 252 | top: 85%; 253 | } 254 | } 255 | 256 | .remark-container.remark-help-mode { 257 | .remark-help { 258 | display: block; 259 | } 260 | .remark-backdrop { 261 | display: block; 262 | opacity: 0.95; 263 | } 264 | } 265 | 266 | /******************/ 267 | /* Presenter mode */ 268 | /******************/ 269 | 270 | .remark-preview-area { 271 | bottom: 2%; 272 | left: 2%; 273 | display: none; 274 | opacity: 0.5; 275 | position: absolute; 276 | height: 47.25%; 277 | width: 48%; 278 | 279 | .remark-slide-container { 280 | display: block; 281 | } 282 | } 283 | 284 | .remark-notes-area { 285 | background: #e7e8e2; 286 | bottom: 0; 287 | display: none; 288 | left: 52%; 289 | overflow: hidden; 290 | position: absolute; 291 | right: 0; 292 | top: 0; 293 | 294 | .remark-top-area { 295 | height: 50px; 296 | left: 20px; 297 | position: absolute; 298 | right: 10px; 299 | top: 10px; 300 | } 301 | 302 | .remark-bottom-area { 303 | position: absolute; 304 | top: 75px; 305 | bottom: 10px; 306 | left: 20px; 307 | right: 10px; 308 | 309 | .remark-toggle { 310 | display: block; 311 | text-decoration: none; 312 | font-family: Helvetica,arial,freesans,clean,sans-serif; 313 | border-bottom: 1px solid #ccc; 314 | height: 21px; 315 | font-size: 0.75em; 316 | font-weight: bold; 317 | text-transform: uppercase; 318 | color: #666; 319 | text-shadow: #f5f5f5 1px 1px 1px; 320 | } 321 | 322 | .remark-notes-current-area { 323 | height: 70%; 324 | position: relative; 325 | 326 | .remark-notes { 327 | clear:both; 328 | border-top: 1px solid #f5f5f5; 329 | position: absolute; 330 | top: 22px; 331 | bottom: 0px; 332 | left: 0px; 333 | right: 0px; 334 | overflow-y: auto; 335 | margin-bottom: 20px; 336 | } 337 | } 338 | 339 | .remark-notes-preview-area { 340 | height: 30%; 341 | position: relative; 342 | 343 | .remark-notes-preview { 344 | border-top: 1px solid #f5f5f5; 345 | position: absolute; 346 | top: 22px; 347 | bottom: 0px; 348 | left: 0px; 349 | right: 0px; 350 | overflow-y: auto; 351 | } 352 | } 353 | 354 | .remark-notes > *:first-child, 355 | .remark-notes-preview > *:first-child { 356 | margin-top: 5px; 357 | } 358 | .remark-notes > *:last-child, 359 | .remark-notes-preview > *:last-child { 360 | margin-bottom: 0; 361 | } 362 | } 363 | } 364 | 365 | .remark-toolbar { 366 | color: #979892; 367 | vertical-align: middle; 368 | 369 | .remark-toolbar-link { 370 | border: 2px solid #d7d8d2; 371 | color: #979892; 372 | display: inline-block; 373 | padding: 2px 2px; 374 | text-decoration: none; 375 | text-align: center; 376 | min-width: 20px; 377 | 378 | &:hover { 379 | border-color: #979892; 380 | color: #676862; 381 | } 382 | } 383 | 384 | .remark-toolbar-timer { 385 | border: 2px solid black; 386 | border-radius: 10px; 387 | background: black; 388 | color: white; 389 | display: inline-block; 390 | float: right; 391 | padding: 5px 10px; 392 | font-family: sans-serif; 393 | font-weight: bold; 394 | font-size: 175%; 395 | text-decoration: none; 396 | text-align: center; 397 | } 398 | } 399 | 400 | .remark-container.remark-presenter-mode { 401 | .remark-slides-area { 402 | top: 2%; 403 | left: 2%; 404 | height: 47.25%; 405 | width: 48%; 406 | } 407 | .remark-preview-area { 408 | display: block; 409 | } 410 | .remark-notes-area { 411 | display: block; 412 | } 413 | } 414 | 415 | /************/ 416 | /* Blackout */ 417 | /************/ 418 | 419 | .remark-container.remark-blackout-mode:not(.remark-presenter-mode) { 420 | .remark-backdrop { 421 | display: block; 422 | opacity: 0.99; 423 | } 424 | } 425 | 426 | /************/ 427 | /* Printing */ 428 | /************/ 429 | 430 | @media print { 431 | .remark-container { 432 | overflow: visible; 433 | background-color: #fff; 434 | } 435 | .remark-slide-container { 436 | display: block; 437 | position: relative; 438 | } 439 | .remark-slide-scaler { 440 | -moz-box-shadow: none; 441 | -webkit-box-shadow: none; 442 | box-shadow: none; 443 | } 444 | } 445 | @page { 446 | size: 908px 681px; 447 | margin: 0; 448 | } 449 | --------------------------------------------------------------------------------