diff --git a/README.md b/README.md index 91e1caa..905ae70 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,10 @@ TODO ## My Questions It would be great if you can answer my following questions to help develop this extension. -1. Is the a `onDidChangeScrollTop` function for `TextEditor` in vscode. So that I can track the change of scrollTop position of the text editor. +1. Is there a `onDidChangeScrollTop` function for `TextEditor` in vscode. So that I can track the change of scrollTop position of the text editor. 1. Can I manually set the `scrollTop` of `TextEditor`? 1. How to programmatically close my preview by `uri`? I tried to implement the `Toggle Preview` command but failed because I don't know how to close a preview. So now only `Open Preview` is provided. +1. How do I programmatically close the a `Preview` by `vscode.Uri`? ## Extension Settings diff --git a/dependencies/prism/themes/coy.css b/dependencies/prism/themes/coy.css index cb78496..879cdd1 100644 --- a/dependencies/prism/themes/coy.css +++ b/dependencies/prism/themes/coy.css @@ -38,7 +38,10 @@ pre[class*="language-"] { background-size: 3em 3em; background-origin: content-box; overflow: visible; - padding: 0; + /* padding: 0; */ + + padding-top: 1em; + padding-bottom: 1em; } code[class*="language"] { @@ -194,19 +197,6 @@ pre[class*="language-"]:after { color: #e0d7d1; } -/* Plugin styles: Line Numbers */ -pre[class*="language-"].line-numbers { - padding-left: 0; -} - -pre[class*="language-"].line-numbers code { - padding-left: 3.8em; -} - -pre[class*="language-"].line-numbers .line-numbers-rows { - left: 0; -} - /* Plugin styles: Line Highlight */ pre[class*="language-"][data-line] { padding-top: 0; diff --git a/docs/code-chunk.md b/docs/code-chunk.md new file mode 100644 index 0000000..9adde1f --- /dev/null +++ b/docs/code-chunk.md @@ -0,0 +1,221 @@ +# Code Chunk + +(this doc is now the newest) + +**Changes might happen in the future.** + +**Markdown Preview Enhanced** allows you to render code output into documents. + + ```bash {cmd:true} + ls . + ``` + + ```javascript {cmd:"node"} + const date = Date.now() + console.log(date.toString()) + ``` + +## Commands & Keyboard Shortcuts +* `Markdown Preview Enhanced: Run Code Chunk` or shift-enter +execute single code chunk where your cursor is at. +* `Markdown Preview Enhanced: Run All Code Chunks` or ctrl-shift-enter +execute all code chunks. + +## Format +You can configure code chunk options in format of ```lang {opt1:value1, opt2:value2, ...} + +**lang** +The grammar that the code block should highlight. +It should be put at the most front. + +## Basic Options +**cmd** +The command to run. +If `cmd` is not provided, then `lang` will be regarded as command. + +eg: + + ```python {cmd:"/usr/local/bin/python3"} + print("This will run python3 program") + ``` + + +**output** +`html`, `markdown`, `text`, `png`, `none` + +Defines how to render code output. +`html` will append output as html. +`markdown` will parse output as markdown. (MathJax and graphs will not be supported in this case, but KaTeX works) +`text` will append output to a `pre` block. +`png` will append output as `base64` image. +`none` will hide the output. + +eg: + + ```gnuplot {cmd:true, output:"html"} + set terminal svg + set title "Simple Plots" font ",20" + set key left box + set samples 50 + set style data points + + plot [-10:10] sin(x),atan(x),cos(atan(x)) + ``` + +![screen shot 2017-06-20 at 8 40 07 am](https://user-images.githubusercontent.com/1908863/27336074-1cd3a88a-5594-11e7-857f-b8c598853433.png) + +**args** +args that append to command. eg: + + ```python {cmd:true, args:["-v"]} + print("Verbose will be printed first") + ``` + + ```erd {cmd:true, args:["-f", "svg", "-i"], output:"html"} + # output svg format and append as html result. + ``` + +**stdin** +If `stdin` is set to true, then the code will be passed as stdin instead of as file. + +**hide** +`hide` will hide code chunk but only leave the output visible. default: `false` +eg: + + ```python {hide:true} + print('you can see this output message, but not this code') + ``` + +**continue** +If set `continue: true`, then this code chunk will continue from the last code chunk. +If set `continue: id`, then this code chunk will continue from the code chunk of id. +eg: + + ```python {cmd:true, id:"izdlk700"} + x = 1 + ``` + + ```python {cmd:true, id:"izdlkdim"} + x = 2 + ``` + + ```python {cmd:true, continue:"izdlk700", id:"izdlkhso"} + print(x) # will print 1 + ``` + +**class** +If set `class:"class1 class2"`, then `class1 class2` will be add to the code chunk. +* `lineNo` class will show line numbers to code chunk. + +**element** +The element that you want to append after. +Check the **Plotly** example below. + +**id** +The `id` of the code chunk. This option would be useful if `continue` is used. + +## Macro +* **input_file** +`input_file` is automatically generated under the same directory of your markdown file and will be deleted after running code that is copied to `input_file`. +By default, it is appended at the very end of program arguments. +However, you can set the position of `input_file` in your `args` option by `{input_file}` macro. eg: + + + ```program {cmd:true, args:["-i", "{input_file}", "-o", "./output.png"]} + ...your code here + ``` + + +## Matplotlib +If set `matplotlib: true`, then the python code chunk will plot graphs inline in the preview. +eg: + + ```python {cmd:true, matplotlib:true} + import matplotlib.pyplot as plt + plt.plot([1,2,3, 4]) + plt.show() # show figure + ``` +![screen shot 2017-06-20 at 8 44 25 am](https://user-images.githubusercontent.com/1908863/27336286-acc41d8a-5594-11e7-9a10-ed7a6fc41f6c.png) + +## LaTeX +Markdown Preview Enhanced also supports `LaTeX` compilation. +Before using this feature, you need to have [pdf2svg](extra.md?id=install-svg2pdf) and [LaTeX engine](extra.md?id=install-latex-distribution) installed. +Then you can simply write LaTeX in code chunk like this: + + + ```latex {cmd:true} + \documentclass{standalone} + \begin{document} + Hello world! + \end{document} + ``` + +![screen shot 2017-06-20 at 8 45 15 am](https://user-images.githubusercontent.com/1908863/27336322-caba8658-5594-11e7-87cf-e54518d46435.png) + + +### LaTeX output configuration +**latex_zoom** +If set `latex_zoom:num`, then the result will be scaled `num` times. + +**latex_width** +The width of result. + +**latex_height** +The height of result. + +**latex_engine** +The latex engine that you used to compile `tex` file. By default `pdflatex` is used. You can change the default value from the [pacakge settings](usages.md?id=package-settings). + + +### TikZ example +It is recommended to use `standalone` while drawing `tikz` graphs. +![screen shot 2017-06-05 at 9 48 10 pm](https://cloud.githubusercontent.com/assets/1908863/26811633/b018aa76-4a38-11e7-9ec2-688f273468bb.png) + + +## Plotly +Markdown Preview Enhanced allows you to draw [Plotly](https://plot.ly/) easily. +For example: +![screen shot 2017-06-06 at 3 27 28 pm](https://user-images.githubusercontent.com/1908863/26850341-c6095a94-4acc-11e7-83b4-7fdb4eb8b1d8.png) + +* The first line `@import "https://cdn.plot.ly/plotly-latest.min.js" ` uses the [file import](file-imports.md) functionality to import `plotly-latest.min.js` file. However, it is recommended to download the js file to local disk for better performance. +* Then we created a `javascript` code chunk. + +## Demo +This demo shows you how to render entity-relation diagram by using [erd](https://github.com/BurntSushi/erd) library. + + ```erd {cmd:true, output:"html", args:["-i", "{input_file}", "-f", "svg"], id:"ithhv4z4"} + + [Person] + *name + height + weight + +birth_location_id + + [Location] + *id + city + state + country + + Person *--1 Location + ``` + +`erd {cmd:true, output:"html", args:["-i", "{input_file}", "-f", "svg"]}` +* `erd` the program that we are using. (*you need to have the program installed first*) +* `output:"html"` we will append the running result as `html`. +* `args` field shows the arguments that we will use. + +Then we can click the `run` button at the preview to run our code. + +![code_chunk](http://i.imgur.com/a7LkJYD.gif) + +## Showcases (outdated) +**bash** +![Screen Shot 2016-09-24 at 1.41.06 AM](http://i.imgur.com/v5Y7juh.png) + +**gnuplot with svg output** +![Screen Shot 2016-09-24 at 1.44.14 AM](http://i.imgur.com/S93g7Tk.png) + +## Limitations +* Doesn't work with `ebook` yet. +* Might be buggy when using`pandoc document export` diff --git a/package.json b/package.json index 040ece8..f723347 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,38 @@ { "command": "markdown-preview-enhanced.runCodeChunk", "title": "Markdown Preview Enhanced: Run Code Chunk" + }, + { + "command": "markdown-preview-enhanced.customizeCss", + "title": "Markdown Preview Enhanced: Customize CSS" + }, + { + "command": "markdown-preview-enhanced.insertNewSlide", + "title": "Markdown Preview Enhanced: Insert New Slide" + }, + { + "command": "markdown-preview-enhanced.insertTable", + "title": "Markdown Preview Enhanced: Insert Table" + }, + { + "command": "markdown-preview-enhanced.insertPagebreak", + "title": "Markdown Preview Enhanced: Insert Page Break" + }, + { + "command": "markdown-preview-enhanced.createTOC", + "title": "Markdown Preview Enhanced: Create TOC" + }, + { + "command": "markdown-preview-enhanced.openMermaidConfig", + "title": "Markdown Preview Enhanced: Open Mermaid Config" + }, + { + "command": "markdown-preview-enhanced.openMathJaxConfig", + "title": "Markdown Preview Enhanced: Open MathJax Config" + }, + { + "command": "markdown-preview-enhanced.showUploadedImages", + "title": "Markdown Preview Enhanced: Show Uploaded Images" } ], "keybindings": [ diff --git a/src/code-chunk.ts b/src/code-chunk.ts index e2f81f6..c72bb3a 100644 --- a/src/code-chunk.ts +++ b/src/code-chunk.ts @@ -1,8 +1,44 @@ import * as path from "path" import * as fs from "fs" import {spawn} from "child_process" +import * as vm from "vm" import * as utility from "./utility" +import * as LaTeX from "./latex" + +async function compileLaTeX(content:string, fileDirectoryPath:string, options:object):Promise { + const latexEngine = options['latex_engine'] || 'pdflatex' + const latexSVGDir = options['latex_svg_dir'] // if not provided, the svg files will be stored in temp folder and will be deleted automatically + const latexZoom = options['latex_zoom'] + const latexWidth = options['latex_width'] + const latexHeight = options['latex_height'] + + const texFilePath = path.resolve(fileDirectoryPath, Math.random().toString(36).substr(2, 9) + '_code_chunk.tex') + + await utility.writeFile(texFilePath, content) + + try { + const svgMarkdown = await LaTeX.toSVGMarkdown(texFilePath, {latexEngine, markdownDirectoryPath:fileDirectoryPath, svgDirectoryPath:latexSVGDir, svgZoom:latexZoom, svgWidth:latexWidth, svgHeight:latexHeight}) + fs.unlink(texFilePath, (error)=> {}) + + options['output'] = 'markdown' // set output as markdown + return svgMarkdown + } catch(e) { + fs.unlink(texFilePath, (error)=> {}) + throw e + } +} + +/** + * + * @param code should be a javascript function string + * @param options + */ +async function runInVm(code:string, options:object):Promise { + const script = new vm.Script(`((${code})())`) + const context = vm.createContext( options['context'] || {}) + return script.runInContext(context) +} export async function run(content:string, fileDirectoryPath:string, options:object):Promise { const cmd = options['cmd'] @@ -14,6 +50,14 @@ export async function run(content:string, fileDirectoryPath:string, options:obje const savePath = path.resolve(fileDirectoryPath, Math.random().toString(36).substr(2, 9) + '_code_chunk') content = content.replace(/\u00A0/g, ' ') + if (cmd.match(/(la)?tex/) || cmd === 'pdflatex') { + return compileLaTeX(content, fileDirectoryPath, options) + } + + if (cmd === 'node.vm') { + return runInVm(content, options) + } + if (cmd.match(/python/) && (options['matplotlib'] || options['mpl'])) { content = ` diff --git a/src/config.ts b/src/config.ts index 8065932..44301da 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode" -export class MarkdownPreviewEnhancedConfig implements MPEConfig { +export class MarkdownPreviewEnhancedConfig implements MarkdownEngineConfig { public static getCurrentConfig() { return new MarkdownPreviewEnhancedConfig() } diff --git a/src/extension.ts b/src/extension.ts index 8f4e04f..496dad5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -60,6 +60,66 @@ export function activate(context: vscode.ExtensionContext) { }) } + function customizeCSS() { + const globalStyleLessFile = 'file://'+path.resolve(utility.extensionConfigDirectoryPath, './style.less') + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(globalStyleLessFile)) + } + + function openMermaidConfig() { + const mermaidConfigFilePath = 'file://'+path.resolve(utility.extensionConfigDirectoryPath, './mermaid_config.js') + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(mermaidConfigFilePath)) + } + + function openMathJaxConfig() { + const mathjaxConfigFilePath = 'file://'+path.resolve(utility.extensionConfigDirectoryPath, './mathjax_config.js') + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(mathjaxConfigFilePath)) + } + + function showUploadedImages() { + const imageHistoryFilePath = 'file://'+path.resolve(utility.extensionConfigDirectoryPath, './image_history.md') + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(imageHistoryFilePath)) + } + + function insertNewSlide() { + const editor = vscode.window.activeTextEditor + if (editor && editor.document && editor.edit) { + editor.edit((textEdit)=> { + textEdit.insert(editor.selection.active, '\n') + }) + } + } + + function insertPagebreak() { + const editor = vscode.window.activeTextEditor + if (editor && editor.document && editor.edit) { + editor.edit((textEdit)=> { + textEdit.insert(editor.selection.active, '\n') + }) + } + } + + function createTOC() { + const editor = vscode.window.activeTextEditor + if (editor && editor.document && editor.edit) { + editor.edit((textEdit)=> { + textEdit.insert(editor.selection.active, '\n\n') + }) + } + } + + function insertTable() { + const editor = vscode.window.activeTextEditor + if (editor && editor.document && editor.edit) { + editor.edit((textEdit)=> { + textEdit.insert(editor.selection.active, +`| | | +|---|---| +| | | +`) + }) + } + } + function openImageHelper() { contentProvider.openImageHelper(vscode.window.activeTextEditor.document.uri) } @@ -113,11 +173,26 @@ export function activate(context: vscode.ExtensionContext) { contentProvider.eBookExport(sourceUri, fileType) } + function pandocExport(uri) { + const sourceUri = vscode.Uri.parse(decodeURIComponent(uri)); + contentProvider.pandocExport(sourceUri) + } + + function markdownExport(uri) { + const sourceUri = vscode.Uri.parse(decodeURIComponent(uri)); + contentProvider.markdownExport(sourceUri) + } + function cacheSVG(uri, code, svg) { const sourceUri = vscode.Uri.parse(decodeURIComponent(uri)); contentProvider.cacheSVG(sourceUri, code, svg) } + function cacheCodeChunkResult(uri, id, result) { + const sourceUri = vscode.Uri.parse(decodeURIComponent(uri)); + contentProvider.cacheCodeChunkResult(sourceUri, id, result) + } + function runCodeChunk(uri, codeChunkId) { const sourceUri = vscode.Uri.parse(decodeURIComponent(uri)); contentProvider.runCodeChunk(sourceUri, codeChunkId) @@ -157,11 +232,26 @@ export function activate(context: vscode.ExtensionContext) { type: 'run-code-chunk' }) } + + function clickTagA(uri, href) { + const sourceUri = vscode.Uri.parse(decodeURIComponent(uri)); + if (['.pdf', '.xls', '.xlsx', '.doc', '.ppt', '.docx', '.pptx'].indexOf(path.extname(href)) >= 0) { + utility.openFile(href) + } else if (href.match(/^file\:\/\/\//)) { + // openFilePath = href.slice(8) # remove protocal + let openFilePath = href.replace(/(\s*)[\#\?](.+)$/, '') // remove #anchor and ?params... + openFilePath = decodeURI(openFilePath) + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(openFilePath), vscode.ViewColumn.One) + } else { + utility.openFile(href) + } + } context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => { if (isMarkdownFile(document)) { - contentProvider.update(document.uri); + // contentProvider.update(document.uri, true); + contentProvider.updateMarkdown(document.uri, true) } })) @@ -240,6 +330,22 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand('markdown-preview-enhanced.runCodeChunk', runCodeChunkCommand)) + context.subscriptions.push(vscode.commands.registerCommand('markdown-preview-enhanced.customizeCss', customizeCSS)) + + context.subscriptions.push(vscode.commands.registerCommand('markdown-preview-enhanced.openMermaidConfig', openMermaidConfig)) + + context.subscriptions.push(vscode.commands.registerCommand('markdown-preview-enhanced.openMathJaxConfig', openMathJaxConfig)) + + context.subscriptions.push(vscode.commands.registerCommand('markdown-preview-enhanced.showUploadedImages', showUploadedImages)) + + context.subscriptions.push(vscode.commands.registerCommand('markdown-preview-enhanced.insertNewSlide', insertNewSlide)) + + context.subscriptions.push(vscode.commands.registerCommand('markdown-preview-enhanced.insertTable', insertTable)) + + context.subscriptions.push(vscode.commands.registerCommand('markdown-preview-enhanced.insertPagebreak', insertPagebreak)) + + context.subscriptions.push(vscode.commands.registerCommand('markdown-preview-enhanced.createTOC', createTOC)) + context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.revealLine', revealLine)) context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.insertImageUrl', insertImageUrl)) @@ -258,14 +364,22 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.eBookExport', eBookExport)) + context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.pandocExport', pandocExport)) + + context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.markdownExport', markdownExport)) + context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.webviewFinishLoading', webviewFinishLoading)) context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.cacheSVG', cacheSVG)) + context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.cacheCodeChunkResult', cacheCodeChunkResult)) + context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.runCodeChunk', runCodeChunk)) context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.runAllCodeChunks', runAllCodeChunks)) + context.subscriptions.push(vscode.commands.registerCommand('_markdown-preview-enhanced.clickTagA', clickTagA)) + context.subscriptions.push(contentProviderRegistration) } diff --git a/src/image-helper.ts b/src/image-helper.ts index fa23129..6c6084d 100644 --- a/src/image-helper.ts +++ b/src/image-helper.ts @@ -74,8 +74,26 @@ export function pasteImageFile(sourceUri: any, imageFilePath: string) { }) } -function addImageURLToHistory(markdown) { +async function addImageURLToHistory(markdownImage) { // TODO: save to history + const imageHistoryPath = path.resolve(utility.extensionConfigDirectoryPath, './image_history.md') + let data:string + try { + data = await utility.readFile(imageHistoryPath, {encoding: 'utf-8'}) + } catch(e) { + data = '' + } + data = ` +${markdownImage} + +\`${markdownImage}\` + +${(new Date()).toString()} + +--- + +` + data + utility.writeFile(imageHistoryPath, data, {encoding: 'utf-8'}) } function replaceHint(editor:vscode.TextEditor, line:number, hint:string, withStr:string):boolean { diff --git a/src/latex.ts b/src/latex.ts new file mode 100644 index 0000000..c16e5d9 --- /dev/null +++ b/src/latex.ts @@ -0,0 +1,77 @@ +import {spawn} from "child_process" +import * as path from "path" +import * as fs from "fs" + +import * as PDF from "./pdf" + + +function cleanUpFiles(texFilePath:string) { + const directoryPath = path.dirname(texFilePath) + const extensionName = path.extname(texFilePath) + const filePrefix = path.basename(texFilePath).replace(new RegExp(extensionName + '$'), '') + + fs.readdir(directoryPath, (error, items)=> { + if (error) return + + items.forEach((fileName)=> { + if (fileName.startsWith(filePrefix) && !fileName.match(/\.(la)?tex/)) + fs.unlink(path.resolve(directoryPath, fileName), ()=>{}) + }) + }) +} + + +export function toSVGMarkdown(texFilePath:string, {latexEngine="pdflatex", svgDirectoryPath, markdownDirectoryPath, svgZoom, svgWidth, svgHeight}: +{ + latexEngine: string, + svgDirectoryPath?: string, + markdownDirectoryPath: string, + svgZoom?: string, + svgWidth?: string, + svgHeight?: string +}):Promise { +return new Promise((resolve, reject)=> { + const task = spawn(latexEngine, [texFilePath], {cwd: path.dirname(texFilePath)}) + + const chunks = [] + task.stdout.on('data', (chunk)=> { + chunks.push(chunk) + }) + + const errorChunks = [] + task.stderr.on('data', (chunk)=> { + errorChunks.push(chunk) + }) + + task.on('error', (error)=> { + errorChunks.push(Buffer.from(error.toString(), 'utf-8')) + }) + + task.on('close', ()=> { + if (errorChunks.length) { + cleanUpFiles(texFilePath) + return reject(Buffer.concat(errorChunks).toString()) + } else { + const output = Buffer.concat(chunks).toString() + if (output.indexOf('LaTeX Error') >= 0) { // meet error + cleanUpFiles(texFilePath) + return reject(output) + } + + const pdfFilePath = texFilePath.replace(/\.(la)?tex$/, '.pdf') + + PDF.toSVGMarkdown(pdfFilePath, {svgDirectoryPath, markdownDirectoryPath, svgZoom, svgWidth, svgHeight}) + .then((svgMarkdown)=> { + cleanUpFiles(texFilePath) + return resolve(svgMarkdown) + }) + .catch((error)=> { + cleanUpFiles(texFilePath) + return reject(error) + }) + } + }) + + task.stdin.end() +}) +} \ No newline at end of file diff --git a/src/magick.ts b/src/magick.ts new file mode 100644 index 0000000..8b6cd65 --- /dev/null +++ b/src/magick.ts @@ -0,0 +1,21 @@ +/** + * ImageMagick magick command wrapper + */ + +import * as fs from "fs" +import * as path from "path" +import {execFile} from "child_process" +import * as temp from "temp" + +import * as utility from "./utility" + +export async function svgElementToPNGFile(svgElement:string, pngFilePath:string):Promise { + const info = await utility.tempOpen({prefix: "mpe-svg", suffix:'.svg'}) + await utility.write(info.fd, svgElement) // write svgElement to temp .svg file + try { + await utility.execFile('magick', [info.path, pngFilePath]) + } catch(error) { + throw "ImageMagick is required to be installed to convert svg to png.\n" + error.toString() + } + return pngFilePath +} \ No newline at end of file diff --git a/src/markdown-convert.ts b/src/markdown-convert.ts new file mode 100644 index 0000000..6645ac3 --- /dev/null +++ b/src/markdown-convert.ts @@ -0,0 +1,184 @@ +/** + * Convert MPE markdown to Githb Flavored Markdown + */ + +import * as path from "path" +import * as fs from "fs" +import * as mkdirp from "mkdirp" +// processGraphs = require './process-graphs' +// encrypt = require './encrypt' +// CACHE = require './cache' +import {transformMarkdown} from "./transformer" +import * as utility from "./utility" +import {processGraphs} from "./process-graphs" +const md5 = require(path.resolve(utility.extensionDirectoryPath, './dependencies/javascript-md5/md5.js')) + + +/** + * Convert all math expressions inside markdown to images. + * @param text input markdown text + * @param config + */ +function processMath(text:string, {mathInlineDelimiters, mathBlockDelimiters}):string { + let line = text.replace(/\\\$/g, '#slash_dollarsign#') + + const inline = mathInlineDelimiters + const block = mathBlockDelimiters + + const inlineBegin = '(?:' + inline.map((x)=> x[0]) + .join('|') + .replace(/\\/g, '\\\\') + .replace(/([\(\)\[\]\$])/g, '\\$1') + ')' + const inlineEnd = '(?:' + inline.map((x)=> x[1]) + .join('|') + .replace(/\\/g, '\\\\') + .replace(/([\(\)\[\]\$])/g, '\\$1') + ')' + const blockBegin = '(?:' + block.map((x)=> x[0]) + .join('|') + .replace(/\\/g, '\\\\') + .replace(/([\(\)\[\]\$])/g, '\\$1') + ')' + const blockEnd = '(?:' + block.map((x)=> x[1]) + .join('|') + .replace(/\\/g, '\\\\') + .replace(/([\(\)\[\]\$])/g, '\\$1') + ')' + + // display + line = line.replace(new RegExp(`(\`\`\`(?:[\\s\\S]+?)\`\`\`\\s*(?:\\n|$))|(?:${blockBegin}([\\s\\S]+?)${blockEnd})`, 'g'), ($0, $1, $2)=> { + if ($1) return $1 + let math = $2 + math = math.replace(/\n/g, '').replace(/\#slash\_dollarsign\#/g, '\\\$') + math = utility.escapeString(math) + return `

