Added support for code-block delimiter and code fragment highlights.

This commit add support for a new code-block delimiter that provides a simple way to inject any source code file found within the repo as a Markdown code-block on a presentation slide. The delimiter syntax looks as follows:

—?code=path/to/source/code.file

This commit also introduces support for code fragment highlighting. The CF-marker syntax, one or more of which must follow a code-block in your markdown, looks as follows:

@[fragment-range](optional-note)

Where fragment-range identifies a single line number, @[1], or a range of numbers, @[5-10]. The optional note is plain text that is displayed on the slide below the code-block when the fragment is in focus.
This commit is contained in:
David Russell 2017-05-27 16:09:18 +07:00
parent 15a9a95369
commit 1f834dbf73
12 changed files with 632 additions and 17 deletions

View File

@ -51,6 +51,7 @@ public class Module extends AbstractModule {
bind(ImageService.class).asEagerSingleton();
bind(VideoService.class).asEagerSingleton();
bind(GISTService.class).asEagerSingleton();
bind(CodeService.class).asEagerSingleton();
bind(ShortcutsService.class).asEagerSingleton();
bind(WebService.class).asEagerSingleton();
bind(ComposableService.class).asEagerSingleton();

View File

@ -28,6 +28,7 @@ import com.gitpitch.utils.*;
import com.gitpitch.services.ImageService;
import com.gitpitch.services.VideoService;
import com.gitpitch.services.GISTService;
import com.gitpitch.services.CodeService;
import com.gitpitch.services.ShortcutsService;
import org.apache.commons.io.FilenameUtils;
import com.google.inject.assistedinject.Assisted;
@ -53,6 +54,7 @@ public class MarkdownModel implements Markdown {
private final ImageService imageService;
private final VideoService videoService;
private final GISTService gistService;
private final CodeService codeService;
private final ShortcutsService shortcutsService;
private final MarkdownRenderer mrndr;
private final String markdown;
@ -63,12 +65,14 @@ public class MarkdownModel implements Markdown {
public MarkdownModel(ImageService imageService,
VideoService videoService,
GISTService gistService,
CodeService codeService,
ShortcutsService shortcutsService,
@Nullable @Assisted MarkdownRenderer mrndr) {
this.imageService = imageService;
this.videoService = videoService;
this.gistService = gistService;
this.codeService = codeService;
this.shortcutsService = shortcutsService;
this.mrndr = mrndr;
@ -122,7 +126,7 @@ public class MarkdownModel implements Markdown {
}
/*
* Process text, image, video and gist content
* Process text, image, video, gist, and code content
* in PITCHME.md for online viewing.
*/
private String process(String md,
@ -175,11 +179,9 @@ public class MarkdownModel implements Markdown {
.toString();
} else if (gistDelimFound(md)) {
String gist = new StringBuffer(gistService.build(md,
pp, yOpts, this)).toString();
return gist;
return gistService.build(md, pp, yOpts, this);
} else if(codeDelimFound(md)) {
return codeService.build(md, pp, yOpts, gitRawBase, this);
}
if (yOpts != null && yOpts.hasImageBg()) {
@ -253,8 +255,10 @@ public class MarkdownModel implements Markdown {
String absTagLink = gitRawBase + tagLink;
return md.replace(tagLink, absTagLink);
}
} else if(shortcutsService.fragmentFound(md)) {
return shortcutsService.expandFragment(md);
} else if(shortcutsService.listFragmentFound(md)) {
return shortcutsService.expandListFragment(md);
} else if(shortcutsService.codeFragmentFound(md)) {
return shortcutsService.expandCodeFragment(md);
} else {
/*
@ -269,7 +273,7 @@ public class MarkdownModel implements Markdown {
}
/*
* Process text, image, video and gist content
* Process text, image, video, gist, and code content
* in PITCHME.md for offline viewing.
*/
public String offline(String md) {
@ -402,6 +406,10 @@ public class MarkdownModel implements Markdown {
return md.startsWith(horizGISTDelim()) || md.startsWith(vertGISTDelim());
}
private boolean codeDelimFound(String md) {
return md.startsWith(horizCodeDelim()) || md.startsWith(vertCodeDelim());
}
private String delimiter(String md) {
return (md.startsWith(hSlideDelim)) ? horizDelim() : vertDelim();
}
@ -579,6 +587,10 @@ public class MarkdownModel implements Markdown {
return isHorizontal(md) ? horizGISTDelim() : vertGISTDelim();
}
public String extractCodeDelim(String md) {
return isHorizontal(md) ? horizCodeDelim() : vertCodeDelim();
}
/*
* Generate a key for querying the cache for matching MarkdownModel.
*/
@ -629,6 +641,14 @@ public class MarkdownModel implements Markdown {
return vertDelim() + MD_VSLIDE_GIST;
}
public String horizCodeDelim() {
return horizDelim() + MD_HSLIDE_CODE;
}
public String vertCodeDelim() {
return vertDelim() + MD_VSLIDE_CODE;
}
/*
* The initial releases of GitPitch used #HSLIDE and #VSLIDE
* as default delimiters. To provide backwards compatability
@ -677,8 +697,14 @@ public class MarkdownModel implements Markdown {
public static final String MD_CLOSER = "\" -->";
public static final String MD_SPACER = "\n";
public static final String DATA_IMAGE_ATTR = "data-background-image=";
public static final String MD_FRAG_OPEN = "- ";
public static final String MD_FRAG_CLOSE = "|";
public static final String MD_LIST_FRAG_OPEN = "- ";
public static final String MD_LIST_FRAG_CLOSE = "|";
public static final String MD_CODE_FRAG_OPEN = "@[";
public static final String MD_CODE_FRAG_CLOSE = "]";
public static final String MD_CODE_FRAG_NOTE_OPEN = "(";
public static final String MD_CODE_FRAG_NOTE_CLOSE = ")";
public static final String MD_CODE_BLOCK_OPEN = "```";
public static final String MD_CODE_BLOCK_CLOSE = "```";
private static final String MD_HSLIDE_IMAGE = "?image=";
private static final String MD_VSLIDE_IMAGE = "?image=";
@ -686,6 +712,8 @@ public class MarkdownModel implements Markdown {
private static final String MD_VSLIDE_VIDEO = "?video=";
private static final String MD_HSLIDE_GIST = "?gist=";
private static final String MD_VSLIDE_GIST = "?gist=";
private static final String MD_HSLIDE_CODE = "?code=";
private static final String MD_VSLIDE_CODE = "?code=";
private static final String SLASH = "/";
private static final String PARAM_BRANCH = "?b=";

View File

@ -40,6 +40,7 @@ public final class Dependencies {
private final String fontawesomeVersion;
private final String octiconsVersion;
private final String highlightjsVersion;
private final Boolean highlightPluginEnabled;
@Inject
public Dependencies(Configuration cfg) {
@ -51,6 +52,8 @@ public final class Dependencies {
this.fontawesomeVersion = cfg.getString("gitpitch.dependency.fontawesome");
this.octiconsVersion = cfg.getString("gitpitch.dependency.octicons");
this.highlightjsVersion = cfg.getString("gitpitch.dependency.highlightjs");
this.highlightPluginEnabled =
cfg.getBoolean("gitpitch.dependency.highlight.plugin", false);
}
public String revealjs(boolean offline, String versionOverride) {
@ -86,6 +89,10 @@ public final class Dependencies {
return build(offline, GIPITCHIMG);
}
public Boolean highlightPluginEnabled() {
return highlightPluginEnabled;
}
private String build(boolean offline, String libName) {
if(offline) {

View File

@ -0,0 +1,153 @@
/*
* MIT License
*
* Copyright (c) 2016 David Russell
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.gitpitch.services;
import com.gitpitch.models.MarkdownModel;
import com.gitpitch.git.GRS;
import com.gitpitch.git.GRSService;
import com.gitpitch.git.GRSManager;
import com.gitpitch.services.DiskService;
import com.gitpitch.utils.PitchParams;
import com.gitpitch.utils.YAMLOptions;
import java.util.*;
import java.nio.file.*;
import javax.inject.*;
import play.Logger;
import play.Logger.ALogger;
/*
* PITCHME.md code support service.
*/
@Singleton
public class CodeService {
private final Logger.ALogger log = Logger.of(this.getClass());
private final GRSManager grsManager;
private final DiskService diskService;
@Inject
public CodeService(GRSManager grsManager,
DiskService diskService) {
this.grsManager = grsManager;
this.diskService = diskService;
}
public String build(String md,
PitchParams pp,
YAMLOptions yOpts,
String gitRawBase,
MarkdownModel mdm) {
String codeBlock = mdm.extractCodeDelim(md);
try {
String codePath = extractCodePath(md, gitRawBase, mdm);
GRS grs = grsManager.get(pp);
GRSService grsService = grsManager.getService(grs);
int downStatus =
grsService.download(pp, SOURCE_CODE, codePath);
if(downStatus == 0) {
String code = diskService.asText(pp, SOURCE_CODE);
return buildCodeBlock(mdm.extractCodeDelim(md), code);
} else {
return buildCodeBlockError(mdm.extractCodeDelim(md),
extractPath(md, mdm));
}
} catch (Exception gex) {}
return codeBlock;
}
public String extractCodePath(String md,
String gitRawBase,
MarkdownModel mdm) {
String codePath = null;
try {
String delim = mdm.extractCodeDelim(md);
codePath = md.substring(delim.length());
if (!mdm.linkAbsolute(codePath)) {
codePath =
new StringBuffer(gitRawBase).append(codePath)
.toString();
}
} catch (Exception pex) {
log.warn("extractCodePath: ex={}", pex);
}
return codePath;
}
private String extractPath(String md, MarkdownModel mdm) {
String path = null;
try {
String delim = mdm.extractCodeDelim(md);
path = md.substring(delim.length());
} catch (Exception pex) {
log.warn("extractPath: ex={}", pex);
}
return path;
}
private String buildCodeBlock(String delim, String code) {
return new StringBuffer(delim)
.append(MarkdownModel.MD_SPACER)
.append(MarkdownModel.MD_CODE_BLOCK_OPEN)
.append(MarkdownModel.MD_SPACER)
.append(code)
.append(MarkdownModel.MD_SPACER)
.append(MarkdownModel.MD_CODE_BLOCK_CLOSE)
.append(MarkdownModel.MD_SPACER)
.toString();
}
private String buildCodeBlockError(String delim, String codePath) {
return new StringBuffer(delim)
.append(MarkdownModel.MD_SPACER)
.append(SOURCE_CODE_DELIMITER)
.append(MarkdownModel.MD_SPACER)
.append(codePath)
.append(MarkdownModel.MD_SPACER)
.append(SOURCE_CODE_NOT_FOUND)
.append(MarkdownModel.MD_SPACER)
.toString();
}
private static final String SOURCE_CODE = "PITCHME.code";
private static final String SOURCE_CODE_DELIMITER =
"### Code Block Delimiter";
private static final String SOURCE_CODE_NOT_FOUND =
"### Source File Not Found";
}

View File

@ -91,6 +91,19 @@ public class DiskService {
return asPath(pp, filename).toFile();
}
/*
* Return text contents for file within PitchParams branch working directory.
*/
public String asText(PitchParams pp,
String filename) {
String text = "";
try {
Path filePath = asPath(pp, filename);
text = new String(Files.readAllBytes(filePath));
} catch(Exception tex) {}
return text;
}
/*
* Ensure PitchParams branch working directory exists.
*/

View File

@ -40,12 +40,23 @@ public class ShortcutsService {
private final Logger.ALogger log = Logger.of(this.getClass());
public boolean fragmentFound(String md) {
public boolean listFragmentFound(String md) {
boolean found = false;
if(md != null) {
String trimmed = md.trim();
if(trimmed.startsWith(MarkdownModel.MD_FRAG_OPEN) &&
trimmed.endsWith(MarkdownModel.MD_FRAG_CLOSE)) {
if(trimmed.startsWith(MarkdownModel.MD_LIST_FRAG_OPEN) &&
trimmed.endsWith(MarkdownModel.MD_LIST_FRAG_CLOSE)) {
found = true;
}
}
return found;
}
public boolean codeFragmentFound(String md) {
boolean found = false;
if(md != null) {
String trimmed = md.trim();
if(trimmed.startsWith(MarkdownModel.MD_CODE_FRAG_OPEN)) {
found = true;
}
}
@ -53,14 +64,69 @@ public class ShortcutsService {
}
/*
* Expand shortcut syntax for fragment with fully expanded
* Expand shortcut syntax for list fragment with fully expanded
* HTML comment syntax.
*/
public String expandFragment(String md) {
int fragCloseIdx = md.lastIndexOf(MarkdownModel.MD_FRAG_CLOSE);
public String expandListFragment(String md) {
int fragCloseIdx = md.lastIndexOf(MarkdownModel.MD_LIST_FRAG_CLOSE);
return md.substring(0, fragCloseIdx) + FRAGMENT;
}
/*
* Expand shortcut syntax for code fragment with fully expanded
* HTML span syntax with data-code-focus class.
*/
public String expandCodeFragment(String md) {
try {
String codeFragRange = null;
String codeFragNote = null;
int codeFragStart =
md.indexOf(MarkdownModel.MD_CODE_FRAG_OPEN);
int codeFragEnd =
md.indexOf(MarkdownModel.MD_CODE_FRAG_CLOSE);
if(codeFragEnd > codeFragStart) {
codeFragRange =
md.substring(codeFragStart+2, codeFragEnd);
}
int codeFragNoteStart =
md.indexOf(MarkdownModel.MD_CODE_FRAG_NOTE_OPEN);
int codeFragNoteEnd =
md.indexOf(MarkdownModel.MD_CODE_FRAG_NOTE_CLOSE);
if(codeFragNoteEnd > codeFragNoteStart) {
codeFragNote =
md.substring(codeFragNoteStart+1, codeFragNoteEnd);
}
if(codeFragRange != null) {
md = buildCodeFragment(codeFragRange, codeFragNote);
}
} catch(Exception cfex) {
} finally {
return md;
}
}
/*
* Construct a HTML fragment for a code fragment based on the
* reveal-code-focus plugin syntax:
* <span class="fragment current-only" data-code-focus="1-9">Note</span>
*/
private String buildCodeFragment(String range, String note) {
if(note == null) note = "";
return new StringBuffer(HTML_CODE_FRAG_OPEN)
.append(range)
.append(HTML_CODE_FRAG_CLOSE)
.append(note)
.append(HTML_CODE_FRAG_NOTE_CLOSE)
.toString();
}
/*
* Note, the space before opening bracket is intentional
* so tag injection can happen directly alongside other
@ -68,4 +134,8 @@ public class ShortcutsService {
*/
private static final String FRAGMENT =
" <!-- .element: class=\"fragment\" -->";
private static final String HTML_CODE_FRAG_OPEN =
"<span class=\"fragment current-only\" data-code-focus=\"";
private static final String HTML_CODE_FRAG_CLOSE = "\">";
private static final String HTML_CODE_FRAG_NOTE_CLOSE = "</span>";
}

View File

@ -22,6 +22,7 @@
<link href="@deps.octicons(offline)/octicons.css" rel="stylesheet" type="text/css"/>
<link href="@deps.fontawesome(offline)/css/font-awesome.min.css" rel="stylesheet" type="text/css"/>
@SlideshowStyle()
@SlideshowCodeFragHighlightStyle()
@if(ssm.hasThemeOverride()) {
<style>
@Html(ssm.fetchThemeOverride())

View File

@ -0,0 +1,23 @@
<style>
.reveal .slides section .fragment.current-only {
opacity: 1;
visibility: visible;
display: none;
}
.reveal .slides section .fragment.current-only.current-fragment {
display: block;
}
.line { display: block; }
.line.focus { background: #fdf6e3; color: #657b83; }
.line.focus .hljs-comment, .line.focus .hljs-quote { color: #93a1a1; }
.line.focus .hljs-keyword, .line.focus .hljs-selector-tag, .line.focus .hljs-addition { color: #859900; }
.line.focus .hljs-number, .line.focus .hljs-string, .line.focus .hljs-meta .hljs-meta-string, .line.focus .hljs-literal, .line.focus .hljs-doctag, .line.focus .hljs-regexp { color: #2aa198; }
.line.focus .hljs-title, .line.focus .hljs-section, .line.focus .hljs-name, .line.focus .hljs-selector-id, .line.focus .hljs-selector-class { color: #268bd2; }
.line.focus .hljs-attribute, .line.focus .hljs-attr, .line.focus .hljs-variable, .line.focus .hljs-template-variable, .line.focus .hljs-class .hljs-title, .line.focus .hljs-type { color: #b58900; }
.line.focus .hljs-symbol, .line.focus .hljs-bullet, .line.focus .hljs-subst, .line.focus .hljs-meta, .line.focus .hljs-meta .hljs-keyword, .line.focus .hljs-selector-attr, .line.focus .hljs-selector-pseudo, .line.focus .hljs-link { color: #cb4b16; }
.line.focus .hljs-built_in, .line.focus .hljs-deletion { color: #dc322f; }
.line.focus .hljs-formula { background: #eee8d5; }
.line.focus .hljs-emphasis { font-style: italic; }
.line.focus .hljs-strong { font-weight: bold; }
.yellow-slide .line.focus:nth-child(2) { background: yellow; }
</style>

View File

@ -34,7 +34,16 @@
@if(offline) {
{ src: "@deps.revealjs(offline, ssm.fetchRevealVersionOverride())/plugin/notes/notes.js", async: true },
}
@if(deps.highlightPluginEnabled()) {
{ src: "@deps.revealjs(offline, ssm.fetchRevealVersionOverride())/plugin/highlight/highlight.js", async: true, callback: function() { hljs.initHighlightingOnLoad(); } },
} else {
{ src: '@deps.highlightjs(offline)/highlight.js' },
{ src: '@deps.highlightjs(offline)/reveal-code-focus.js',
callback: function() {
RevealCodeFocus();
}
},
}
@if(ssm.mathEnabled()) {
{ src: "@deps.revealjs(offline, ssm.fetchRevealVersionOverride())/plugin/math/math.js", async: true }
}

View File

@ -409,6 +409,9 @@ gitpitch {
fontawesome = "4.6.3"
octicons = "3.5.0"
highlightjs = "9.6.0"
highlight {
plugin = false
}
}
github {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,229 @@
/*!
* reveal-code-focus 1.0.0
* Copyright 2015-2017 Benjamin Tan <https://demoneaux.github.io/>
* Available under MIT license <https://github.com/demoneaux/reveal-code-focus/blob/master/LICENSE>
*/
;(function(window, Reveal, hljs) {
if (typeof window.RevealCodeFocus == 'function') {
return;
}
var currentSlide, currentFragments, scrollToFocused = true, prevSlideData = null;
// Iterates through `array`, running `callback` for each `array` element.
function forEach(array, callback) {
var i = -1, length = array ? array.length : 0;
while (++i < length) {
callback(array[i]);
}
}
function indexOf(array, elem) {
var i = -1, length = array ? array.length : 0;
while (++i < length) {
if (array[i] === elem) {
return i;
}
}
}
function initialize(e) {
// Initialize code only once.
// TODO: figure out why `initialize` is being called twice.
if (initialize.ran) {
return;
}
initialize.ran = true;
// TODO: mark as parsed.
forEach(document.querySelectorAll('pre code'), function(element) {
// Trim whitespace if the `data-trim` attribute is present.
if (element.hasAttribute('data-trim') && typeof element.innerHTML.trim == 'function') {
element.innerHTML = element.innerHTML.trim();
}
// Highlight code using highlight.js.
hljs.highlightBlock(element);
// Split highlighted code into lines.
var openTags = [], reHtmlTag = /<(\/?)span(?:\s+(?:class=(['"])hljs-.*?\2)?\s*|\s*)>/g;
element.innerHTML = element.innerHTML.replace(/(.*?)\r?\n/g, function(_, string) {
if (!string) {
return '<span class=line>&nbsp;</span>';
}
var openTag, stringPrepend;
// Re-open all tags that were previously closed.
if (openTags.length) {
stringPrepend = openTags.join('');
}
// Match all HTML `<span>` tags.
reHtmlTag.lastIndex = 0;
while (openTag = reHtmlTag.exec(string)) {
// If it is a closing tag, remove the opening tag from the list.
if (openTag[1]) {
openTags.pop();
}
// Otherwise if it is an opening tag, push it to the list.
else {
openTags.push(openTag[0]);
}
}
// Close all opened tags, so that strings can be wrapped with `span.line`.
if (openTags.length) {
string += Array(openTags.length + 1).join('</span>');
}
if (stringPrepend) {
string = stringPrepend + string;
}
return '<span class=line>' + string + '</span>';
});
});
Reveal.addEventListener('slidechanged', updateCurrentSlide);
Reveal.addEventListener('fragmentshown', function(e) {
focusFragment(e.fragment);
});
// TODO: make this configurable.
// When a fragment is hidden, clear the current focused fragment,
// and focus on the previous fragment.
Reveal.addEventListener('fragmenthidden', function(e) {
var index = indexOf(currentFragments, e.fragment);
focusFragment(currentFragments[index - 1]);
});
updateCurrentSlide(e);
}
initialize.ran = false;
function updateCurrentSlide(e) {
currentSlide = e.currentSlide;
currentFragments = currentSlide.getElementsByClassName('fragment');
clearPreviousFocus();
// If moving back to a previous slide…
if (
currentFragments.length &&
prevSlideData &&
(
prevSlideData.indexh > e.indexh ||
(prevSlideData.indexh == e.indexh && prevSlideData.indexv > e.indexv)
)
) {
// …return to the last fragment and highlight the code.
while (Reveal.nextFragment()) {}
var currentFragment = currentFragments[currentFragments.length - 1];
currentFragment.classList.add('current-fragment');
focusFragment(currentFragment);
}
// Update previous slide information.
prevSlideData = {
'indexh': e.indexh,
'indexv': e.indexv
};
}
// Removes any previously focused lines.
function clearPreviousFocus() {
forEach(currentSlide.querySelectorAll('pre code .line.focus'), function(line) {
line.classList.remove('focus');
});
}
function focusFragment(fragment) {
clearPreviousFocus();
if (!fragment) {
return;
}
var lines = fragment.getAttribute('data-code-focus');
if (!lines) {
return;
}
var codeBlock = parseInt(fragment.getAttribute('data-code-block'));
if (isNaN(codeBlock)) {
codeBlock = 1;
}
var preElems = currentSlide.querySelectorAll('pre');
if (!preElems.length) {
return;
}
var pre = preElems[codeBlock - 1];
var code = pre.querySelectorAll('code .line');
if (!code.length) {
return;
}
var topLineNumber, bottomLineNumber;
forEach(lines.split(','), function(line) {
lines = line.split('-');
if (lines.length == 1) {
focusLine(lines[0]);
} else {
var i = lines[0] - 1, j = lines[1];
while (++i <= j) {
focusLine(i);
}
}
});
function focusLine(lineNumber) {
// Convert from 1-based index to 0-based index.
lineNumber -= 1;
var line = code[lineNumber];
if (!line) {
return;
}
line.classList.add('focus');
if (scrollToFocused) {
if (topLineNumber == null) {
topLineNumber = bottomLineNumber = lineNumber;
} else {
if (lineNumber < topLineNumber) {
topLineNumber = lineNumber;
}
if (lineNumber > bottomLineNumber) {
bottomLineNumber = lineNumber;
}
}
}
}
if (scrollToFocused && topLineNumber != null) {
var topLine = code[topLineNumber];
var bottomLine = code[bottomLineNumber];
var codeParent = topLine.parentNode;
var scrollTop = topLine.offsetTop;
var scrollBottom = bottomLine.offsetTop + bottomLine.clientHeight;
codeParent.scrollTop = scrollTop - (codeParent.clientHeight - (scrollBottom - scrollTop)) / 2;
}
}
function RevealCodeFocus(options) {
options || (options = {
'scrollToFocused': true
});
if (options.scrollToFocused != null) {
scrollToFocused = options.scrollToFocused;
}
if (Reveal.isReady()) {
initialize({ 'currentSlide': Reveal.getCurrentSlide() });
} else {
Reveal.addEventListener('ready', initialize);
}
}
window.RevealCodeFocus = RevealCodeFocus;
}(this, this.Reveal, this.hljs));