Rewrite quickpreview to use strategy concept, add PrismJS for code file types

This commit is contained in:
Ben Gotow 2019-01-06 13:10:58 -08:00
parent b7939d38bd
commit 80dcc9af73
6 changed files with 351 additions and 61 deletions

View file

@ -131,6 +131,7 @@ export class AttachmentItem extends Component {
_onAttachmentKeyDown = event => {
if (event.key === SPACE && this.props.filePreviewPath) {
event.preventDefault();
event.stopPropagation();
Actions.quickPreviewFile(this.props.filePath);
}
if (event.key === 'Escape') {

View file

@ -11,8 +11,7 @@ const ThumbnailWidth = 320 * (11 / 8.5);
const QuicklookIsAvailable = process.platform === 'darwin';
const PDFJSRoot = path.join(__dirname, 'pdfjs-2.0.943');
// TODO make this list more exhaustive
const QuicklookNonPreviewableExtensions = [
const QuicklookBlacklist = [
'jpg',
'bmp',
'gif',
@ -27,31 +26,75 @@ const QuicklookNonPreviewableExtensions = [
'ics',
];
const CrossplatformPreviewableExtensions = [
// pdfjs
'pdf',
const CrossplatformStrategies = {
pdfjs: ['pdf'],
mammoth: ['docx'],
snarkdown: ['md'],
xlsx: [
'xls',
'xlsx',
'csv',
'eth',
'ods',
'fods',
'uos1',
'uos2',
'dbf',
'txt',
'prn',
'xlw',
'xlsb',
],
prism: [
'html',
'svg',
'xml',
'css',
'c',
'cc',
'cpp',
'js',
'jsx',
'tsx',
'ts',
'go',
'cs',
'patch',
'swift',
'java',
'json',
'jsonp',
'tex',
'mm',
'm',
'h',
'py',
'rb',
'rs',
'sql',
'yaml',
'txt',
'log',
],
};
// Mammoth
'docx',
const CrossplatformStrategiesBetterThanQuicklook = ['snarkdown', 'prism'];
// Snarkdown
'md',
function strategyForPreviewing(ext) {
if (ext.startsWith('.')) ext = ext.substr(1);
// XLSX
'xls',
'xlsx',
'csv',
'eth',
'ods',
'fods',
'uos1',
'uos2',
'dbf',
'txt',
'prn',
'xlw',
'xlsb',
];
const strategy = Object.keys(CrossplatformStrategies).find(strategy =>
CrossplatformStrategies[strategy].includes(ext)
);
if (QuicklookIsAvailable && !QuicklookBlacklist.includes(ext)) {
if (!strategy || !CrossplatformStrategiesBetterThanQuicklook.includes(strategy)) {
return 'quicklook';
}
}
return strategy;
}
const PreviewWindowMenuTemplate = [
{
@ -146,25 +189,19 @@ export function canPossiblyPreviewExtension(file) {
if (file.size > FileSizeLimit) {
return false;
}
// On macOS, we try to quicklook basically everything because it supports
// a large number of formats and plugins (adding support for Sketch files, etc).
// On other platforms, we only preview a specific set of formats we support.
if (QuicklookIsAvailable) {
return !QuicklookNonPreviewableExtensions.includes(file.displayExtension());
} else {
return CrossplatformPreviewableExtensions.includes(file.displayExtension());
}
return !!strategyForPreviewing(file.displayExtension());
}
export function displayQuickPreviewWindow(filePath) {
if (QuicklookIsAvailable) {
const isPDF = filePath.endsWith('.pdf');
const strategy = strategyForPreviewing(path.extname(filePath));
if (strategy === 'quicklook') {
const currentWin = AppEnv.getCurrentWindow();
currentWin.previewFile(filePath);
return;
}
const isPDF = filePath.endsWith('.pdf');
if (quickPreviewWindow === null) {
quickPreviewWindow = new remote.BrowserWindow({
width: 800,
@ -192,28 +229,28 @@ export function displayQuickPreviewWindow(filePath) {
});
} else {
quickPreviewWindow.loadFile(path.join(__dirname, 'renderer.html'), {
search: JSON.stringify({ mode: 'display', filePath }),
search: JSON.stringify({ mode: 'display', filePath, strategy }),
});
}
}
export async function generatePreview(...args) {
if (QuicklookIsAvailable) {
return await _generateQuicklookPreview(...args);
export async function generatePreview({ file, filePath, previewPath }) {
const strategy = strategyForPreviewing(file.displayExtension());
if (strategy === 'quicklook') {
return await _generateQuicklookPreview({ file, filePath, previewPath });
} else if (strategy) {
return await _generateCrossplatformPreview({ file, filePath, previewPath, strategy });
} else {
return await _generateCrossplatformPreview(...args);
return false;
}
}
// Private
async function _generateCrossplatformPreview({ file, filePath, previewPath }) {
if (!CrossplatformPreviewableExtensions.includes(file.displayExtension())) {
return false;
}
async function _generateCrossplatformPreview({ file, filePath, previewPath, strategy }) {
return new Promise(resolve => {
captureQueue.push({ file, filePath, previewPath, resolve });
captureQueue.push({ file, filePath, previewPath, strategy, resolve });
if (!captureWindow || captureWindow.isDestroyed()) {
captureWindow = _createCaptureWindow();
@ -255,11 +292,11 @@ function _generateNextCrossplatformPreview() {
return;
}
const { filePath, previewPath, resolve } = captureQueue.pop();
const { strategy, filePath, previewPath, resolve } = captureQueue.pop();
// Start the thumbnail generation
captureWindow.loadFile(path.join(__dirname, 'renderer.html'), {
search: JSON.stringify({ mode: 'capture', filePath, previewPath }),
search: JSON.stringify({ strategy, mode: 'capture', filePath, previewPath }),
});
// Race against a timer to complete the preview. We don't want this to hang

View file

@ -1,18 +1,29 @@
const fs = require('fs');
const path = require('path');
const PDFJSRoot = path.join(__dirname, 'pdfjs-2.0.943');
global.PDFJSRoot = path.join(__dirname, 'pdfjs-2.0.943');
global.PrismRoot = path.join(__dirname, 'prism-1.15.0');
global.nodePDFForFile = async filePath => {
global.pdfjs = require(`${PDFJSRoot}/build/pdf.js`);
global.pdfjs.GlobalWorkerOptions.workerSrc = path.join(PDFJSRoot, 'build', 'pdf.worker.js');
global.pdfjs = require(`${global.PDFJSRoot}/build/pdf.js`);
global.pdfjs.GlobalWorkerOptions.workerSrc = path.join(
global.PDFJSRoot,
'build',
'pdf.worker.js'
);
return await global.pdfjs.getDocument(filePath, {
cMapUrl: path.join(PDFJSRoot, 'web', 'cmaps'),
cMapUrl: path.join(global.PDFJSRoot, 'web', 'cmaps'),
cMapPacked: true,
});
};
global.nodeStringForFile = (filePath, { truncate } = {}) => {
let raw = fs.readFileSync(filePath).toString();
if (truncate) raw = raw.substr(0, 1000);
return raw;
};
global.nodeXLSXForFile = filepath => {
global.xlsx = require('xlsx');
return fs.readFileSync(filepath);
@ -47,10 +58,16 @@ global.finishWithWindowCapture = (previewPath, startedAt = Date.now()) => {
return;
}
const win = require('electron').remote.getCurrentWindow();
win.capturePage(img => {
fs.writeFileSync(previewPath, img.toPNG());
document.title = 'Finished';
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
const win = require('electron').remote.getCurrentWindow();
win.capturePage(img => {
fs.writeFileSync(previewPath, img.toPNG());
document.title = 'Finished';
});
});
});
});
};

View file

@ -0,0 +1,125 @@
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript+c+csharp+cpp+ruby+diff+docker+go+java+json+latex+makefile+objectivec+sql+python+jsx+typescript+rust+swift+yaml+tsx */
/**
* okaidia theme for JavaScript, CSS and HTML
* Loosely based on Monokai textmate theme by http://www.monokai.nl/
* @author ocodia
*/
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #272822;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #f8f8f2;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #f92672;
}
.token.boolean,
.token.number {
color: #ae81ff;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #a6e22e;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #f8f8f2;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #e6db74;
}
.token.keyword {
color: #66d9ef;
}
.token.regex,
.token.important {
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

File diff suppressed because one or more lines are too long

View file

@ -124,6 +124,87 @@
}
}
async function runPrism({ filePath, previewPath, mode }) {
const ext = filePath.split('.').pop();
const str = await nodeStringForFile(filePath, {
truncate: mode === 'capture',
});
const grammars = {
html: 'html',
xml: 'xml',
svg: 'svg',
css: 'css',
js: 'javascript',
c: 'c',
h: 'c',
cs: 'csharp',
cc: 'cpp',
cpp: 'cpp',
patch: 'diff',
go: 'go',
java: 'java',
json: 'json',
jsonp: 'json',
tex: 'latex',
m: 'objectivec',
mm: 'objectivec',
py: 'python',
jsx: 'jsx',
tsx: 'tsx',
rb: 'ruby',
rs: 'rust',
sql: 'sql',
swift: 'swift',
ts: 'typescript',
yaml: 'yaml',
txt: 'clike',
log: 'clike',
};
const grammar = grammars[ext] || 'clike';
// Attach Prism CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = './prism-1.15.0/prism.css';
document.body.appendChild(link);
document.body.style.margin = '0';
// Attach <pre><code class="language-XXX">{text}</code></pre> to the DOM
div.innerHTML = '';
if (mode !== 'capture') {
div.style.opacity = 0;
}
const preEl = document.createElement('pre');
preEl.style = 'min-height: 100%; margin: 0; border-radius: 0;';
div.appendChild(preEl);
const codeEl = document.createElement('code');
codeEl.textContent = str;
codeEl.classList.add(`language-${grammar}`);
preEl.appendChild(codeEl);
// Load the Prism script and capture/show the DOM when it's completed
const finish = () => {
const stylesReady = window.getComputedStyle(preEl).fontFamily.includes('Monaco');
if (!stylesReady) {
setTimeout(finish, 1);
return;
}
div.style.opacity = 1;
if (mode === 'capture') {
window.finishWithWindowCapture(previewPath);
}
};
const script = document.createElement('script');
script.onload = finish;
script.src = './prism-1.15.0/prism.js';
document.body.appendChild(script);
}
async function runMammoth({ filePath, previewPath, mode }) {
div.innerHTML = await nodeMammothHTMLForFile(filePath);
if (mode === 'capture') {
@ -140,22 +221,24 @@
}
}
const { mode, filePath, previewPath } = JSON.parse(
const { mode, filePath, previewPath, strategy } = JSON.parse(
decodeURIComponent(window.location.search.substr(1))
);
window.requestAnimationFrame(() => {
if (filePath.endsWith('.pdf')) {
if (strategy === 'pdfjs') {
if (mode === 'capture') {
runPDFCapture({ filePath, previewPath });
} else {
// To view PDFs we use the separate pdfviewer included with pdfjs
}
} else if (filePath.endsWith('.docx')) {
} else if (strategy === 'mammoth') {
runMammoth({ mode, filePath, previewPath });
} else if (filePath.endsWith('.md')) {
} else if (strategy === 'snarkdown') {
runSnarkdown({ mode, filePath, previewPath });
} else {
} else if (strategy === 'prism') {
runPrism({ mode, filePath, previewPath });
} else if (strategy === 'xlsx') {
runXLSX({ mode, filePath, previewPath });
}
});