` + }) + + // inline + line = line.replace(new RegExp(`(\`\`\`(?:[\\s\\S]+?)\`\`\`\\s*(?:\\n|$))|(?:${inlineBegin}([\\s\\S]+?)${inlineEnd})`, 'g'), ($0, $1, $2)=> { + if ($1) return $1 + let math = $2 + math = math.replace(/\n/g, '').replace(/\#slash\_dollarsign\#/g, '\\\$') + math = utility.escapeString(math) + return `` + }) + + line = line.replace(/\#slash\_dollarsign\#/g, '\\\$') + return line +} + +/** + * Format paths + * @param text + * @param fileDirectoryPath + * @param projectDirectoryPath + * @param useRelativeFilePath + * @param protocolsWhiteListRegExp + */ +function processPaths(text, fileDirectoryPath, projectDirectoryPath, useRelativeFilePath, protocolsWhiteListRegExp:RegExp) { + let match = null, + offset = 0, + output = '' + + function resolvePath(src) { + if (src.match(protocolsWhiteListRegExp)) + return src + + if (useRelativeFilePath) { + if (src.startsWith('/')) + return path.relative(fileDirectoryPath, path.resolve(projectDirectoryPath, '.'+src)) + else // ./test.png or test.png + return src + } + else { + if (src.startsWith('/')) + return src + else // ./test.png or test.png + return '/' + path.relative(projectDirectoryPath, path.resolve(fileDirectoryPath, src)) + } + } + + let inBlock = false + let lines = text.split('\n') + lines = lines.map((line)=> { + if (line.match(/^\s*```/)) { + inBlock = !inBlock + return line + } + else if (inBlock) + return line + else { + // replace path in ![](...) and []() + let r = /(\!?\[.*?]\()([^\)|^'|^"]*)(.*?\))/gi + line = line.replace(r, (whole, a, b, c)=> { + if (b[0] === '<') { + b = b.slice(1, b.length-1) + return a + '<' + resolvePath(b.trim()) + '> ' + c + } else { + return a + resolvePath(b.trim()) + ' ' + c + } + }) + + // replace path in tag + r = /(<[img|a|iframe].*?[src|href]=['"])(.+?)(['"].*?>)/gi + line = line.replace(r, (whole, a, b, c)=> { + return a + resolvePath(b) + c + }) + return line + } + }) + + return lines.join('\n') +} + +export async function markdownConvert(text, +{projectDirectoryPath, fileDirectoryPath, protocolsWhiteListRegExp, filesCache, mathInlineDelimiters, mathBlockDelimiters, codeChunksData, graphsCache}: +{projectDirectoryPath:string, fileDirectoryPath:string, protocolsWhiteListRegExp:RegExp, filesCache:{[key:string]:string}, mathInlineDelimiters:string[][], mathBlockDelimiters:string[][], codeChunksData:{[key:string]:CodeChunkData}, graphsCache:{[key:string]:string}}, +config:object):Promise { + if (!config['path']) + throw '{path} has to be specified' + + if (!config['image_dir']) + throw '{image_dir} has to be specified' + + // dest + let outputFilePath + if (config['path'][0] == '/') + outputFilePath = path.resolve(projectDirectoryPath, '.' + config['path']) + else + outputFilePath = path.resolve(fileDirectoryPath, config['path']) + + // TODO: create imageFolder + let imageDirectoryPath:string + if (config['image_dir'][0] === '/') + imageDirectoryPath = path.resolve(projectDirectoryPath, '.' + config['image_dir']) + else + imageDirectoryPath = path.resolve(fileDirectoryPath, config['image_dir']) + + const useRelativeFilePath = !config['absolute_image_path'] + + // import external files + const data = await transformMarkdown(text, {fileDirectoryPath, projectDirectoryPath, useRelativeFilePath, filesCache, forPreview:false, protocolsWhiteListRegExp, imageDirectoryPath}) + text = data.outputString + + // change link path to project '/' path + // this is actually differnet from pandoc-convert.coffee + text = processPaths(text, fileDirectoryPath, projectDirectoryPath, useRelativeFilePath, protocolsWhiteListRegExp) + + text = processMath(text, {mathInlineDelimiters, mathBlockDelimiters}) + + return await new Promise((resolve, reject)=> { + mkdirp(imageDirectoryPath, (error, made)=> { + if (error) return reject(error.toString()) + + processGraphs(text, + {fileDirectoryPath, projectDirectoryPath, imageDirectoryPath, imageFilePrefix: md5(outputFilePath), useRelativeFilePath, codeChunksData, graphsCache}) + .then(({outputString})=> { + fs.writeFile(outputFilePath, outputString, {encoding: 'utf-8'}, (error)=> { + if (error) return reject(error.toString()) + return resolve(outputFilePath) + }) + }) + }) + }) +} + + + diff --git a/src/markdown-engine.ts b/src/markdown-engine.ts index fbf98b8..7021013 100644 --- a/src/markdown-engine.ts +++ b/src/markdown-engine.ts @@ -6,41 +6,42 @@ import * as request from "request" const matter = require('gray-matter') -import {MarkdownPreviewEnhancedConfig} from "./config" import * as plantumlAPI from "./puml" -import {escapeString, unescapeString, getExtensionDirectoryPath, readFile} from "./utility" +import {escapeString, unescapeString, readFile} from "./utility" import * as utility from "./utility" -let viz = null import {scopeForLanguageName} from "./extension-helper" import {transformMarkdown} from "./transformer" import {toc} from "./toc" import {CustomSubjects} from "./custom-subjects" import {princeConvert} from "./prince-convert" import {ebookConvert} from "./ebook-convert" +import {pandocConvert} from "./pandoc-convert" +import {markdownConvert} from "./markdown-convert" import * as CodeChunkAPI from "./code-chunk" -const extensionDirectoryPath = getExtensionDirectoryPath() +const extensionDirectoryPath = utility.extensionDirectoryPath const katex = require(path.resolve(extensionDirectoryPath, './dependencies/katex/katex.min.js')) const remarkable = require(path.resolve(extensionDirectoryPath, './dependencies/remarkable/remarkable.js')) const jsonic = require(path.resolve(extensionDirectoryPath, './dependencies/jsonic/jsonic.js')) const md5 = require(path.resolve(extensionDirectoryPath, './dependencies/javascript-md5/md5.js')) const CryptoJS = require(path.resolve(extensionDirectoryPath, './dependencies/crypto-js/crypto-js.js')) +const Viz = require(path.resolve(extensionDirectoryPath, './dependencies/viz/viz.js')) // import * as uslug from "uslug" // import * as Prism from "prismjs" let Prism = null - interface MarkdownEngineConstructorArgs { filePath: string, projectDirectoryPath: string, - config: MarkdownPreviewEnhancedConfig + config: MarkdownEngineConfig } interface MarkdownEngineRenderOption { - useRelativeImagePath: boolean, + useRelativeFilePath: boolean, isForPreview: boolean, - hideFrontMatter: boolean + hideFrontMatter: boolean, + triggeredBySave?: boolean } interface MarkdownEngineOutput { @@ -48,6 +49,12 @@ interface MarkdownEngineOutput { markdown:string, tocHTML:string, yamlConfig: any, + /** + * imported javascript and css files + * convert .js file to + * convert .css file to + */ + JSAndCssFiles: string[] // slideConfigs: Array } @@ -64,33 +71,6 @@ interface Heading { id:string } -interface CodeChunkData { - /** - * id of the code chunk - */ - id: string, - /** - * code content of the code chunk - */ - code: string, - /** - * code chunk options - */ - options: object, - /** - * result after running code chunk - */ - result: string, - /** - * whether is running the code chunk or not - */ - running: boolean, - /** - * last code chunk - */ - prev: string -} - const defaults = { html: true, // Enable HTML tags in source xhtmlOut: false, // Use '/' to close single tags (
) @@ -101,14 +81,40 @@ const defaults = { typographer: true, // Enable smartypants and other sweet transforms } +let MODIFY_SOURCE:(codeChunkData:CodeChunkData, result:string, filePath:string)=>Promise = null + export class MarkdownEngine { + /** + * Modify markdown source, append `result` after corresponding code chunk. + * @param codeChunkData + * @param result + */ + public static async modifySource(codeChunkData:CodeChunkData, result:string, filePath:string) { + if (MODIFY_SOURCE) { + await MODIFY_SOURCE(codeChunkData, result, filePath) + } else { + // TODO: direcly modify the local file. + } + + codeChunkData.running = false + return result + } + + /** + * Bind cb to MODIFY_SOURCE + * @param cb + */ + public static onModifySource(cb:(codeChunkData:CodeChunkData, result:string, filePath:string)=>Promise) { + MODIFY_SOURCE = cb + } + /** * markdown file path */ private readonly filePath: string private readonly fileDirectoryPath: string private readonly projectDirectoryPath: string - private config: MarkdownPreviewEnhancedConfig + private config: MarkdownEngineConfig private breakOnSingleNewLine: boolean private enableTypographer: boolean @@ -169,6 +175,12 @@ export class MarkdownEngine { this.graphsCache[md5(code)] = CryptoJS.AES.decrypt(svg, 'markdown-preview-enhanced').toString(CryptoJS.enc.Utf8) } + public cacheCodeChunkResult(id:string, result:string) { + const codeChunkData = this.codeChunksData[id] + if (!codeChunkData) return + codeChunkData.result = CryptoJS.AES.decrypt(result, 'markdown-preview-enhanced').toString(CryptoJS.enc.Utf8) + } + /** * * @param content the math expression @@ -435,23 +447,9 @@ export class MarkdownEngine { const block = this.config.mathBlockDelimiters // TODO - const mathJaxConfig = { - extensions: ['tex2jax.js'], - jax: ['input/TeX','output/HTML-CSS'], - showMathMenu: false, - messageStyle: 'none', - - tex2jax: { - inlineMath: this.config.mathInlineDelimiters, - displayMath: this.config.mathBlockDelimiters, - processEnvironments: false, - processEscapes: true, - }, - TeX: { - extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js'] - }, - 'HTML-CSS': { availableFonts: ['TeX'] }, - } + const mathJaxConfig = await utility.getMathJaxConfig() + mathJaxConfig['tex2jax']['inlineMath'] = this.config.mathInlineDelimiters + mathJaxConfig['tex2jax']['displayMath'] = this.config.mathBlockDelimiters if (options.offline) { mathStyle = ` @@ -540,6 +538,15 @@ export class MarkdownEngine { } catch(e) { styleCSS = '' } + + // global styles + let globalStyles = "" + try { + globalStyles = await utility.getGlobalStyles() + } catch(error) { + // ignore it + } + html = ` @@ -552,7 +559,7 @@ export class MarkdownEngine { ${presentationScript} - + ${html} @@ -575,7 +582,7 @@ export class MarkdownEngine { */ public async openInBrowser():Promise { const inputString = await utility.readFile(this.filePath, {encoding:'utf-8'}) - let {html, yamlConfig} = await this.parseMD(inputString, {useRelativeImagePath: false, hideFrontMatter: true, isForPreview: false}) + let {html, yamlConfig} = await this.parseMD(inputString, {useRelativeFilePath: false, hideFrontMatter: true, isForPreview: false}) html = await this.generateHTMLFromTemplate(html, yamlConfig, {isForPrint: false, isForPrince: false, offline: true, embedLocalImages: false} ) // create temp file @@ -598,7 +605,7 @@ export class MarkdownEngine { */ public async saveAsHTML():Promise { const inputString = await utility.readFile(this.filePath, {encoding:'utf-8'}) - let {html, yamlConfig} = await this.parseMD(inputString, {useRelativeImagePath:true, hideFrontMatter:true, isForPreview: false}) + let {html, yamlConfig} = await this.parseMD(inputString, {useRelativeFilePath:true, hideFrontMatter:true, isForPreview: false}) const htmlConfig = yamlConfig['html'] || {} let cdn = htmlConfig['cdn'], offline = !cdn @@ -638,7 +645,7 @@ export class MarkdownEngine { */ public async princeExport():Promise { const inputString = await utility.readFile(this.filePath, {encoding:'utf-8'}) - let {html, yamlConfig} = await this.parseMD(inputString, {useRelativeImagePath:false, hideFrontMatter:true, isForPreview: false}) + let {html, yamlConfig} = await this.parseMD(inputString, {useRelativeFilePath:false, hideFrontMatter:true, isForPreview: false}) let dest = this.filePath let extname = path.extname(dest) dest = dest.replace(new RegExp(extname+'$'), '.pdf') @@ -701,7 +708,7 @@ export class MarkdownEngine { */ public async eBookExport(fileType='epub'):Promise { const inputString = await utility.readFile(this.filePath, {encoding:'utf-8'}) - let {html, yamlConfig} = await this.parseMD(inputString, {useRelativeImagePath:false, hideFrontMatter:true, isForPreview: false}) + let {html, yamlConfig} = await this.parseMD(inputString, {useRelativeFilePath:false, hideFrontMatter:true, isForPreview: false}) let dest = this.filePath let extname = path.extname(dest) @@ -775,7 +782,7 @@ export class MarkdownEngine { fs.readFile(filePath, {encoding: 'utf-8'}, (error, text)=> { if (error) return reject(error.toString()) - this.parseMD(text, {useRelativeImagePath: false, isForPreview: false, hideFrontMatter:true}) + this.parseMD(text, {useRelativeFilePath: false, isForPreview: false, hideFrontMatter:true}) .then(({html})=> { return resolve({heading, id, level, filePath, html, offset}) }) @@ -844,6 +851,14 @@ export class MarkdownEngine { styleCSS = '' } + // global styles + let globalStyles = "" + try { + globalStyles = await utility.getGlobalStyles() + } catch(error) { + // ignore it + } + // only use github-light style for ebook html = ` @@ -852,7 +867,7 @@ export class MarkdownEngine { ${title} - + ${mathStyle} @@ -888,6 +903,76 @@ export class MarkdownEngine { } } + /** + * pandoc export + */ + public async pandocExport():Promise { + const inputString = await utility.readFile(this.filePath, {encoding: 'utf-8'}) + const {data:config} = this.processFrontMatter(inputString, false) + let content = inputString + if (content.match(/\-\-\-\s+/)) { + const end = content.indexOf('---\n', 4) + content = content.slice(end+4) + } + + const outputFilePath = await pandocConvert(content, { + fileDirectoryPath: this.fileDirectoryPath, + projectDirectoryPath: this.projectDirectoryPath, + sourceFilePath: this.filePath, + protocolsWhiteListRegExp: this.protocolsWhiteListRegExp, + // deleteImages: true, + filesCache: this.filesCache, + codeChunksData: this.codeChunksData, + graphsCache: this.graphsCache, + imageDirectoryPath: this.config.imageFolderPath + }, config) + + utility.openFile(outputFilePath) + return outputFilePath + } + + /** + * markdown(gfm) export + */ + public async markdownExport():Promise { + let inputString = await utility.readFile(this.filePath, {encoding: 'utf-8'}) + let {data:config} = this.processFrontMatter(inputString, false) + + if (inputString.startsWith('---\n')) { + const end = inputString.indexOf('---\n', 4) + inputString = inputString.slice(end+4) + } + + config = config['markdown'] || {} + if (!config['image_dir']) { + config['image_dir'] = this.config.imageFolderPath + } + + if (!config['path']) { + if (this.filePath.match(/\.src\./)) { + config['path'] = this.filePath.replace(/\.src\./, '.') + } else { + config['path'] = this.filePath.replace(new RegExp(path.extname(this.filePath)), '_'+path.extname(this.filePath)) + } + config['path'] = path.basename(config['path']) + } + + if (config['front_matter']) { + inputString = matter.stringify(inputString, config['front-matter']) + } + + return await markdownConvert(inputString, { + projectDirectoryPath: this.projectDirectoryPath, + fileDirectoryPath: this.fileDirectoryPath, + protocolsWhiteListRegExp: this.protocolsWhiteListRegExp, + filesCache: this.filesCache, + mathInlineDelimiters: this.config.mathInlineDelimiters, + mathBlockDelimiters: this.config.mathBlockDelimiters, + codeChunksData: this.codeChunksData, + graphsCache: this.graphsCache + }, config) + } + /** * * @param filePath @@ -920,26 +1005,57 @@ export class MarkdownEngine { if (!codeChunkData) return '' if (codeChunkData.running) return '' + let code = codeChunkData.code + let cc = codeChunkData + while (cc.options['continue']) { + let id = cc.options['continue'] + if (id === true) { + id = cc.prev + } + cc = this.codeChunksData[id] + if (!cc) break + code = cc.code + code + } + codeChunkData.running = true - let result = await CodeChunkAPI.run(codeChunkData.code, this.fileDirectoryPath, codeChunkData.options) - - const outputFormat = codeChunkData.options['output'] || 'text' - if (outputFormat === 'html') { - result = result - } else if (outputFormat === 'png') { - const base64 = new Buffer(result).toString('base64') - result = `` - } else if (outputFormat === 'markdown') { - const {html} = await this.parseMD(result, {useRelativeImagePath:true, isForPreview:false, hideFrontMatter: true} ) - result = html - } else if (outputFormat === 'none') { - result = '' - } else { - result = `
${result}
` + let result + try { + const options = codeChunkData.options + if (options['cmd'] === 'toc') { // toc code chunk. <= this is a special code chunk. + const tocObject = toc(this.headings, {ordered: options['orderedList'], depthFrom: options['depthFrom'], depthTo: options['depthTo'], tab: options['tab'] || '\t'}) + result = tocObject.content + } else { + result = await CodeChunkAPI.run(code, this.fileDirectoryPath, codeChunkData.options) + } + codeChunkData.plainResult = result + + if (codeChunkData.options['modify_source'] && ('code_chunk_offset' in codeChunkData.options)) { + codeChunkData.result = '' + return MarkdownEngine.modifySource(codeChunkData, result, this.filePath) + } + + const outputFormat = codeChunkData.options['output'] || 'text' + if (!result) { // do nothing + result = '' + } else if (outputFormat === 'html') { + result = result + } else if (outputFormat === 'png') { + const base64 = new Buffer(result).toString('base64') + result = `` + } else if (outputFormat === 'markdown') { + const {html} = await this.parseMD(result, {useRelativeFilePath:true, isForPreview:false, hideFrontMatter: true} ) + result = html + } else if (outputFormat === 'none') { + result = '' + } else { + result = `
${result}
` + } + } catch(error) { + result = `
${error}
` } - codeChunkData.running = false codeChunkData.result = result // save result. + codeChunkData.running = false return result } @@ -950,7 +1066,23 @@ export class MarkdownEngine { } return await Promise.all(asyncFunctions) } - + /** + * Add line numbers to code block
 element
+   * @param  
+   * @param code 
+   */  
+  private addLineNumbersIfNecessary($preElement, code:string):void {
+    if ($preElement.hasClass('line-numbers')) {
+      const match = code.match(/\n(?!$)/g)
+      const linesNum = match ? (match.length + 1) : 1
+      let lines = ''
+      for (let i = 0; i < linesNum; i++) {
+        lines += ''
+      }
+      $preElement.append(``)
+    }
+  }
+  
   /**
    * 
    * @param preElement the cheerio element
@@ -959,7 +1091,10 @@ export class MarkdownEngine {
    */
   private async renderCodeBlock($, $preElement, code, parameters, 
   { graphsCache, 
-    codeChunksArray}:{graphsCache:object, codeChunksArray:CodeChunkData[]}) {
+    codeChunksArray, 
+    isForPreview,
+    triggeredBySave }:{graphsCache:object, codeChunksArray:CodeChunkData[], isForPreview:boolean, triggeredBySave:boolean}) {
+    
     let match, lang, optionsStr:string, options:object 
     if (match = parameters.match(/\s*([^\s]+)\s+\{(.+?)\}/)) {
       lang = match[1]
@@ -979,23 +1114,27 @@ export class MarkdownEngine {
       options = {}
     }
 
-    function renderPlainCodeBlock() {
+    const renderPlainCodeBlock = ()=> {
       try {
         if (!Prism) {
-          Prism = require(path.resolve(getExtensionDirectoryPath(), './dependencies/prism/prism.js'))
+          Prism = require(path.resolve(extensionDirectoryPath, './dependencies/prism/prism.js'))
         }
         const html = Prism.highlight(code, Prism.languages[scopeForLanguageName(lang)])
         $preElement.html(html)  
       } catch(e) {
         // do nothing
       }
+      if (options['class']) {
+        $preElement.addClass(options['class'])
+        this.addLineNumbersIfNecessary($preElement, code)
+      }
     }
 
     const codeBlockOnly = options['code_block']
     if (codeBlockOnly) {
       renderPlainCodeBlock()
     } else if (lang.match(/^(puml|plantuml)$/)) { // PlantUML 
-      const checksum = md5(code)
+      const checksum = md5(optionsStr + code)
       let svg:string = this.graphsCache[checksum] 
       if (!svg) {
         svg = await plantumlAPI.render(code, this.fileDirectoryPath)
@@ -1004,7 +1143,7 @@ export class MarkdownEngine {
       graphsCache[checksum] = svg // store to new cache 
 
     } else if (lang.match(/^mermaid$/)) { // mermaid 
-      const checksum = md5(code) 
+      const checksum = md5(optionsStr + code) 
       let svg:string = this.graphsCache[checksum]
       if (!svg) {
         $preElement.replaceWith(`
${code}
`) @@ -1013,21 +1152,22 @@ export class MarkdownEngine { graphsCache[checksum] = svg // store to new cache } } else if (lang.match(/^(dot|viz)$/)) { // GraphViz - const checksum = md5(code) + const checksum = md5(optionsStr + code) let svg = this.graphsCache[checksum] - if (!svg) { - if (!viz) viz = require(path.resolve(extensionDirectoryPath, './dependencies/viz/viz.js')) - + if (!svg) { try { let engine = options['engine'] || "dot" - svg = viz(code, {engine}) + svg = Viz(code, {engine}) + + $preElement.replaceWith(`

${svg}

`) + graphsCache[checksum] = svg // store to new cache } catch(e) { - $preElement.replaceWith(`
${e.toString()}
`) + $preElement.replaceWith(`
${e.toString()}
`) } - } - - $preElement.replaceWith(`

${svg}

`) - graphsCache[checksum] = svg // store to new cache + } else { + $preElement.replaceWith(`

${svg}

`) + graphsCache[checksum] = svg // store to new cache + } } else if (options['cmd']) { const $el = $("
") // create code chunk if (!options['id']) { @@ -1039,21 +1179,28 @@ export class MarkdownEngine { } $el.attr({ - 'data-id': options['id'] + 'data-id': options['id'], + 'data-cmd': options['cmd'], + 'data-code': options['cmd'] === 'javascript' ? code : '' }) let highlightedBlock = '' if (!options['hide']) { try { if (!Prism) { - Prism = require(path.resolve(getExtensionDirectoryPath(), './dependencies/prism/prism.js')) + Prism = require(path.resolve(extensionDirectoryPath, './dependencies/prism/prism.js')) } - highlightedBlock = `
${Prism.highlight(code, Prism.languages[scopeForLanguageName(lang)])}
` + highlightedBlock = `
${Prism.highlight(code, Prism.languages[scopeForLanguageName(lang)])}
` } catch(e) { // do nothing - highlightedBlock = `
${code}
` + highlightedBlock = `
${code}
` } + + const $highlightedBlock = $(highlightedBlock) + this.addLineNumbersIfNecessary($highlightedBlock, code) + highlightedBlock = $.html($highlightedBlock) } + /* if (!options['id']) { // id is required for code chunk highlightedBlock = `
'id' is required for code chunk
` @@ -1067,8 +1214,10 @@ export class MarkdownEngine { code, options: options, result: '', + plainResult: '', running: false, - prev: previousCodeChunkDataId + prev: previousCodeChunkDataId, + next: null } this.codeChunksData[options['id']] = codeChunkData } else { @@ -1076,14 +1225,34 @@ export class MarkdownEngine { codeChunkData.options = options codeChunkData.prev = previousCodeChunkDataId } - codeChunksArray.push(codeChunkData) + if (previousCodeChunkDataId && this.codeChunksData[previousCodeChunkDataId]) + this.codeChunksData[previousCodeChunkDataId].next = options['id'] + + codeChunksArray.push(codeChunkData) // this line has to be put above the `if` statement. + + if (triggeredBySave && options['run_on_save']) { + await this.runCodeChunk(options['id']) + } + + let result = codeChunkData.result + // element option + if (!result && codeChunkData.options['element']) { + result = codeChunkData.options['element'] + codeChunkData.result = result + } if (codeChunkData.running) { $el.addClass('running') } const statusDiv = `
running...
` const buttonGroup = '
▶︎
all
' - const outputDiv = `
${codeChunkData.result}
` + let outputDiv = `
${result}
` + + // check javascript code chunk + if (!isForPreview && options['cmd'] === 'javascript') { + outputDiv += `` + result = codeChunkData.options['element'] || '' + } $el.append(highlightedBlock) $el.append(buttonGroup) @@ -1102,19 +1271,6 @@ export class MarkdownEngine { */ private async resolveImagePathAndCodeBlock(html, options:MarkdownEngineRenderOption) { let $ = cheerio.load(html, {xmlMode:true}) - - // resolve image paths - $('img, a').each((i, imgElement)=> { - let srcTag = 'src' - if (imgElement.name === 'a') - srcTag = 'href' - - const img = $(imgElement) - const src = img.attr(srcTag) - - img.attr(srcTag, this.resolveFilePath(src, options.useRelativeImagePath)) - }) - // new caches // which will be set when this.renderCodeBlocks is called @@ -1143,14 +1299,31 @@ export class MarkdownEngine { $preElement.attr('class', 'language-text') } - asyncFunctions.push(this.renderCodeBlock($, $preElement, code, lang, {graphsCache: newGraphsCache, codeChunksArray})) + asyncFunctions.push(this.renderCodeBlock($, $preElement, code, lang, + {graphsCache: newGraphsCache, codeChunksArray, isForPreview:options.isForPreview, triggeredBySave: options.triggeredBySave})) }) await Promise.all(asyncFunctions) + + // resolve image paths + $('img, a').each((i, imgElement)=> { + let srcTag = 'src' + if (imgElement.name === 'a') + srcTag = 'href' + + const img = $(imgElement) + const src = img.attr(srcTag) + + img.attr(srcTag, this.resolveFilePath(src, options.useRelativeFilePath)) + }) + // reset caches // the line below actually has problem. - this.graphsCache = newGraphsCache + if (options.isForPreview) { + this.graphsCache = newGraphsCache + } + return $.html() } @@ -1326,7 +1499,7 @@ export class MarkdownEngine { ` } - private parseSlidesForExport(html:string, slideConfigs:Array, useRelativeImagePath:boolean) { + private parseSlidesForExport(html:string, slideConfigs:Array, useRelativeFilePath:boolean) { let slides = html.split('') let before = slides[0] slides = slides.slice(1) @@ -1337,7 +1510,7 @@ export class MarkdownEngine { let attrString = '' if (slideConfig['data-background-image']) - attrString += ` data-background-image='${this.resolveFilePath(slideConfig['data-background-image'], useRelativeImagePath)}'` + attrString += ` data-background-image='${this.resolveFilePath(slideConfig['data-background-image'], useRelativeFilePath)}'` if (slideConfig['data-background-size']) attrString += ` data-background-size='${slideConfig['data-background-size']}'` @@ -1355,7 +1528,7 @@ export class MarkdownEngine { attrString += ` data-notes='${slideConfig['data-notes']}'` if (slideConfig['data-background-video']) - attrString += ` data-background-video='${this.resolveFilePath(slideConfig['data-background-video'], useRelativeImagePath)}'` + attrString += ` data-background-video='${this.resolveFilePath(slideConfig['data-background-video'], useRelativeFilePath)}'` if (slideConfig['data-background-video-loop']) attrString += ` data-background-video-loop` @@ -1367,7 +1540,7 @@ export class MarkdownEngine { attrString += ` data-transition='${slideConfig['data-transition']}'` if (slideConfig['data-background-iframe']) - attrString += ` data-background-iframe='${this.resolveFilePath(slideConfig['data-background-iframe'], useRelativeImagePath)}'` + attrString += ` data-background-iframe='${this.resolveFilePath(slideConfig['data-background-iframe'], useRelativeFilePath)}'` return attrString } @@ -1404,6 +1577,8 @@ export class MarkdownEngine { } public async parseMD(inputString:string, options:MarkdownEngineRenderOption):Promise { + if (!inputString) inputString = await utility.readFile(this.filePath, {encoding:'utf-8'}) + // process front-matter const fm = this.processFrontMatter(inputString, options.hideFrontMatter) const frontMatterTable = fm.table, @@ -1411,13 +1586,13 @@ export class MarkdownEngine { inputString = fm.content // import external files and insert anchors if necessary - const {outputString, slideConfigs, tocBracketEnabled} = await transformMarkdown(inputString, + const {outputString, slideConfigs, tocBracketEnabled, JSAndCssFiles} = await transformMarkdown(inputString, { fileDirectoryPath: this.fileDirectoryPath, projectDirectoryPath: this.projectDirectoryPath, forPreview: options.isForPreview, protocolsWhiteListRegExp: this.protocolsWhiteListRegExp, - useRelativeImagePath: options.useRelativeImagePath, + useRelativeFilePath: options.useRelativeFilePath, filesCache: this.filesCache }) @@ -1518,12 +1693,12 @@ export class MarkdownEngine { if (options.isForPreview) { html = this.parseSlides(html, slideConfigs, yamlConfig) } else { - html = this.parseSlidesForExport(html, slideConfigs, options.useRelativeImagePath) + html = this.parseSlidesForExport(html, slideConfigs, options.useRelativeFilePath) } if (yamlConfig) yamlConfig['isPresentationMode'] = true // mark as presentation mode } this.cachedHTML = html // save to cache - return {html, markdown:inputString, tocHTML: this.tocHTML, yamlConfig} + return {html, markdown:inputString, tocHTML: this.tocHTML, yamlConfig, JSAndCssFiles} } } \ No newline at end of file diff --git a/src/markdown-preview-enhanced-view.ts b/src/markdown-preview-enhanced-view.ts index 6ba85e6..0cb2af6 100644 --- a/src/markdown-preview-enhanced-view.ts +++ b/src/markdown-preview-enhanced-view.ts @@ -2,10 +2,13 @@ import * as vscode from 'vscode' import * as path from 'path' import {Uri, CancellationToken, Event, ProviderResult, TextEditor} from 'vscode' +import * as mpe from "./mpe" import {MarkdownEngine} from './markdown-engine' import {MarkdownPreviewEnhancedConfig} from './config' import * as utility from './utility' +let singlePreviewSouceUri:Uri = null + // http://www.typescriptlang.org/play/ // https://github.com/Microsoft/vscode/blob/master/extensions/markdown/media/main.js // https://github.com/Microsoft/vscode/tree/master/extensions/markdown/src @@ -21,10 +24,113 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr */ private engineMaps:{[key:string]: MarkdownEngine} = {} + /** + * The key is markdown file fsPath + * value is JSAndCssFiles + */ + private jsAndCssFilesMaps: {[key:string]: string[]} = {} + private config:MarkdownPreviewEnhancedConfig public constructor(private context: vscode.ExtensionContext) { this.config = MarkdownPreviewEnhancedConfig.getCurrentConfig() + + mpe.init() // init markdown-preview-enhanced + .then(()=> { + mpe.onDidChangeConfigFile(this.refreshAllPreviews.bind(this)) + + MarkdownEngine.onModifySource(this.modifySource.bind(this)) + }) + } + + private refreshAllPreviews() { + vscode.workspace.textDocuments.forEach(document => { + if (document.uri.scheme === 'markdown-preview-enhanced') { + // this.update(document.uri); + this._onDidChange.fire(document.uri) + } + }) + } + + /** + * modify markdown source, append `result` after corresponding code chunk. + * @param codeChunkData + * @param result + * @param filePath + */ + private async modifySource(codeChunkData:CodeChunkData, result:string, filePath:string):Promise { + function insertResult(i:number, editor:TextEditor) { + const lineCount = editor.document.lineCount + if (i + 1 < lineCount && editor.document.lineAt(i + 1).text.startsWith('')) { + // TODO: modify exited output + let start = i + 1 + let end = i + 2 + while (end < lineCount) { + if (editor.document.lineAt(end).text.startsWith('')){ + break + } + end += 1 + } + + // if output not changed, then no need to modify editor buffer + let r = "" + for (let i = start+2; i < end-1; i++) { + r += editor.document.lineAt(i).text+'\n' + } + if (r === result+'\n') return "" // no need to modify output + + editor.edit((edit)=> { + edit.replace(new vscode.Range( + new vscode.Position(start + 2, 0), + new vscode.Position(end-1, 0) + ), result+'\n') + }) + return "" + } else { + editor.edit((edit)=> { + edit.insert(new vscode.Position(i+1, 0), `\n\n${result}\n\n\n`) + }) + return "" + } + } + + const visibleTextEditors = vscode.window.visibleTextEditors + for (let i = 0; i < visibleTextEditors.length; i++) { + const editor = visibleTextEditors[i] + if (editor.document.uri.fsPath === filePath) { + + let codeChunkOffset = 0, + targetCodeChunkOffset = codeChunkData.options['code_chunk_offset'] + + const lineCount = editor.document.lineCount + for (let i = 0; i < lineCount; i++) { + const line = editor.document.lineAt(i) + if (line.text.match(/^```(.+)\"?cmd\"?\s*\:/)) { + if (codeChunkOffset === targetCodeChunkOffset) { + i = i + 1 + while (i < lineCount) { + if (editor.document.lineAt(i).text.match(/^\`\`\`\s*/)) { + break + } + i += 1 + } + return insertResult(i, editor) + } else { + codeChunkOffset++ + } + } else if (line.text.match(/\@import\s+(.+)\"?cmd\"?\s*\:/)) { + if (codeChunkOffset === targetCodeChunkOffset) { + // console.log('find code chunk' ) + return insertResult(i, editor) + } else { + codeChunkOffset++ + } + } + } + break + } + } + return "" } /** @@ -51,6 +157,8 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr * @param previewUri */ public destroyEngine(previewUri: Uri) { + delete(previewUri['markdown_source']) + if (useSinglePreview()) { return this.engineMaps = {} } @@ -96,28 +204,13 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr // mermaid scripts += `` - + scripts += `` + // math if (this.config.mathRenderingOption === 'MathJax') { - const mathJaxConfig = { - extensions: ['tex2jax.js'], - jax: ['input/TeX','output/HTML-CSS'], - showMathMenu: false, - messageStyle: 'none', - - tex2jax: { - inlineMath: this.config.mathInlineDelimiters, - displayMath: this.config.mathBlockDelimiters, - processEnvironments: false, - processEscapes: true, - preview: "none" - }, - TeX: { - extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js'] - }, - 'HTML-CSS': { availableFonts: ['TeX'] }, - skipStartupTypeset: true - } + const mathJaxConfig = mpe.extensionConfig.mathjaxConfig + mathJaxConfig['tex2jax']['inlineMath'] = this.config.mathInlineDelimiters + mathJaxConfig['tex2jax']['displayMath'] = this.config.mathBlockDelimiters scripts += `` scripts += `` @@ -159,19 +252,46 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr // check preview theme styles += `` + // global styles + styles += `` + return styles } + private getJSAndCssFiles(fsPath:string) { + if (!this.jsAndCssFilesMaps[fsPath]) return '' + + let output = '' + this.jsAndCssFilesMaps[fsPath].forEach((sourcePath)=> { + let absoluteFilePath = sourcePath + if (sourcePath[0] === '/') { + absoluteFilePath = 'file://' + path.resolve(vscode.workspace.rootPath, '.' + sourcePath) + } else if (sourcePath.match(/^file:\/\//) || sourcePath.match(/^https?\:\/\//)) { + // do nothing + } else { + absoluteFilePath = 'file://' + path.resolve(path.dirname(fsPath), sourcePath) + } + + if (absoluteFilePath.endsWith('.js')) { + output += `` + } else { // css + output += `` + } + }) + return output + } + public provideTextDocumentContent(previewUri: Uri) : Thenable { // console.log(sourceUri, uri, vscode.workspace.rootPath) - let sourceUri + let sourceUri:Uri if (useSinglePreview()) { - sourceUri = vscode.window.activeTextEditor.document.uri + sourceUri = singlePreviewSouceUri } else { sourceUri = vscode.Uri.parse(previewUri.query) } + // console.log('open preview for source: ' + sourceUri.toString()) let initialLine: number | undefined = undefined; @@ -195,7 +315,7 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr html = engine.getCachedHTML() } - return ` + const htmlTemplate = ` @@ -203,6 +323,7 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr ${this.getStyles()} ${this.getScripts()} + ${this.getJSAndCssFiles(sourceUri.fsPath)} @@ -215,7 +336,7 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr
⬆︎
⟳︎
- +
@@ -252,10 +373,12 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr ` + + return htmlTemplate }) } - public updateMarkdown(sourceUri:Uri) { + public updateMarkdown(sourceUri:Uri, triggeredBySave?:boolean) { const engine = this.getEngine(sourceUri) // console.log('updateMarkdown: ' + Object.keys(this.engineMaps).length) if (!engine) return @@ -270,17 +393,26 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr type: 'start-parsing-markdown', }) - engine.parseMD(text, {isForPreview: true, useRelativeImagePath: false, hideFrontMatter: false}).then(({markdown, html, tocHTML})=> { - vscode.commands.executeCommand( - '_workbench.htmlPreview.postMessage', - getPreviewUri(sourceUri), - { - type: 'update-html', - html: html, - tocHTML: tocHTML, - totalLineCount: document.lineCount, - sourceUri: sourceUri.toString() - }) + engine.parseMD(text, {isForPreview: true, useRelativeFilePath: false, hideFrontMatter: false, triggeredBySave}) + .then(({markdown, html, tocHTML, JSAndCssFiles})=> { + + // check JSAndCssFiles + if (JSON.stringify(JSAndCssFiles) !== JSON.stringify(this.jsAndCssFilesMaps[sourceUri.fsPath])) { + this.jsAndCssFilesMaps[sourceUri.fsPath] = JSAndCssFiles + // restart iframe + this._onDidChange.fire(getPreviewUri(sourceUri)) + } else { + vscode.commands.executeCommand( + '_workbench.htmlPreview.postMessage', + getPreviewUri(sourceUri), + { + type: 'update-html', + html: html, + tocHTML: tocHTML, + totalLineCount: document.lineCount, + sourceUri: sourceUri.toString() + }) + } }) }) } @@ -345,6 +477,32 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr } } + public pandocExport(sourceUri) { + const engine = this.getEngine(sourceUri) + if (engine) { + engine.pandocExport() + .then((dest)=> { + vscode.window.showInformationMessage(`Document ${path.basename(dest)} was created as path: ${dest}`) + }) + .catch((error)=> { + vscode.window.showErrorMessage(error) + }) + } + } + + public markdownExport(sourceUri) { + const engine = this.getEngine(sourceUri) + if (engine) { + engine.markdownExport() + .then((dest)=> { + vscode.window.showInformationMessage(`Document ${path.basename(dest)} was created as path: ${dest}`) + }) + .catch((error)=> { + vscode.window.showErrorMessage(error) + }) + } + } + public cacheSVG(sourceUri: Uri, code:string, svg:string) { const engine = this.getEngine(sourceUri) if (engine) { @@ -352,6 +510,13 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr } } + public cacheCodeChunkResult(sourceUri: Uri, id:string, result:string) { + const engine = this.getEngine(sourceUri) + if (engine) { + engine.cacheCodeChunkResult(id, result) + } + } + public runCodeChunk(sourceUri: Uri, codeChunkId: string) { const engine = this.getEngine(sourceUri) if (engine) { @@ -401,7 +566,6 @@ export class MarkdownPreviewEnhancedView implements vscode.TextDocumentContentPr // update all generated md documents vscode.workspace.textDocuments.forEach(document => { if (document.uri.scheme === 'markdown-preview-enhanced') { - console.log(document.uri) // this.update(document.uri); this._onDidChange.fire(document.uri) } @@ -439,19 +603,23 @@ export function getPreviewUri(uri: vscode.Uri) { if (uri.scheme === 'markdown-preview-enhanced') { return uri } - + + + let previewUri:Uri if (useSinglePreview()) { - return uri.with({ + previewUri = uri.with({ + scheme: 'markdown-preview-enhanced', + path: 'single-preview.rendered', + }) + singlePreviewSouceUri = uri + } else { + previewUri = uri.with({ scheme: 'markdown-preview-enhanced', - path: 'single-preview.rendered' + path: uri.path + '.rendered', + query: uri.toString() }) } - - return uri.with({ - scheme: 'markdown-preview-enhanced', - path: uri.path + '.rendered', - query: uri.toString() - }); + return previewUri } diff --git a/src/markdown-preview-enhanced-webview.ts b/src/markdown-preview-enhanced-webview.ts index fafbfd9..b35d1de 100644 --- a/src/markdown-preview-enhanced-webview.ts +++ b/src/markdown-preview-enhanced-webview.ts @@ -343,8 +343,18 @@ function initContextMenu() { } } }, - "pandoc_export": {name: "Pandoc (not done)"}, - "save_as_markdown": {name: "Save as Markdown (not done)"}, + "pandoc_export": { + name: "Pandoc", + callback: ()=> { + window.parent.postMessage({ command: 'did-click-link', data: `command:_markdown-preview-enhanced.pandocExport?${JSON.stringify([sourceUri])}`}, 'file://') + } + }, + "save_as_markdown": { + name: "Save as Markdown", + callback: ()=> { + window.parent.postMessage({ command: 'did-click-link', data: `command:_markdown-preview-enhanced.markdownExport?${JSON.stringify([sourceUri])}`}, 'file://') + } + }, "sep2": "---------", "sync_source": {name: "Sync Source (not done)"} } @@ -523,10 +533,29 @@ function runCodeChunk(id:string) { if (running) return codeChunk.classList.add('running') - window.parent.postMessage({ - command: 'did-click-link', // <= this has to be `did-click-link` to post message - data: `command:_markdown-preview-enhanced.runCodeChunk?${JSON.stringify([sourceUri, id])}` - }, 'file://') + if (codeChunk.getAttribute('data-cmd') === 'javascript') { // javascript code chunk + const code = codeChunk.getAttribute('data-code') + try { + eval(`((function(){${code}$})())`) + codeChunk.classList.remove('running') // done running javascript code + + const CryptoJS = window["CryptoJS"] + const result = CryptoJS.AES.encrypt(codeChunk.getElementsByClassName('output-div')[0].outerHTML, "markdown-preview-enhanced").toString() + + window.parent.postMessage({ + command: 'did-click-link', // <= this has to be `did-click-link` to post message + data: `command:_markdown-preview-enhanced.cacheCodeChunkResult?${JSON.stringify([sourceUri, id, result])}` + }, 'file://') + } catch(e) { + const outputDiv = codeChunk.getElementsByClassName('output-div')[0] + outputDiv.innerHTML = `
${e.toString()}
` + } + } else { + window.parent.postMessage({ + command: 'did-click-link', // <= this has to be `did-click-link` to post message + data: `command:_markdown-preview-enhanced.runCodeChunk?${JSON.stringify([sourceUri, id])}` + }, 'file://') + } } function runAllCodeChunks() { @@ -638,6 +667,27 @@ async function initEvents() { mpe.refreshingIcon.style.display = "none" } +function bindTagAClickEvent() { + const as = mpe.previewElement.getElementsByTagName('a') + for (let i = 0; i < as.length; i++) { + const a = as[i] + const href = a.getAttribute('href') + if (href && href[0] === '#') { + // anchor, do nothing + } else { + a.onclick = (event)=> { + event.preventDefault() + event.stopPropagation() + + window.parent.postMessage({ + command: 'did-click-link', // <= this has to be `did-click-link` to post message + data: `command:_markdown-preview-enhanced.clickTagA?${JSON.stringify([sourceUri, href])}` + }, 'file://') + } + } + } +} + /** * update previewElement innerHTML content * @param html @@ -666,6 +716,8 @@ function updateHTML(html) { // init several events initEvents().then(()=> { mpe.scrollMap = null + + bindTagAClickEvent() // scroll to initial position if (!mpe.doneLoadingPreview) { diff --git a/src/mermaid.ts b/src/mermaid.ts new file mode 100644 index 0000000..a735637 --- /dev/null +++ b/src/mermaid.ts @@ -0,0 +1,30 @@ +/** + * A wrapper of mermaid CLI + * http://knsv.github.io/mermaid/#mermaid-cli + * But it doesn't work well + */ + +import * as fs from "fs" +import * as path from "path" +import {execFile} from "child_process" +import * as temp from "temp" + +import * as utility from "./utility" + +export async function mermaidToPNG(mermaidCode:string, pngFilePath:string, css="mermaid.css"):Promise { + const info = await utility.tempOpen({prefix: 'mpe-mermaid', suffix: '.mermaid'}) + await utility.write(info.fd, mermaidCode) + try { + await utility.execFile('mermaid', + [ info.path, '-p', + '-o', path.dirname(info.path), + '--css', path.resolve(utility.extensionDirectoryPath, './dependencies/mermaid/'+ css) + ]) + console.log(info.path) + fs.createReadStream(info.path + '.png').pipe(fs.createWriteStream(pngFilePath)) + fs.unlink(info.path + '.png', ()=>{}) + return pngFilePath + } catch(error) { + throw "mermaid CLI is required to be installed.\nCheck http://knsv.github.io/mermaid/#mermaid-cli for more information.\n\n" + error.toString() + } +} \ No newline at end of file diff --git a/src/mpe.ts b/src/mpe.ts new file mode 100644 index 0000000..95c8d66 --- /dev/null +++ b/src/mpe.ts @@ -0,0 +1,75 @@ +/** + * The core of markdown preview enhanced package. + */ +import * as fs from "fs" +import * as path from "path" +import * as os from "os" +import * as less from "less" + +import * as utility from "./utility" + +let INITIALIZED = false +let CONFIG_CHANGE_CALLBACK:()=>void = null + +/** + * style.less, mathjax_config.js, and mermaid_config.js files + */ +export const extensionConfig:{globalStyle:string, mathjaxConfig:object, mermaidConfig: object} = { + globalStyle: "", + mathjaxConfig: null, + mermaidConfig: null +} + +/** + * init markdown-preview-enhanced config folder at ~/.markdown-preview-enhanced + */ +export async function init():Promise { + if (INITIALIZED) return + + const homeDir = os.homedir() + const extensionConfigDirectoryPath = path.resolve(homeDir, './.markdown-preview-enhanced') + if (!fs.existsSync(extensionConfigDirectoryPath)) { + fs.mkdirSync(extensionConfigDirectoryPath) + } + + extensionConfig.globalStyle = await utility.getGlobalStyles() + extensionConfig.mermaidConfig = await utility.getMermaidConfig() + extensionConfig.mathjaxConfig = await utility.getMathJaxConfig() + + fs.watch(extensionConfigDirectoryPath, (eventType, fileName)=> { + if (eventType === 'change' && CONFIG_CHANGE_CALLBACK) { + if (fileName === 'style.less') { // || fileName==='mermaid_config.js' || fileName==='mathjax_config') + utility.getGlobalStyles() + .then((css)=> { + extensionConfig.globalStyle = css + CONFIG_CHANGE_CALLBACK() + }) + } else if (fileName === 'mermaid_config.js') { + utility.getMermaidConfig() + .then((mermaidConfig)=> { + extensionConfig.mermaidConfig = mermaidConfig + CONFIG_CHANGE_CALLBACK() + }) + } else if (fileName === 'mathjax_config.js') { + utility.getMathJaxConfig() + .then((mathjaxConfig)=> { + extensionConfig.mathjaxConfig = mathjaxConfig + CONFIG_CHANGE_CALLBACK() + }) + } + } + }) + + INITIALIZED = true + return +} + + +/** + * cb will be called when global style.less file is changed. + * @param cb function(error, css){} + */ + +export function onDidChangeConfigFile(cb:()=>void) { + CONFIG_CHANGE_CALLBACK = cb +} diff --git a/src/pandoc-convert.ts b/src/pandoc-convert.ts new file mode 100644 index 0000000..736e473 --- /dev/null +++ b/src/pandoc-convert.ts @@ -0,0 +1,337 @@ +import * as path from "path" +import * as fs from "fs" +import {execFile} from "child_process" +import * as mkdirp from "mkdirp" + +const matter = require('gray-matter') + +import {transformMarkdown} from "./transformer" +import {processGraphs} from "./process-graphs" +import * as utility from "./utility" +const md5 = require(path.resolve(utility.extensionDirectoryPath, './dependencies/javascript-md5/md5.js')) + +function getFileExtension(documentType:string) { + if (documentType === 'pdf_document' || documentType === 'beamer_presentation') + return 'pdf' + else if (documentType === 'word_document') + return 'docx' + else if (documentType === 'rtf_document') + return 'rtf' + else if (documentType === 'custom_document') + return '*' + else + return null +} + +/** + * eg: process config inside pdf_document block + */ +function processOutputConfig(config:object, args:string[]) { + if (config['toc']) + args.push('--toc') + + if (config['toc_depth']) + args.push('--toc-depth='+config['toc_depth']) + + if (config['highlight']) { + if (config['highlight'] == 'default') + config['highlight'] = 'pygments' + args.push('--highlight-style='+config['highlight']) + } + + if (config['reference_docx']) { // issue #448 + args.push('--reference_docx=' + config['reference_docx']) + } + + if (config['highlight'] === null) + args.push('--no-highlight') + + if (config['pandoc_args']) + config['pandoc_args'].forEach((arg)=> args.push(arg)) + + if (config['citation_package']) { + if (config['citation_package'] === 'natbib') + args.push('--natbib') + else if (config['citation_package'] == 'biblatex') + args.push('--biblatex') + } + + if (config['number_sections']) + args.push('--number-sections') + + if (config['incremental']) + args.push('--incremental') + + if (config['slide_level']) + args.push('--slide-level='+config['slide_level']) + + if (config['theme']) + args.push('-V', 'theme:'+config['theme']) + + if (config['colortheme']) + args.push('-V', 'colortheme:'+config['colortheme']) + + if (config['fonttheme']) + args.push('-V', 'fonttheme:'+config['fonttheme']) + + if (config['latex_engine']) + args.push('--latex-engine='+config['latex_engine']) + + if (config['includes'] && typeof(config['includes']) === 'object') { + let includesConfig = config['includes'] + const helper = (prefix, data)=> { + if (typeof(data) == 'string') + args.push(prefix+data) + else if (data.constructor === Array) { + data.forEach((d)=> args.push(prefix+d)) + } + else + args.push(prefix+data) + } + + // TODO: includesConfig['in_header'] is array + if (includesConfig['in_header']) + helper('--include-in-header=', includesConfig['in_header']) + if (includesConfig['before_body']) + helper('--include-before-body=', includesConfig['before_body']) + if (includesConfig['after_body']) + helper('--include-after-body=', includesConfig['after_body']) + } + + if (config['template']) + args.push('--template=' + config['template']) +} + +function loadOutputYAML(fileDirectoryPath, config) { + const yamlPath = path.resolve(fileDirectoryPath, '_output.yaml') + let yaml:string = "" + try { + yaml = fs.readFileSync(yamlPath, {encoding: 'utf-8'}) + } catch (error) { + return Object.assign({}, config) + } + + let data:any = matter('---\n'+yaml+'---\n').data + data = data || {} + + if (config['output']) { + if (typeof(config['output']) === 'string' && data[config['output']]) { + const format = config['output'] + config['output'] = {} + config['output'][format] = data[format] + } else { + const format = Object.keys(config['output'])[0] + if (data[format]) + config['output'][format] = Object.assign({}, data[format], config['output'][format]) + } + } + return Object.assign({}, data, config) +} + +/* +function processConfigPaths(config, fileDirectoryPath, projectDirectoryPath)-> + # same as the one in processPaths function + # TODO: refactor in the future + resolvePath = (src)-> + if src.startsWith('/') + return path.relative(fileDirectoryPath, path.resolve(projectDirectoryPath, '.'+src)) + else # ./test.png or test.png + return src + + helper = (data)-> + if typeof(data) == 'string' + return resolvePath(data) + else if data.constructor == Array + return data.map (d)->resolvePath(d) + else + data + + if config['bibliography'] + config['bibliography'] = helper(config['bibliography']) + + if config['csl'] + config['csl'] = helper(config['csl']) + + if config['output'] and typeof(config['output']) == 'object' + documentFormat = Object.keys(config['output'])[0] + outputConfig = config['output'][documentFormat] + if outputConfig['includes'] + if outputConfig['includes']['in_header'] + outputConfig['includes']['in_header'] = helper(outputConfig['includes']['in_header']) + if outputConfig['includes']['before_body'] + outputConfig['includes']['before_body'] = helper(outputConfig['includes']['before_body']) + if outputConfig['includes']['after_body'] + outputConfig['includes']['after_body'] = helper(outputConfig['includes']['after_body']) + + if outputConfig['reference_docx'] + outputConfig['reference_docx'] = helper(outputConfig['reference_docx']) + + if outputConfig['template'] + outputConfig['template'] = helper(outputConfig['template']) +*/ + +function processPaths(text, fileDirectoryPath, projectDirectoryPath) { + let match = null, + offset = 0, + output = '' + + function resolvePath(src) { + if (src.startsWith('/')) + return path.relative(fileDirectoryPath, path.resolve(projectDirectoryPath, '.'+src)) + else // ./test.png or test.png + return src + } + + let inBlock = false + let lines = text.split('\n') + lines = lines.map((line)=> { + if (line.match(/^\s*```/)) { + inBlock = !inBlock + return line + } else if (inBlock) { + return line + } else { + // replace path in ![](...) and []() + let r = /(\!?\[.*?]\()([^\)|^'|^"]*)(.*?\))/gi + line = line.replace(r, (whole, a, b, c)=> { + if (b[0] === '<') { + b = b.slice(1, b.length-1) + return a + '<' + resolvePath(b.trim()) + '> ' + c + } else { + return a + resolvePath(b.trim()) + ' ' + c + } + }) + + // replace path in tag + r = /(<[img|a|iframe].*?[src|href]=['"])(.+?)(['"].*?>)/gi + line = line.replace(r, (whole, a, b, c)=> { + return a + resolvePath(b) + c + }) + return line + } + }) + + return lines.join('\n') +} + +/* +@param {String} text: markdown string +@param {Object} all properties are required! + @param {String} fileDirectoryPath + @param {String} projectDirectoryPath + @param {String} sourceFilePath +callback(err, outputFilePath) +*/ +/** + * @return outputFilePath + */ +export async function pandocConvert(text, + {fileDirectoryPath, projectDirectoryPath, sourceFilePath, filesCache, protocolsWhiteListRegExp, /*deleteImages=true,*/ codeChunksData, graphsCache, imageDirectoryPath}, + config={}):Promise { + + config = loadOutputYAML(fileDirectoryPath, config) + // TODO => + // args = ['-f', atom.config.get('markdown-preview-enhanced.pandocMarkdownFlavor').replace(/\-raw\_tex/, '')] + const args = [] + + let extension = null + let outputConfig = null + let documentFormat = null + if (config['output']) { + if (typeof(config['output']) == 'string') { + documentFormat = config['output'] + extension = getFileExtension(documentFormat) + } + else { + documentFormat = Object.keys(config['output'])[0] + extension = getFileExtension(documentFormat) + outputConfig = config['output'][documentFormat] + } + } else { + throw "Output format needs to be specified." + } + + if (extension === null) throw "Invalid document type." + + // custom_document requires path to be defined + if (documentFormat == 'custom_document' && (!outputConfig || !outputConfig['path'])) + throw 'custom_document requires path to be defined.' + + if (documentFormat === 'beamer_presentation') + args.push('-t', 'beamer') + + // dest + let outputFilePath + if (outputConfig && outputConfig['path']) { + outputFilePath = outputConfig['path'] + if (outputFilePath.startsWith('/')) + outputFilePath = path.resolve(projectDirectoryPath, '.'+outputFilePath) + else + outputFilePath = path.resolve(fileDirectoryPath, outputFilePath) + + if (documentFormat !== 'custom_document' && path.extname(outputFilePath) !== '.' + extension) + throw ('Invalid extension for ' + documentFormat + '. Extension .' + extension + ' is required, but ' + path.extname(outputFilePath) + ' was provided.') + + args.push('-o', outputFilePath) + } else { + outputFilePath = sourceFilePath + outputFilePath = outputFilePath.slice(0, outputFilePath.length - path.extname(outputFilePath).length) + '.' + extension + args.push('-o', outputFilePath) + } + + // NOTE: 0.12.4 No need to resolve paths. + // #409: https://github.com/shd101wyy/markdown-preview-enhanced/issues/409 + // resolve paths in front-matter(yaml) + // processConfigPaths config, fileDirectoryPath, projectDirectoryPath + + if (outputConfig) + processOutputConfig(outputConfig, args) + + // add front-matter(yaml) to text + text = matter.stringify(text, config) + + // import external files + let data = await transformMarkdown(text, {fileDirectoryPath, projectDirectoryPath, useRelativeFilePath:true, filesCache, protocolsWhiteListRegExp, forPreview: false}) + text = data.outputString + + // change link path to relative path + text = processPaths(text, fileDirectoryPath, projectDirectoryPath) + + // change working directory + const cwd = process.cwd() + process.chdir(fileDirectoryPath) + + // citation + if (config['bibliography'] || config['references']) + args.push('--filter', 'pandoc-citeproc') + + if (imageDirectoryPath[0] === '/') + imageDirectoryPath = path.resolve(projectDirectoryPath, '.' + imageDirectoryPath) + else + imageDirectoryPath = path.resolve(fileDirectoryPath, imageDirectoryPath) + await utility.mkdirp(imageDirectoryPath) // create imageDirectoryPath + + const {outputString, imagePaths} = await processGraphs(text, + {fileDirectoryPath, projectDirectoryPath, imageDirectoryPath, imageFilePrefix: md5(outputFilePath), useRelativeFilePath:true, codeChunksData, graphsCache}) + + // pandoc will cause error if directory doesn't exist, + // therefore I will create directory first. + await utility.mkdirp(path.dirname(outputFilePath)) + + return await new Promise((resolve, reject)=> { + // const pandocPath = atom.config.get('markdown-preview-enhanced.pandocPath') + const pandocPath = 'pandoc' + const program = execFile(pandocPath, args, (error)=> { + /*if (deleteImages) { + imagePaths.forEach((p)=> fs.unlink(p, (error)=>{})) + }*/ + + process.chdir(cwd) // change cwd back + + if (error) return reject(error.toString()) + return resolve(outputFilePath) + }) + + program.stdin.end(outputString) + }) +} \ No newline at end of file diff --git a/src/pdf.ts b/src/pdf.ts new file mode 100644 index 0000000..376178a --- /dev/null +++ b/src/pdf.ts @@ -0,0 +1,88 @@ +// `pdf2svg` is required to be installed +// http://www.cityinthesky.co.uk/opensource/pdf2svg/ +// + +import * as path from "path" +import * as fs from "fs" +import {spawn} from "child_process" +import * as temp from "temp" +// import * as gm from "gm" +// gm.subClass({imageMagick: true}) + +import * as utility from "./utility" +const extensionDirectoryPath = utility.extensionDirectoryPath +const md5 = require(path.resolve(extensionDirectoryPath, './dependencies/javascript-md5/md5.js')) + +let SVG_DIRECTORY_PATH:string = null + + +export function toSVGMarkdown(pdfFilePath:string, {svgDirectoryPath, markdownDirectoryPath, svgZoom, svgWidth, svgHeight}:{ + markdownDirectoryPath: string, + svgDirectoryPath?: string, + svgZoom?: string, + svgWidth?: string, + svgHeight?: string +}):Promise { +return new Promise((resolve, reject)=> { + if (!svgDirectoryPath) { + if (!SVG_DIRECTORY_PATH) SVG_DIRECTORY_PATH = temp.mkdirSync('mpe_pdf') + svgDirectoryPath = SVG_DIRECTORY_PATH + } + + const svgFilePrefix = md5(pdfFilePath) + '_' + + const task = spawn('pdf2svg', [pdfFilePath, path.resolve(svgDirectoryPath, svgFilePrefix + '%d.svg'), 'all']) + const chunks = [] + task.stdout.on('data', (chunk)=> { + chunks.push(chunk) + }) + + const errorChunks = [] + task.stderr.on('data', (chunk)=> { + errorChunks.push(chunk) + }) + + task.on('error', (error)=> { + errorChunks.push(Buffer.from(error.toString(), 'utf-8')) + }) + + task.on('close', ()=> { + if (errorChunks.length) { + return reject(Buffer.concat(errorChunks).toString()) + } else { + fs.readdir(svgDirectoryPath, (error, items)=> { + if (error) + return reject(error.toString()) + + let svgMarkdown = '' + const r = Math.random() + items.forEach((fileName)=> { + let match + if (match = fileName.match(new RegExp(`^${svgFilePrefix}(\\d+)\.svg`))) { + const svgFilePath = path.relative(markdownDirectoryPath, path.resolve(svgDirectoryPath, fileName)) + + // nvm, the converted result looks so ugly + /* + const pngFilePath = svgFilePath.replace(/\.svg$/, '.png') + + // convert svg to png + gm(svgFilePath) + .noProfile() + .write(pngFilePath, function(error) { + console.log(error, pngFilePath) + }) + */ + + if (svgZoom || svgWidth || svgHeight) + svgMarkdown += `` + else + svgMarkdown += `![](${svgFilePath}?${r})\n` + } + }) + + return resolve(svgMarkdown) + }) + } + }) +}) +} \ No newline at end of file diff --git a/src/process-graphs.ts b/src/process-graphs.ts new file mode 100644 index 0000000..150cc3a --- /dev/null +++ b/src/process-graphs.ts @@ -0,0 +1,202 @@ +import * as path from "path" +import * as fs from "fs" +import * as cheerio from "cheerio" + +import * as plantumlAPI from "./puml" +import * as utility from "./utility" +import {svgElementToPNGFile} from "./magick" +import {mermaidToPNG} from "./mermaid" +const Viz = require(path.resolve(utility.extensionDirectoryPath, './dependencies/viz/viz.js')) +const jsonic = require(path.resolve(utility.extensionDirectoryPath, './dependencies/jsonic/jsonic.js')) +const md5 = require(path.resolve(utility.extensionDirectoryPath, './dependencies/javascript-md5/md5.js')) + +export async function processGraphs(text:string, +{fileDirectoryPath, projectDirectoryPath, imageDirectoryPath, imageFilePrefix, useRelativeFilePath, codeChunksData, graphsCache}: +{fileDirectoryPath:string, projectDirectoryPath:string, imageDirectoryPath:string, imageFilePrefix:string, useRelativeFilePath:boolean, codeChunksData: {[key:string]: CodeChunkData}, graphsCache:{[key:string]:string}}) +:Promise<{outputString:string, imagePaths: string[]}> { + let lines = text.split('\n') + const codes:Array<{start:number, end:number, content:string, options:object, optionsStr:string}> = [] + + let i = 0 + while (i < lines.length) { + const line = lines[i] + const trimmedLine = line.trim() + + if (trimmedLine.match(/^```(.+)\"?cmd\"?\:/) || // code chunk + trimmedLine.match(/^```(puml|plantuml|dot|viz|mermaid)/)) { // graphs + const numOfSpacesAhead = line.match(/^\s*/).length + let j = i + 1 + let content = '' + while (j < lines.length) { + if (lines[j].trim() == '```' && lines[j].match(/^\s*/).length == numOfSpacesAhead) { + let options = {}, + optionsStr = '', + optionsMatch + if (optionsMatch = trimmedLine.match(/\{(.+)\}$/)) { + try { + options = jsonic(optionsMatch[0]) + optionsStr = optionsMatch[1] + } catch(error) { + options = {} + } + } + + codes.push({ + start: i, + end: j, + content, + options, + optionsStr + }) + i = j + break + } + content += (lines[j]+'\n') + j += 1 + } + } else if (trimmedLine.match(/^```\S/)) { // remove {...} after ```lang + const indexOfFirstSpace = line.indexOf(' ', line.indexOf('```')) + if (indexOfFirstSpace > 0) + lines[i] = line.slice(0, indexOfFirstSpace) + } else if (!trimmedLine) { + lines[i] = ' ' + } + + i += 1 + } + + if (!imageFilePrefix) + imageFilePrefix = (Math.random().toString(36).substr(2, 9) + '_') + + imageFilePrefix = imageFilePrefix.replace(/[\/&]/g, '_ss_') + imageFilePrefix = encodeURIComponent(imageFilePrefix) + + let imgCount = 0 + + const asyncFunctions = [], + imagePaths = [] + + let currentCodeChunk:CodeChunkData = null + for (let key in codeChunksData) { // get the first code chunk. + if (!codeChunksData[key].prev) { + currentCodeChunk = codeChunksData[key] + break + } + } + + function clearCodeBlock(lines:string[], start:number, end:number) { + let i = start + while (i <= end) { + lines[i] = '' + i += 1 + } + } + + async function convertSVGToPNGFile(svg:string, lines:string[], start:number, end:number, modifyCodeBlock:boolean) { + const pngFilePath = path.resolve(imageDirectoryPath, imageFilePrefix+imgCount+'.png') + await svgElementToPNGFile(svg, pngFilePath) + let displayPNGFilePath + if (useRelativeFilePath) { + displayPNGFilePath = path.relative(fileDirectoryPath, pngFilePath) + '?' + Math.random() + } else { + displayPNGFilePath = '/' + path.relative(projectDirectoryPath, pngFilePath) + '?' + Math.random() + } + + imgCount++ + + if (modifyCodeBlock) { + clearCodeBlock(lines, start, end) + lines[end] += '\n' + `![](${displayPNGFilePath}) ` + } + + imagePaths.push(pngFilePath) + return displayPNGFilePath + } + + for (let i = 0; i < codes.length; i++) { + const codeData = codes[i] + const {start, end, content, options, optionsStr} = codeData + const def = lines[start].trim().slice(3).trim() + + if (def.match(/^(puml|plantuml)/)) { + try { + const checksum = md5(optionsStr + content) + let svg + if (!(svg = graphsCache[checksum])) { // check whether in cache + svg = await plantumlAPI.render(content, fileDirectoryPath) + } + await convertSVGToPNGFile(svg, lines, start, end, true) + } catch(error) { + clearCodeBlock(lines, start, end) + lines[end] += `\n` + `\`\`\`\n${error}\n\`\`\` \n` + } + } else if (def.match(/^(viz|dot)/)) { + try { + const checksum = md5(optionsStr + content) + let svg + if (!(svg = graphsCache[checksum])) { + const engine = options['engine'] || 'dot' + svg = Viz(content, {engine}) + } + await convertSVGToPNGFile(svg, lines, start, end, true) + } catch(error) { + clearCodeBlock(lines, start, end) + lines[end] += `\n` + `\`\`\`\n${error}\n\`\`\` \n` + } + } else if (def.match(/^mermaid/)) { + // do nothing as it doesn't work well... + /* + try { + const pngFilePath = path.resolve(imageDirectoryPath, imageFilePrefix+imgCount+'.png') + imgCount++ + await mermaidToPNG(content, pngFilePath) + + let displayPNGFilePath + if (useRelativeFilePath) { + displayPNGFilePath = path.relative(fileDirectoryPath, pngFilePath) + '?' + Math.random() + } else { + displayPNGFilePath = '/' + path.relative(projectDirectoryPath, pngFilePath) + '?' + Math.random() + } + clearCodeBlock(lines, start, end) + + lines[end] += '\n' + `![](${displayPNGFilePath}) ` + + imagePaths.push(pngFilePath) + } catch(error) { + clearCodeBlock(lines, start, end) + lines[end] += `\n` + `\`\`\`\n${error}\n\`\`\` \n` + } + */ + } else if (currentCodeChunk) { // code chunk + if (currentCodeChunk.options['hide']) { // remove code block + clearCodeBlock(lines, start, end) + } else { // remove {...} after ```lang + const line = lines[start] + const indexOfFirstSpace = line.indexOf(' ', line.indexOf('```')) + lines[start] = line.slice(0, indexOfFirstSpace) + } + + if (currentCodeChunk.result) { // append result + let result = currentCodeChunk.result + if (currentCodeChunk.options['output'] === 'html') { // check svg and convert it to png + const $ = cheerio.load(currentCodeChunk.result, {xmlMode: true}) + const svg = $('svg') + if (svg.length === 1) { + const pngFilePath = await convertSVGToPNGFile($.html('svg'), lines, start, end, false) + result = `![](${pngFilePath}) \n` + } + } else if (currentCodeChunk.options['output'] === 'markdown') { + result = currentCodeChunk.plainResult + } + + lines[end] += ('\n' + result) + } + currentCodeChunk = codeChunksData[currentCodeChunk.next] + } + } + + await Promise.all(asyncFunctions) + + const outputString = lines.filter((line)=> line).join('\n') + return {outputString, imagePaths} +} \ No newline at end of file diff --git a/src/puml.ts b/src/puml.ts index 38bdd1b..7ea88d4 100644 --- a/src/puml.ts +++ b/src/puml.ts @@ -1,8 +1,8 @@ import * as path from "path" -import {getExtensionDirectoryPath} from './utility' +import {extensionDirectoryPath} from './utility' import {spawn} from "child_process" -const PlantUMLJarPath = path.resolve(getExtensionDirectoryPath(), './dependencies/plantuml/plantuml.jar') +const PlantUMLJarPath = path.resolve(extensionDirectoryPath, './dependencies/plantuml/plantuml.jar') /** * key is fileDirectoryPath, value is PlantUMLTask diff --git a/src/transformer.ts b/src/transformer.ts index f3fbbe1..a437c72 100644 --- a/src/transformer.ts +++ b/src/transformer.ts @@ -4,6 +4,7 @@ import * as fs from "fs" import * as less from "less" import * as request from "request" import * as Baby from "babyparse" +import * as temp from "temp" // import * as request from 'request' // import * as less from "less" @@ -11,11 +12,12 @@ import * as Baby from "babyparse" // import * as temp from "temp" // temp.track() import * as utility from "./utility" -const extensionDirectoryPath = utility.getExtensionDirectoryPath() +const extensionDirectoryPath = utility.extensionDirectoryPath const jsonic = require(path.resolve(extensionDirectoryPath, './dependencies/jsonic/jsonic.js')) const md5 = require(path.resolve(extensionDirectoryPath, './dependencies/javascript-md5/md5.js')) import {CustomSubjects} from "./custom-subjects" +import * as PDF from "./pdf" interface TransformMarkdownOutput { outputString: string, @@ -27,15 +29,24 @@ interface TransformMarkdownOutput { * whehter we found [TOC] in markdown file or not. */ tocBracketEnabled: boolean + + /** + * imported javascript and css files + * convert .js file to + * convert .css file to + */ + JSAndCssFiles: string[] } interface TransformMarkdownOptions { fileDirectoryPath: string projectDirectoryPath: string filesCache: {[key:string]: string} - useRelativeImagePath: boolean + useRelativeFilePath: boolean forPreview: boolean - protocolsWhiteListRegExp: RegExp + protocolsWhiteListRegExp: RegExp, + notSourceFile?: boolean, + imageDirectoryPath?: string } const fileExtensionToLanguageMap = { @@ -80,6 +91,33 @@ function createAnchor(lineNo) { return `\n\n

\n\n` } +let DOWNLOADS_TEMP_FOLDER = null +/** + * download file and return its local path + */ +function downloadFileIfNecessary(filePath:string):Promise { + return new Promise((resolve, reject)=> { + if (!filePath.match(/^https?\:\/\//)) + return resolve(filePath) + + if (!DOWNLOADS_TEMP_FOLDER) DOWNLOADS_TEMP_FOLDER = temp.mkdirSync('mpe_downloads') + request.get({url: filePath, encoding: 'binary'}, (error, response, body)=> { + if (error) + return reject(error) + else { + const localFilePath = path.resolve(DOWNLOADS_TEMP_FOLDER, md5(filePath)) + path.extname(filePath) + fs.writeFile(localFilePath, body, 'binary', (error)=> { + if (error) + return reject(error) + else + return resolve(localFilePath) + }) + } + }) + }) +} + + /** * * Load file by `filePath` @@ -87,7 +125,7 @@ function createAnchor(lineNo) { * @param param1 * @param filesCache */ -async function loadFile(filePath:string, {fileDirectoryPath, forPreview}, filesCache={}):Promise { +async function loadFile(filePath:string, {fileDirectoryPath, forPreview, imageDirectoryPath}, filesCache={}):Promise { if (filesCache[filePath]) return filesCache[filePath] @@ -100,17 +138,12 @@ async function loadFile(filePath:string, {fileDirectoryPath, forPreview}, filesC }) }) } + else if (filePath.endsWith('.pdf')) { // pdf file + const localFilePath = await downloadFileIfNecessary(filePath) + const svgMarkdown = await PDF.toSVGMarkdown(localFilePath, {markdownDirectoryPath: fileDirectoryPath, svgDirectoryPath: imageDirectoryPath}) + return svgMarkdown + } /* - else if filePath.endsWith('.pdf') # pdf file - downloadFileIfNecessary(filePath) - .then (localFilePath)-> - PDF ?= require('./pdf') - PDF.toSVGMarkdown localFilePath, {svgDirectoryPath: imageDirectoryPath, markdownDirectoryPath: fileDirectoryPath}, (error, svgMarkdown)-> - if error - return reject error - else - return resolve(svgMarkdown) - else if filePath.endsWith('.js') # javascript file requiresJavaScriptFiles(filePath, forPreview).then (jsCode)-> return resolve(jsCode) @@ -149,17 +182,21 @@ export async function transformMarkdown(inputString:string, { fileDirectoryPath = '', projectDirectoryPath = '', filesCache = {}, - useRelativeImagePath = null, + useRelativeFilePath = null, forPreview = false, - protocolsWhiteListRegExp = null }:TransformMarkdownOptions):Promise { + protocolsWhiteListRegExp = null, + notSourceFile = false, + imageDirectoryPath = '' }:TransformMarkdownOptions):Promise { let inBlock = false // inside code block + let codeChunkOffset = 0 const tocConfigs = [], - slideConfigs = [] + slideConfigs = [], + JSAndCssFiles = [] let tocBracketEnabled = false async function helper(i, lineNo=0, outputString=""):Promise { if (i >= inputString.length) { // done - return {outputString, slideConfigs, tocBracketEnabled} + return {outputString, slideConfigs, tocBracketEnabled, JSAndCssFiles} } if (inputString[i] == '\n') @@ -169,8 +206,14 @@ export async function transformMarkdown(inputString:string, if (end < 0) end = inputString.length let line = inputString.substring(i, end) - if (line.match(/^\s*```/)) { + if (line.match(/^```/)) { if (!inBlock && forPreview) outputString += createAnchor(lineNo) + + let match; + if (!inBlock && !notSourceFile && (match = line.match(/\"?cmd\"?\s*:/))) { // it's code chunk, so mark its offset + line = line.replace('{', `{code_chunk_offset:${codeChunkOffset}, `) + codeChunkOffset++ + } inBlock = !inBlock return helper(end+1, lineNo+1, outputString+line+'\n') } @@ -186,7 +229,12 @@ export async function transformMarkdown(inputString:string, if (forPreview) outputString += createAnchor(lineNo) let subject = subjectMatch[1] - if (subject in CustomSubjects) { + if (subject === '@import') { + const commentEnd = line.lastIndexOf('-->') + if (commentEnd > 0) + line = line.slice(4, commentEnd).trim() + } + else if (subject in CustomSubjects) { let commentEnd = inputString.indexOf('-->', i + 4) if (commentEnd < 0) { // didn't find --> @@ -271,7 +319,7 @@ export async function transformMarkdown(inputString:string, if (!imageSrc) { if (filePath.match(protocolsWhiteListRegExp)) imageSrc = filePath - else if (useRelativeImagePath) + else if (useRelativeFilePath) imageSrc = path.relative(fileDirectoryPath, absoluteFilePath) + '?' + Math.random() else imageSrc = '/' + path.relative(projectDirectoryPath, absoluteFilePath) + '?' + Math.random() @@ -283,7 +331,7 @@ export async function transformMarkdown(inputString:string, } if (config) { - if (config.width || config.height || config.class || config.id) { + if (config['width'] || config['height'] || config['class'] || config['id']) { output = `= 0) { // markdown files // this return here is necessary - let {outputString:output} = await transformMarkdown(fileContent, {fileDirectoryPath: path.dirname(absoluteFilePath), projectDirectoryPath, filesCache, useRelativeImagePath: false, forPreview: false, protocolsWhiteListRegExp}) + let {outputString:output} = await transformMarkdown(fileContent, { + fileDirectoryPath: path.dirname(absoluteFilePath), + projectDirectoryPath, + filesCache, + useRelativeFilePath: false, + forPreview: false, + protocolsWhiteListRegExp, + notSourceFile: true, // <= this is not the sourcefile + imageDirectoryPath + }) output = '\n' + output + ' ' return helper(end+1, lineNo+1, outputString+output+'\n') } @@ -338,25 +418,47 @@ export async function transformMarkdown(inputString:string, output = _2DArrayToMarkdownTable(parseResult.data) } } - else if (extname === '.css' || extname === '.less') { // css or less file + else if (extname === '.css' || extname === '.js') { + if (!forPreview) { // not for preview, so convert to corresponding HTML tag directly. + let sourcePath + if (filePath.match(protocolsWhiteListRegExp)) + sourcePath = filePath + else if (useRelativeFilePath) + sourcePath = path.relative(fileDirectoryPath, absoluteFilePath) + else + sourcePath = 'file://' + absoluteFilePath + + if (extname === '.js') { + output = `` + } else { + output = `` + } + } else { + output = '' + } + JSAndCssFiles.push(filePath) + } + else if (/*extname === '.css' || */ extname === '.less') { // css or less file output = `` } - /* - else if extname == '.pdf' - if config?.page_no # only disply the nth page. 1-indexed - pages = fileContent.split('\n') - pageNo = parseInt(config.page_no) - 1 - pageNo = 0 if pageNo < 0 - output = pages[pageNo] or '' - else if config?.page_begin or config?.page_end - pages = fileContent.split('\n') - pageBegin = parseInt(config.page_begin) - 1 or 0 - pageEnd = config.page_end or pages.length - 1 - pageBegin = 0 if pageBegin < 0 - output = pages.slice(pageBegin, pageEnd).join('\n') or '' - else + else if (extname === '.pdf') { + if (config && config['page_no']) { // only disply the nth page. 1-indexed + const pages = fileContent.split('\n') + let pageNo = parseInt(config['page_no']) - 1 + if (pageNo < 0) pageNo = 0 + output = pages[pageNo] || '' + } + else if (config && (config['page_begin'] || config['page_end'])) { + const pages = fileContent.split('\n') + let pageBegin = parseInt(config['page_begin']) - 1 || 0 + const pageEnd = config['page_end'] || pages.length - 1 + if (pageBegin < 0) pageBegin = 0 + output = pages.slice(pageBegin, pageEnd).join('\n') || '' + } + else { output = fileContent - */ + } + } else if (extname === '.dot' || extname === '.gv' || extname === '.viz') { // graphviz output = `\`\`\`dot\n${fileContent}\n\`\`\` ` } diff --git a/src/typings/mpe.d.ts b/src/typings/mpe.d.ts index 4a9409e..7d4d340 100644 --- a/src/typings/mpe.d.ts +++ b/src/typings/mpe.d.ts @@ -1,3 +1,4 @@ +/* interface MPEConfig { breakOnSingleNewLine: boolean enableTypographer: boolean @@ -6,24 +7,69 @@ interface MPEConfig { frontMatterRenderingOption:string scrollSync: boolean mermaidTheme: string - /** - * "KaTeX", "MathJax", or "None" - */ mathRenderingOption: string mathInlineDelimiters: Array mathBlockDelimiters: Array + codeBlockTheme: string + previewTheme: string + protocolsWhiteList: string + imageFolderPath: string + imageUploader: string +} +*/ +interface MarkdownEngineConfig { + breakOnSingleNewLine: boolean + enableTypographer: boolean + enableWikiLinkSyntax: boolean + wikiLinkFileExtension: string + protocolsWhiteList: string /** - * Themes + * "KaTeX", "MathJax", or "None" */ + mathRenderingOption: string + mathInlineDelimiters: string[][] + mathBlockDelimiters: string[][] codeBlockTheme: string previewTheme: string + mermaidTheme: string + frontMatterRenderingOption: string + imageFolderPath: string +} - protocolsWhiteList: string +interface CodeChunkData { /** - * image helper + * id of the code chunk */ - imageFolderPath: string - imageUploader: string + id: string, + /** + * code content of the code chunk + */ + code: string, + /** + * code chunk options + */ + options: object, + /** + * result after running code chunk + */ + plainResult: string, + + /** + * result after formatting according to options['output'] format + */ + result: string, + /** + * whether is running the code chunk or not + */ + running: boolean, + /** + * previous code chunk + */ + prev: string, + /** + * next code chunk + */ + next: string, } diff --git a/src/utility.ts b/src/utility.ts index 9b4b69b..f7437f4 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -1,14 +1,14 @@ import * as path from "path" import * as fs from "fs" -import * as vscode from "vscode" +import * as os from "os" import {exec} from "child_process" +import * as child_process from "child_process" +import * as less from "less" +import * as mkdirp_ from "mkdirp" + import * as temp from "temp" temp.track() -export function getExtensionDirectoryPath() { - return path.resolve(__dirname, "../../") // -} - const TAGS_TO_REPLACE = { '&': '&', '<': '<', @@ -74,6 +74,25 @@ export function tempOpen(options):Promise { }) } +export function execFile(file:string, args:string[], options?:object):Promise { + return new Promise((resolve, reject)=> { + child_process.execFile(file, args, options, (error, stdout, stderr)=> { + if (error) return reject(error.toString()) + else if (stderr) return reject(stderr) + else return resolve(stdout) + }) + }) +} + +export function mkdirp(dir:string):Promise { + return new Promise((resolve, reject)=> { + mkdirp_(dir, (error, made)=> { + if (error) return reject(error) + return resolve(made) + }) + }) +} + /** * open html file in browser or open pdf file in reader ... etc * @param filePath @@ -88,4 +107,128 @@ export function openFile(filePath) { cmd = 'xdg-open' exec(`${cmd} ${filePath}`) +} + +/** + * get "~/.markdown-preview-enhanced" path + */ +export const extensionConfigDirectoryPath = path.resolve(os.homedir(), './.markdown-preview-enhanced') + +/** + * get the directory path of this extension. + */ +export const extensionDirectoryPath = path.resolve(__dirname, "../../") + + +/** + * compile ~/.markdown-preview-enhanced/style.less and return 'css' content. + */ +export async function getGlobalStyles():Promise { + const homeDir = os.homedir() + const globalLessFilePath = path.resolve(homeDir, './.markdown-preview-enhanced/style.less') + + let fileContent:string + try { + fileContent = await readFile(globalLessFilePath, {encoding: 'utf-8'}) + } catch(e) { + // create style.less file + fileContent = ` +.markdown-preview-enhanced.markdown-preview-enhanced { + // modify your style here + // eg: background-color: blue; +} ` + await writeFile(globalLessFilePath, fileContent, {encoding: 'utf-8'}) + } + + return await new Promise((resolve, reject)=> { + less.render(fileContent, {paths: [path.dirname(globalLessFilePath)]}, (error, output)=> { + if (error) return reject(error) + return resolve(output.css || '') + }) + }) +} + +/** + * load ~/.markdown-preview-enhanced/mermaid_config.js file. + */ +export async function getMermaidConfig():Promise { + const homeDir = os.homedir() + const mermaidConfigPath = path.resolve(homeDir, './.markdown-preview-enhanced/mermaid_config.js') + + let mermaidConfig:object + if (fs.existsSync(mermaidConfigPath)) { + try { + delete require.cache[mermaidConfigPath] // return uncached + mermaidConfig = require(mermaidConfigPath) + } catch(e) { + mermaidConfig = { startOnLoad: false } + } + } else { + const fileContent = `// config mermaid init call +// http://knsv.github.io/mermaid/#configuration +// +// You can edit the 'config' variable below. +let config = { + startOnLoad: false +} + +module.exports = config || {startOnLoad: false} +` + await writeFile(mermaidConfigPath, fileContent, {encoding: 'utf-8'}) + mermaidConfig = { startOnLoad: false } + } + + return mermaidConfig +} + +const defaultMathjaxConfig = { + extensions: ['tex2jax.js'], + jax: ['input/TeX','output/HTML-CSS'], + messageStyle: 'none', + tex2jax: { + processEnvironments: false, + processEscapes: true + }, + TeX: { + extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js'] + }, + 'HTML-CSS': { availableFonts: ['TeX'] } +} + +/** + * load ~/.markdown-preview-enhanced/mermaid_config.js file. + */ +export async function getMathJaxConfig():Promise { + const homeDir = os.homedir() + const mathjaxConfigPath = path.resolve(homeDir, './.markdown-preview-enhanced/mathjax_config.js') + + let mathjaxConfig:object + if (fs.existsSync(mathjaxConfigPath)) { + try { + delete require.cache[mathjaxConfigPath] // return uncached + mathjaxConfig = require(mathjaxConfigPath) + } catch(e) { + mathjaxConfig = defaultMathjaxConfig + } + } else { + const fileContent = ` +module.exports = { + extensions: ['tex2jax.js'], + jax: ['input/TeX','output/HTML-CSS'], + messageStyle: 'none', + tex2jax: { + processEnvironments: false, + processEscapes: true + }, + TeX: { + extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js'] + }, + 'HTML-CSS': { availableFonts: ['TeX'] } +} +` + await writeFile(mathjaxConfigPath, fileContent, {encoding: 'utf-8'}) + mathjaxConfig = defaultMathjaxConfig + } + + return mathjaxConfig } \ No newline at end of file diff --git a/styles/preview.css b/styles/preview.css index 5f1a7b1..aa47852 100644 --- a/styles/preview.css +++ b/styles/preview.css @@ -1 +1 @@ -.scrollbar-style::-webkit-scrollbar{width:8px}.scrollbar-style::-webkit-scrollbar-track{border-radius:10px;background-color:#fff}.scrollbar-style::-webkit-scrollbar-thumb{border-radius:5px;background-color:rgba(150,150,150,0.66);border:4px solid rgba(150,150,150,0.66);background-clip:content-box}.markdown-preview-enhanced-container{width:100%;margin:0;padding:0;box-sizing:border-box}.markdown-preview-enhanced-container .btn{display:inline-block;color:#99aeb8;text-shadow:none;border:1px solid #171e22;background-color:#32424a;background-image:linear-gradient(#364850, #32424a);height:initial;padding:0 .8em;font-size:1em;line-height:2em;margin-bottom:0;height:27px;font-weight:normal;text-align:center;vertical-align:middle;border:none;border-radius:3px;white-space:nowrap;cursor:pointer;z-index:0;-webkit-user-select:none}.markdown-preview-enhanced-container .refreshing-icon{display:none;position:absolute;bottom:32px;left:48px;width:48px;height:48px;background-image:url(octocat-spinner-128.gif);background-repeat:no-repeat;background-position:top center;background-size:cover;font-size:24px;z-index:999;color:black}.markdown-preview-enhanced-container .mpe-toolbar{position:absolute;top:32px;right:24px;opacity:0;display:block}.markdown-preview-enhanced-container .mpe-toolbar .back-to-top-btn,.markdown-preview-enhanced-container .mpe-toolbar .refresh-btn,.markdown-preview-enhanced-container .mpe-toolbar .sidebar-toc-btn{float:right;width:12px;margin-right:4px;opacity:.4}.markdown-preview-enhanced-container .mpe-toolbar .back-to-top-btn:hover,.markdown-preview-enhanced-container .mpe-toolbar .refresh-btn:hover,.markdown-preview-enhanced-container .mpe-toolbar .sidebar-toc-btn:hover{opacity:1}.markdown-preview-enhanced-container:hover .back-to-top-btn,.markdown-preview-enhanced-container:hover .refresh-btn,.markdown-preview-enhanced-container:hover .sidebar-toc-btn{display:block}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc{font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif;position:absolute;top:0;right:0;width:268px;height:100%;padding:32px 0 12px 0;overflow:auto;background-color:#fff;color:#333;font-size:14px;box-shadow:-4px 0 12px rgba(150,150,150,0.33);box-sizing:border-box}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc a{color:#333;text-decoration:none}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc ul{padding:0 1.6em;margin-top:.8em}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc li{margin-bottom:.8em}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc ul{list-style-type:none}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-toolbar{right:300px}.markdown-preview-enhanced-container.show-sidebar-toc .markdown-preview-enhanced{width:calc(100% - 268px)}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"]{width:100%;height:100%;margin:0;overflow:auto;font-size:16px;display:block;position:absolute}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk{position:relative}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .output-div{overflow-x:auto}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .output-div svg{display:block}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk pre{cursor:text}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group{position:absolute;right:0;top:0;display:none}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group .run-btn,.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group .run-all-btn{float:right;margin-left:4px;border-radius:3px;font-size:.8em;color:#eee;background-color:#528bff;background-image:none;border:none}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group .run-btn:hover,.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group .run-all-btn:hover{background-color:#4b7fe8;cursor:pointer}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk:hover .btn-group{display:block}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .status{position:absolute;right:0;top:0;font-size:.85em;color:inherit;padding:2px 6px;background-color:rgba(0,0,0,0.04);display:none}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk.running .btn-group{display:none}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk.running .status{display:block}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode]{background-color:#f4f4f4}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides{width:100%}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide{position:relative;padding:2em !important;margin-bottom:12px;text-align:left !important;display:flex;align-items:center;border:1px solid #e6e6e6;box-shadow:0 0 16px 4px #eeeeee;font-size:24px}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h1,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h2,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h3,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h4,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h5,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h6{margin-top:0}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide .background-video{position:absolute;top:0;left:0;width:100%;height:100%}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide .background-iframe,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide .background-iframe-overlay{position:absolute;width:100%;height:100%;left:0;top:0;border:none;z-index:1}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide .background-iframe-overlay{z-index:2}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] section{display:block;width:100%;transform-style:preserve-3d;font-size:100%;font:inherit}#image-helper-view{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);color:#333;padding:16px}#image-helper-view .splitter{width:100%;height:1px;margin-bottom:16px;background-color:#f5f5f5}#image-helper-view .url-editor{display:block;margin:12px 0;padding:8px;width:80%}#image-helper-view .drop-area{margin-top:12px;text-align:center;height:64px;border-style:dashed;border-color:#999;cursor:pointer;margin-bottom:32px;background-color:#fafafa}#image-helper-view .drop-area:hover{background-color:#f5f5f5}#image-helper-view .drop-area p{margin:0;position:relative;top:50%;transform:translateY(-50%)}#image-helper-view .drop-area.uploader{margin-bottom:12px}#image-helper-view .uploader-choice{margin-bottom:24px}#image-helper-view .uploader-choice select{background-color:#f5f5f5;border-color:#fff;margin:0 6px}#image-helper-view .close-btn,#image-helper-view .add-btn{margin-right:16px;width:64px;text-align:center;padding:0} \ No newline at end of file +.scrollbar-style::-webkit-scrollbar{width:8px}.scrollbar-style::-webkit-scrollbar-track{border-radius:10px;background-color:#fff}.scrollbar-style::-webkit-scrollbar-thumb{border-radius:5px;background-color:rgba(150,150,150,0.66);border:4px solid rgba(150,150,150,0.66);background-clip:content-box}.markdown-preview-enhanced-container{width:100%;margin:0;padding:0;box-sizing:border-box}.markdown-preview-enhanced-container .btn{display:inline-block;color:#99aeb8;text-shadow:none;border:1px solid #171e22;background-color:#32424a;background-image:linear-gradient(#364850, #32424a);height:initial;padding:0 .8em;font-size:1em;line-height:2em;margin-bottom:0;height:27px;font-weight:normal;text-align:center;vertical-align:middle;border:none;border-radius:3px;white-space:nowrap;cursor:pointer;z-index:0;-webkit-user-select:none}.markdown-preview-enhanced-container .refreshing-icon{display:none;position:absolute;bottom:32px;left:48px;width:48px;height:48px;background-image:url(octocat-spinner-128.gif);background-repeat:no-repeat;background-position:top center;background-size:cover;font-size:24px;z-index:999;color:black}.markdown-preview-enhanced-container .mpe-toolbar{position:absolute;top:32px;right:24px;opacity:0;display:block}.markdown-preview-enhanced-container .mpe-toolbar .back-to-top-btn,.markdown-preview-enhanced-container .mpe-toolbar .refresh-btn,.markdown-preview-enhanced-container .mpe-toolbar .sidebar-toc-btn{float:right;width:12px;margin-right:4px;opacity:.4}.markdown-preview-enhanced-container .mpe-toolbar .back-to-top-btn:hover,.markdown-preview-enhanced-container .mpe-toolbar .refresh-btn:hover,.markdown-preview-enhanced-container .mpe-toolbar .sidebar-toc-btn:hover{opacity:1}.markdown-preview-enhanced-container:hover .back-to-top-btn,.markdown-preview-enhanced-container:hover .refresh-btn,.markdown-preview-enhanced-container:hover .sidebar-toc-btn{display:block}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc{font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif;position:absolute;top:0;right:0;width:268px;height:100%;padding:32px 0 12px 0;overflow:auto;background-color:#fff;color:#333;font-size:14px;box-shadow:-4px 0 12px rgba(150,150,150,0.33);box-sizing:border-box}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc a{color:#333;text-decoration:none}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc ul{padding:0 1.6em;margin-top:.8em}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc li{margin-bottom:.8em}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-sidebar-toc ul{list-style-type:none}.markdown-preview-enhanced-container.show-sidebar-toc .mpe-toolbar{right:300px}.markdown-preview-enhanced-container.show-sidebar-toc .markdown-preview-enhanced{width:calc(100% - 268px)}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"]{width:100%;height:100%;margin:0;overflow:auto;font-size:16px;display:block;position:absolute}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk{position:relative}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk pre{cursor:text}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group{position:absolute;right:0;top:0;display:none}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group .run-btn,.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group .run-all-btn{float:right;margin-left:4px;border-radius:3px;font-size:.8em;color:#eee;background-color:#528bff;background-image:none;border:none}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group .run-btn:hover,.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .btn-group .run-all-btn:hover{background-color:#4b7fe8;cursor:pointer}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk:hover .btn-group{display:block}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk .status{position:absolute;right:0;top:0;font-size:.85em;color:inherit;padding:2px 6px;background-color:rgba(0,0,0,0.04);display:none}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk.running .btn-group{display:none}.markdown-preview-enhanced-container .markdown-preview-enhanced[for="preview"] .code-chunk.running .status{display:block}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode]{background-color:#f4f4f4}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides{width:100%}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide{position:relative;padding:2em !important;margin-bottom:12px;text-align:left !important;display:flex;align-items:center;border:1px solid #e6e6e6;box-shadow:0 0 16px 4px #eeeeee;font-size:24px}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h1,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h2,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h3,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h4,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h5,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide h6{margin-top:0}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide .background-video{position:absolute;top:0;left:0;width:100%;height:100%}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide .background-iframe,.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide .background-iframe-overlay{position:absolute;width:100%;height:100%;left:0;top:0;border:none;z-index:1}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] #preview-slides .slide .background-iframe-overlay{z-index:2}.markdown-preview-enhanced-container .markdown-preview-enhanced[data-presentation-preview-mode] section{display:block;width:100%;transform-style:preserve-3d;font-size:100%;font:inherit}#image-helper-view{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);color:#333;padding:16px}#image-helper-view .splitter{width:100%;height:1px;margin-bottom:16px;background-color:#f5f5f5}#image-helper-view .url-editor{display:block;margin:12px 0;padding:8px;width:80%}#image-helper-view .drop-area{margin-top:12px;text-align:center;height:64px;border-style:dashed;border-color:#999;cursor:pointer;margin-bottom:32px;background-color:#fafafa}#image-helper-view .drop-area:hover{background-color:#f5f5f5}#image-helper-view .drop-area p{margin:0;position:relative;top:50%;transform:translateY(-50%)}#image-helper-view .drop-area.uploader{margin-bottom:12px}#image-helper-view .uploader-choice{margin-bottom:24px}#image-helper-view .uploader-choice select{background-color:#f5f5f5;border-color:#fff;margin:0 6px}#image-helper-view .close-btn,#image-helper-view .add-btn{margin-right:16px;width:64px;text-align:center;padding:0} \ No newline at end of file diff --git a/styles/src/preview.less b/styles/src/preview.less index b30e63d..3f4f5f5 100644 --- a/styles/src/preview.less +++ b/styles/src/preview.less @@ -143,6 +143,7 @@ .code-chunk { position: relative; + /* .output-div { overflow-x: auto; @@ -150,6 +151,7 @@ display: block; } } + */ pre { cursor: text; diff --git a/styles/src/style-template.less b/styles/src/style-template.less index 2728016..ea9e65d 100644 --- a/styles/src/style-template.less +++ b/styles/src/style-template.less @@ -32,8 +32,8 @@ color: @fg-strong; } - h1 { font-size: 2.25em; font-weight: 300; padding-bottom: 0.3em; border-bottom: 1px solid @border;} - h2 { font-size: 1.75em; font-weight: 400; padding-bottom: 0.3em; border-bottom: 1px solid @border;} + h1 { font-size: 2.25em; font-weight: 300; padding-bottom: 0.3em; } + h2 { font-size: 1.75em; font-weight: 400; padding-bottom: 0.3em; } h3 { font-size: 1.5em; font-weight: 500; } h4 { font-size: 1.25em; font-weight: 600; } h5 { font-size: 1.1em; font-weight: 600; } @@ -43,9 +43,11 @@ h5 { font-size: 1em; } h6 { color: @fg-subtle; } + /* h1, h2 { border-bottom: 1px solid @border; } + */ // Emphasis -------------------- @@ -295,6 +297,47 @@ // word-wrap: break-word; } + // code block line numbers + pre.line-numbers { + position: relative; + padding-left: 3.8em; + counter-reset: linenumber; + + & > code { + position: relative; + } + + .line-numbers-rows { + position: absolute; + pointer-events: none; + top: 1em; + font-size: 100%; + left: 0; //-3.8em; + width: 3em; /* works for line-numbers below 1000 lines */ + letter-spacing: -1px; + border-right: 1px solid #999; + + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + & > span { + pointer-events: none; + display: block; + counter-increment: linenumber; + } + + & > span:before { + content: counter(linenumber); + color: #999; + display: block; + padding-right: 0.8em; + text-align: right; + } + } + } + // KBD -------------------- kbd { color: @fg-strong; diff --git a/styles/style-template.css b/styles/style-template.css index 2b17c06..ad2dbb7 100644 --- a/styles/style-template.css +++ b/styles/style-template.css @@ -1 +1 @@ -.markdown-preview-enhanced{font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif;font-size:16px;line-height:1.6;color:#333;background-color:#fff;overflow:initial;margin:10px 13px 10px 13px;padding:2em;box-sizing:border-box;word-wrap:break-word}.markdown-preview-enhanced>:first-child{margin-top:0}.markdown-preview-enhanced h1,.markdown-preview-enhanced h2,.markdown-preview-enhanced h3,.markdown-preview-enhanced h4,.markdown-preview-enhanced h5,.markdown-preview-enhanced h6{line-height:1.2;margin-top:1em;margin-bottom:16px;color:#000}.markdown-preview-enhanced h1{font-size:2.25em;font-weight:300;padding-bottom:.3em;border-bottom:1px solid #d6d6d6}.markdown-preview-enhanced h2{font-size:1.75em;font-weight:400;padding-bottom:.3em;border-bottom:1px solid #d6d6d6}.markdown-preview-enhanced h3{font-size:1.5em;font-weight:500}.markdown-preview-enhanced h4{font-size:1.25em;font-weight:600}.markdown-preview-enhanced h5{font-size:1.1em;font-weight:600}.markdown-preview-enhanced h6{font-size:1em;font-weight:600}.markdown-preview-enhanced h1,.markdown-preview-enhanced h2,.markdown-preview-enhanced h3,.markdown-preview-enhanced h4,.markdown-preview-enhanced h5{font-weight:600}.markdown-preview-enhanced h5{font-size:1em}.markdown-preview-enhanced h6{color:#5c5c5c}.markdown-preview-enhanced h1,.markdown-preview-enhanced h2{border-bottom:1px solid #d6d6d6}.markdown-preview-enhanced strong{color:#000}.markdown-preview-enhanced del{color:#5c5c5c}.markdown-preview-enhanced a:not([href]){color:inherit;text-decoration:none}.markdown-preview-enhanced a{color:#08c;text-decoration:none}.markdown-preview-enhanced a:hover{color:#0050a3;text-decoration:none}.markdown-preview-enhanced img,.markdown-preview-enhanced svg{max-width:100%}.markdown-preview-enhanced>p{margin-top:0;margin-bottom:16px;word-wrap:break-word}.markdown-preview-enhanced>ul,.markdown-preview-enhanced>ol{margin-bottom:16px}.markdown-preview-enhanced ul,.markdown-preview-enhanced ol{padding-left:2em}.markdown-preview-enhanced ul.no-list,.markdown-preview-enhanced ol.no-list{padding:0;list-style-type:none}.markdown-preview-enhanced ul ul,.markdown-preview-enhanced ul ol,.markdown-preview-enhanced ol ol,.markdown-preview-enhanced ol ul{margin-top:0;margin-bottom:0}.markdown-preview-enhanced li{margin-bottom:0}.markdown-preview-enhanced li.task-list-item{list-style:none}.markdown-preview-enhanced li>p{margin-top:0;margin-bottom:0}.markdown-preview-enhanced .task-list-item-checkbox{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-preview-enhanced .task-list-item-checkbox:hover{cursor:pointer}.markdown-preview-enhanced blockquote{margin:16px 0;font-size:inherit;padding:0 15px;color:#5c5c5c;border-left:4px solid #d6d6d6}.markdown-preview-enhanced blockquote>:first-child{margin-top:0}.markdown-preview-enhanced blockquote>:last-child{margin-bottom:0}.markdown-preview-enhanced hr{height:4px;margin:32px 0;background-color:#d6d6d6;border:0 none}.markdown-preview-enhanced table{margin:10px 0 15px 0;border-collapse:collapse;border-spacing:0;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}.markdown-preview-enhanced table th{font-weight:bold;color:#000}.markdown-preview-enhanced table td,.markdown-preview-enhanced table th{border:1px solid #d6d6d6;padding:6px 13px}.markdown-preview-enhanced dl{padding:0}.markdown-preview-enhanced dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:bold}.markdown-preview-enhanced dl dd{padding:0 16px;margin-bottom:16px}.markdown-preview-enhanced code{font-family:Menlo,Monaco,Consolas,'Courier New',monospace;font-size:.85em !important;color:#000;background-color:#f0f0f0;border-radius:3px;padding:.2em 0}.markdown-preview-enhanced code::before,.markdown-preview-enhanced code::after{letter-spacing:-0.2em;content:"\00a0"}.markdown-preview-enhanced pre>code{padding:0;margin:0;font-size:.85em !important;word-break:normal;white-space:pre;background:transparent;border:0}.markdown-preview-enhanced .highlight{margin-bottom:16px}.markdown-preview-enhanced .highlight pre,.markdown-preview-enhanced pre{font-family:Menlo,Monaco,Consolas,'Courier New',monospace;padding:16px;overflow:auto;font-size:.85em !important;line-height:1.45;border:#d6d6d6;border-radius:3px}.markdown-preview-enhanced .highlight pre{margin-bottom:0;word-break:normal}.markdown-preview-enhanced pre{word-wrap:break-word;white-space:normal;word-break:break-all}.markdown-preview-enhanced pre .section{opacity:1}.markdown-preview-enhanced pre code,.markdown-preview-enhanced pre tt{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-preview-enhanced pre code:before,.markdown-preview-enhanced pre tt:before,.markdown-preview-enhanced pre code:after,.markdown-preview-enhanced pre tt:after{content:normal}.markdown-preview-enhanced p,.markdown-preview-enhanced blockquote,.markdown-preview-enhanced ul,.markdown-preview-enhanced ol,.markdown-preview-enhanced dl,.markdown-preview-enhanced pre{margin-top:0;margin-bottom:16px}.markdown-preview-enhanced kbd{color:#000;border:1px solid #d6d6d6;border-bottom:2px solid #c7c7c7;padding:2px 4px;background-color:#f0f0f0;border-radius:3px}.markdown-preview-enhanced .pagebreak,.markdown-preview-enhanced .newpage{page-break-before:always}@media screen and (min-width:914px){.markdown-preview-enhanced{width:980px;margin:10px auto;background:#fff}}@media screen and (max-width:400px){.markdown-preview-enhanced{font-size:14px;margin:0 auto;padding:15px}}@media print{.markdown-preview-enhanced{background-color:#fff}.markdown-preview-enhanced h1,.markdown-preview-enhanced h2,.markdown-preview-enhanced h3,.markdown-preview-enhanced h4,.markdown-preview-enhanced h5,.markdown-preview-enhanced h6{color:#000;page-break-after:avoid}.markdown-preview-enhanced blockquote{color:#5c5c5c}.markdown-preview-enhanced pre{page-break-inside:avoid}.markdown-preview-enhanced table{display:table}.markdown-preview-enhanced img{display:block;max-width:100%;max-height:100%}.markdown-preview-enhanced pre,.markdown-preview-enhanced code{word-wrap:break-word;white-space:normal}}.markdown-preview-enhanced .mathjax-exps .MathJax_Display{text-align:center !important}.markdown-preview-enhanced:not([for="preview"]) .code-chunk .btn-group{display:none}.markdown-preview-enhanced:not([for="preview"]) .code-chunk .status{display:none}.markdown-preview-enhanced[data-presentation-mode]{font-size:24px;width:100%;box-sizing:border-box;margin:0;padding:0}.markdown-preview-enhanced[data-presentation-mode] h1,.markdown-preview-enhanced[data-presentation-mode] h2,.markdown-preview-enhanced[data-presentation-mode] h3,.markdown-preview-enhanced[data-presentation-mode] h4,.markdown-preview-enhanced[data-presentation-mode] h5,.markdown-preview-enhanced[data-presentation-mode] h6{margin-top:0}.markdown-preview-enhanced[data-presentation-mode] strong{font-weight:bold}.markdown-preview-enhanced[data-presentation-mode]::-webkit-scrollbar{display:none}.markdown-preview-enhanced .slides{text-align:left !important} \ No newline at end of file +.markdown-preview-enhanced{font-family:"Helvetica Neue",Helvetica,"Segoe UI",Arial,freesans,sans-serif;font-size:16px;line-height:1.6;color:#333;background-color:#fff;overflow:initial;margin:10px 13px 10px 13px;padding:2em;box-sizing:border-box;word-wrap:break-word}.markdown-preview-enhanced>:first-child{margin-top:0}.markdown-preview-enhanced h1,.markdown-preview-enhanced h2,.markdown-preview-enhanced h3,.markdown-preview-enhanced h4,.markdown-preview-enhanced h5,.markdown-preview-enhanced h6{line-height:1.2;margin-top:1em;margin-bottom:16px;color:#000}.markdown-preview-enhanced h1{font-size:2.25em;font-weight:300;padding-bottom:.3em}.markdown-preview-enhanced h2{font-size:1.75em;font-weight:400;padding-bottom:.3em}.markdown-preview-enhanced h3{font-size:1.5em;font-weight:500}.markdown-preview-enhanced h4{font-size:1.25em;font-weight:600}.markdown-preview-enhanced h5{font-size:1.1em;font-weight:600}.markdown-preview-enhanced h6{font-size:1em;font-weight:600}.markdown-preview-enhanced h1,.markdown-preview-enhanced h2,.markdown-preview-enhanced h3,.markdown-preview-enhanced h4,.markdown-preview-enhanced h5{font-weight:600}.markdown-preview-enhanced h5{font-size:1em}.markdown-preview-enhanced h6{color:#5c5c5c}.markdown-preview-enhanced strong{color:#000}.markdown-preview-enhanced del{color:#5c5c5c}.markdown-preview-enhanced a:not([href]){color:inherit;text-decoration:none}.markdown-preview-enhanced a{color:#08c;text-decoration:none}.markdown-preview-enhanced a:hover{color:#0050a3;text-decoration:none}.markdown-preview-enhanced img,.markdown-preview-enhanced svg{max-width:100%}.markdown-preview-enhanced>p{margin-top:0;margin-bottom:16px;word-wrap:break-word}.markdown-preview-enhanced>ul,.markdown-preview-enhanced>ol{margin-bottom:16px}.markdown-preview-enhanced ul,.markdown-preview-enhanced ol{padding-left:2em}.markdown-preview-enhanced ul.no-list,.markdown-preview-enhanced ol.no-list{padding:0;list-style-type:none}.markdown-preview-enhanced ul ul,.markdown-preview-enhanced ul ol,.markdown-preview-enhanced ol ol,.markdown-preview-enhanced ol ul{margin-top:0;margin-bottom:0}.markdown-preview-enhanced li{margin-bottom:0}.markdown-preview-enhanced li.task-list-item{list-style:none}.markdown-preview-enhanced li>p{margin-top:0;margin-bottom:0}.markdown-preview-enhanced .task-list-item-checkbox{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-preview-enhanced .task-list-item-checkbox:hover{cursor:pointer}.markdown-preview-enhanced blockquote{margin:16px 0;font-size:inherit;padding:0 15px;color:#5c5c5c;border-left:4px solid #d6d6d6}.markdown-preview-enhanced blockquote>:first-child{margin-top:0}.markdown-preview-enhanced blockquote>:last-child{margin-bottom:0}.markdown-preview-enhanced hr{height:4px;margin:32px 0;background-color:#d6d6d6;border:0 none}.markdown-preview-enhanced table{margin:10px 0 15px 0;border-collapse:collapse;border-spacing:0;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}.markdown-preview-enhanced table th{font-weight:bold;color:#000}.markdown-preview-enhanced table td,.markdown-preview-enhanced table th{border:1px solid #d6d6d6;padding:6px 13px}.markdown-preview-enhanced dl{padding:0}.markdown-preview-enhanced dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:bold}.markdown-preview-enhanced dl dd{padding:0 16px;margin-bottom:16px}.markdown-preview-enhanced code{font-family:Menlo,Monaco,Consolas,'Courier New',monospace;font-size:.85em !important;color:#000;background-color:#f0f0f0;border-radius:3px;padding:.2em 0}.markdown-preview-enhanced code::before,.markdown-preview-enhanced code::after{letter-spacing:-0.2em;content:"\00a0"}.markdown-preview-enhanced pre>code{padding:0;margin:0;font-size:.85em !important;word-break:normal;white-space:pre;background:transparent;border:0}.markdown-preview-enhanced .highlight{margin-bottom:16px}.markdown-preview-enhanced .highlight pre,.markdown-preview-enhanced pre{font-family:Menlo,Monaco,Consolas,'Courier New',monospace;padding:16px;overflow:auto;font-size:.85em !important;line-height:1.45;border:#d6d6d6;border-radius:3px}.markdown-preview-enhanced .highlight pre{margin-bottom:0;word-break:normal}.markdown-preview-enhanced pre{word-wrap:break-word;white-space:normal;word-break:break-all}.markdown-preview-enhanced pre .section{opacity:1}.markdown-preview-enhanced pre code,.markdown-preview-enhanced pre tt{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-preview-enhanced pre code:before,.markdown-preview-enhanced pre tt:before,.markdown-preview-enhanced pre code:after,.markdown-preview-enhanced pre tt:after{content:normal}.markdown-preview-enhanced p,.markdown-preview-enhanced blockquote,.markdown-preview-enhanced ul,.markdown-preview-enhanced ol,.markdown-preview-enhanced dl,.markdown-preview-enhanced pre{margin-top:0;margin-bottom:16px}.markdown-preview-enhanced pre.line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}.markdown-preview-enhanced pre.line-numbers>code{position:relative}.markdown-preview-enhanced pre.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:1em;font-size:100%;left:0;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.markdown-preview-enhanced pre.line-numbers .line-numbers-rows>span{pointer-events:none;display:block;counter-increment:linenumber}.markdown-preview-enhanced pre.line-numbers .line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}.markdown-preview-enhanced kbd{color:#000;border:1px solid #d6d6d6;border-bottom:2px solid #c7c7c7;padding:2px 4px;background-color:#f0f0f0;border-radius:3px}.markdown-preview-enhanced .pagebreak,.markdown-preview-enhanced .newpage{page-break-before:always}@media screen and (min-width:914px){.markdown-preview-enhanced{width:980px;margin:10px auto;background:#fff}}@media screen and (max-width:400px){.markdown-preview-enhanced{font-size:14px;margin:0 auto;padding:15px}}@media print{.markdown-preview-enhanced{background-color:#fff}.markdown-preview-enhanced h1,.markdown-preview-enhanced h2,.markdown-preview-enhanced h3,.markdown-preview-enhanced h4,.markdown-preview-enhanced h5,.markdown-preview-enhanced h6{color:#000;page-break-after:avoid}.markdown-preview-enhanced blockquote{color:#5c5c5c}.markdown-preview-enhanced pre{page-break-inside:avoid}.markdown-preview-enhanced table{display:table}.markdown-preview-enhanced img{display:block;max-width:100%;max-height:100%}.markdown-preview-enhanced pre,.markdown-preview-enhanced code{word-wrap:break-word;white-space:normal}}.markdown-preview-enhanced .mathjax-exps .MathJax_Display{text-align:center !important}.markdown-preview-enhanced:not([for="preview"]) .code-chunk .btn-group{display:none}.markdown-preview-enhanced:not([for="preview"]) .code-chunk .status{display:none}.markdown-preview-enhanced[data-presentation-mode]{font-size:24px;width:100%;box-sizing:border-box;margin:0;padding:0}.markdown-preview-enhanced[data-presentation-mode] h1,.markdown-preview-enhanced[data-presentation-mode] h2,.markdown-preview-enhanced[data-presentation-mode] h3,.markdown-preview-enhanced[data-presentation-mode] h4,.markdown-preview-enhanced[data-presentation-mode] h5,.markdown-preview-enhanced[data-presentation-mode] h6{margin-top:0}.markdown-preview-enhanced[data-presentation-mode] strong{font-weight:bold}.markdown-preview-enhanced[data-presentation-mode]::-webkit-scrollbar{display:none}.markdown-preview-enhanced .slides{text-align:left !important} \ No newline at end of file