Browse Source

Initial commit. Render CommonMark. WIP Definitions

dev
Orzu Ionut 3 years ago
commit
934b206917
  1. 1
      .coffeelintignore
  2. 4
      .gitignore
  3. 15
      .travis.yml
  4. 1
      CONTRIBUTING.md
  5. 40
      ISSUE_TEMPLATE.md
  6. 19
      LICENSE.md
  7. 28
      PULL_REQUEST_TEMPLATE.md
  8. 8
      README.md
  9. 27
      appveyor.yml
  10. BIN
      assets/hr.png
  11. 448
      assets/primer-markdown.less
  12. 37
      coffeelint.json
  13. 18
      keymaps/markdown-preview.cson
  14. 203
      lib/definitions.js
  15. 50
      lib/extension-helper.js
  16. 279
      lib/main.js
  17. 513
      lib/markdown-preview-view.js
  18. 245
      lib/renderer.js
  19. 64
      lib/suggested-definition.js
  20. 37
      menus/markdown-preview.cson
  21. 4480
      package-lock.json
  22. 81
      package.json
  23. 1
      spec/fixtures/subdir/áccéntéd.md
  24. 9
      spec/fixtures/subdir/code-block.md
  25. 4
      spec/fixtures/subdir/doctype-tag.md
  26. 5
      spec/fixtures/subdir/evil.md
  27. 1
      spec/fixtures/subdir/file with space.md
  28. 51
      spec/fixtures/subdir/file.markdown
  29. 1
      spec/fixtures/subdir/html-tag.md
  30. 1
      spec/fixtures/subdir/pre-tag.md
  31. 5
      spec/fixtures/subdir/simple.md
  32. 1
      spec/fixtures/subdir/áccéntéd.md
  33. 839
      spec/markdown-preview-spec.js
  34. 604
      spec/markdown-preview-view-spec.js
  35. 156
      styles/markdown-preview-default.less
  36. 40
      styles/markdown-preview-github.less
  37. 129
      styles/markdown-preview.less

1
.coffeelintignore

@ -0,0 +1 @@
spec/fixtures

4
.gitignore

@ -0,0 +1,4 @@
node_modules
npm-debug.log
.github
.idea

15
.travis.yml

@ -0,0 +1,15 @@
language: objective-c
notifications:
email:
on_success: never
on_failure: change
script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh'
git:
depth: 10
branches:
only:
- master

1
CONTRIBUTING.md

@ -0,0 +1 @@
See the [Atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md)

40
ISSUE_TEMPLATE.md

@ -0,0 +1,40 @@
<!--
Have you read Atom's Code of Conduct? By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/atom/atom/blob/master/CODE_OF_CONDUCT.md
Do you want to ask a question? Are you looking for support? The Atom message board is the best place for getting support: https://discuss.atom.io
-->
### Prerequisites
* [ ] Put an X between the brackets on this line if you have done all of the following:
* Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode
* Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/
* Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq
* Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom
* Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages
### Description
[Description of the issue]
### Steps to Reproduce
1. [First Step]
2. [Second Step]
3. [and so on...]
**Expected behavior:** [What you expect to happen]
**Actual behavior:** [What actually happens]
**Reproduces how often:** [What percentage of the time does it reproduce?]
### Versions
You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running.
### Additional Information
Any additional information, configuration or data that might be necessary to reproduce the issue.

19
LICENSE.md

@ -0,0 +1,19 @@
MIT License Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

28
PULL_REQUEST_TEMPLATE.md

@ -0,0 +1,28 @@
### Requirements
* Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.
* All new code requires tests to ensure against regressions
### Description of the Change
<!--
We must be able to understand the design of your change from this description. If we can't get a good idea of what the code will be doing from the description here, the pull request may be closed at the maintainers' discretion. Keep in mind that the maintainer reviewing this PR may not be familiar with or have worked with the code here recently, so please walk us through the concepts.
-->
### Alternate Designs
<!-- Explain what other alternates were considered and why the proposed version was selected -->
### Benefits
<!-- What benefits will be realized by the code change? -->
### Possible Drawbacks
<!-- What are the possible side-effects or negative impacts of the code change? -->
### Applicable Issues
<!-- Enter any applicable Issues here -->

8
README.md

@ -0,0 +1,8 @@
# LegAtom package
Developing a legal IDE based on Atom
## This package is a fork of the [markdown-preview](https://github.com/atom/markdown-preview) package.
Show the rendered HTML CommonMark to the right of the current editor using <kbd>ctrl-shift-m</kbd>.
It is currently enabled for `.markdown`, `.md`, `.mdown`, `.mkd`, `.mkdown`, `.ron`, and `.txt` files.

27
appveyor.yml

@ -0,0 +1,27 @@
platform:
- x64
- x86
branches:
only:
- master
clone_depth: 10
skip_tags: true
environment:
APM_TEST_PACKAGES:
matrix:
- ATOM_CHANNEL: stable
- ATOM_CHANNEL: beta
install:
- ps: Install-Product node 6
build_script:
- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1'))
test: off
deploy: off

BIN
assets/hr.png

After

Width: 6  |  Height: 4  |  Size: 939 B

448
assets/primer-markdown.less

@ -0,0 +1,448 @@
// All of our block level items should have the same margin
@margin: 16px;
// This is styling for generic markdownized text. Anything you put in a
// container with .markdown-body on it should render generally well. It also
// includes some GitHub Flavored Markdown specific styling (like @mentions)
.markdown-body {
overflow: hidden;
font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif;
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
> *:first-child {
margin-top: 0 !important;
}
> *:last-child {
margin-bottom: 0 !important;
}
// Anchors like <a name="examples">. These sometimes end up wrapped around
// text when users mistakenly forget to close the tag or use self-closing tag
// syntax. We don't want them to appear like links.
// FIXME: a:not(:link):not(:visited) would be a little clearer here (and
// possibly faster to match), but it breaks styling of <a href> elements due
// to https://bugs.webkit.org/show_bug.cgi?id=142737.
a:not([href]) {
color: inherit;
text-decoration: none;
}
// Link Colors
.absent {
color: #c00;
}
.anchor {
position: absolute;
top: 0;
left: 0;
display: block;
padding-right: 6px;
padding-left: 30px;
margin-left: -30px;
&:focus {
outline: none;
}
}
// Headings
h1, h2, h3, h4, h5, h6 {
position: relative;
margin-top: 1em;
margin-bottom: @margin;
font-weight: bold;
line-height: 1.4;
.octicon-link {
display: none;
color: #000;
vertical-align: middle;
}
&:hover .anchor {
padding-left: 8px;
margin-left: -30px;
text-decoration: none;
.octicon-link {
display: inline-block;
}
}
tt,
code {
font-size: inherit;
}
}
h1 {
padding-bottom: 0.3em;
font-size: 2.25em;
line-height: 1.2;
border-bottom: 1px solid #eee;
.anchor {
line-height: 1;
}
}
h2 {
padding-bottom: 0.3em;
font-size: 1.75em;
line-height: 1.225;
border-bottom: 1px solid #eee;
.anchor {
line-height: 1;
}
}
h3 {
font-size: 1.5em;
line-height: 1.43;
.anchor {
line-height: 1.2;
}
}
h4 {
font-size: 1.25em;
.anchor {
line-height: 1.2;
}
}
h5 {
font-size: 1em;
.anchor {
line-height: 1.1;
}
}
h6 {
font-size: 1em;
color: #777;
.anchor {
line-height: 1.1;
}
}
p,
blockquote,
ul, ol, dl,
table,
pre {
margin-top: 0;
margin-bottom: @margin;
}
hr {
height: 4px;
padding: 0;
margin: @margin 0;
background-color: #e7e7e7;
border: 0 none;
}
// Lists, Blockquotes & Such
ul,
ol {
padding-left: 2em;
&.no-list {
padding: 0;
list-style-type: none;
}
}
// Did someone complain about list spacing? Encourage them
// to create the spacing with their markdown formatting.
// List behavior should be controled by the markup, not the css.
//
// For lists with padding between items, use blank
// lines between items. This will generate paragraphs with
// padding to space things out.
//
// - item
//
// - item
//
// - item
//
// For list without padding, don't use blank lines.
//
// - item
// - item
// - item
//
// Modifying the css to emulate these behaviors merely brakes
// one case in the process of solving another. Don't change
// this unless it's really really a bug.
ul ul,
ul ol,
ol ol,
ol ul {
margin-top: 0;
margin-bottom: 0;
}
li > p {
margin-top: @margin;
}
dl {
padding: 0;
}
dl dt {
padding: 0;
margin-top: @margin;
font-size: 1em;
font-style: italic;
font-weight: bold;
}
dl dd {
padding: 0 @margin;
margin-bottom: @margin;
}
blockquote {
padding: 0 15px;
color: #777;
border-left: 4px solid #ddd;
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
}
// Tables
table {
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all; // For Firefox to horizontally scroll wider tables.
th {
font-weight: bold;
}
th, td {
padding: 6px 13px;
border: 1px solid #ddd;
}
tr {
background-color: #fff;
border-top: 1px solid #ccc;
&:nth-child(2n) {
background-color: #f8f8f8;
}
}
}
// Images & Stuff
img {
max-width: 100%;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.emoji {
max-width: none;
}
// Gollum Image Tags
// Framed
span.frame {
display: block;
overflow: hidden;
& > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid #ddd;
}
span img {
display: block;
float: left;
}
span span {
display: block;
padding: 5px 0 0;
clear: both;
color: #333;
}
}
span.align-center {
display: block;
overflow: hidden;
clear: both;
& > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
span img {
margin: 0 auto;
text-align: center;
}
}
span.align-right {
display: block;
overflow: hidden;
clear: both;
& > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
span img {
margin: 0;
text-align: right;
}
}
span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
span {
margin: 13px 0 0;
}
}
span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
& > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
}
// Inline code snippets
code,
tt {
padding: 0;
padding-top: 0.2em;
padding-bottom: 0.2em;
margin: 0;
font-size: 85%;
background-color: rgba(0,0,0,0.04);
border-radius: 3px; // don't add padding, gives scrollbars
&:before,
&:after {
letter-spacing: -0.2em; // this creates padding
content: "\00a0";
}
br { display: none; }
}
del code { text-decoration: inherit; }
// Code tags within code blocks (<pre>s)
pre > code {
padding: 0;
margin: 0;
font-size: 100%;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.highlight {
margin-bottom: @margin;
}
.highlight pre,
pre {
padding: @margin;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f7f7f7;
border-radius: 3px;
}
.highlight pre {
margin-bottom: 0;
word-break: normal;
}
pre {
word-wrap: normal;
}
pre code,
pre tt {
display: inline;
max-width: initial;
padding: 0;
margin: 0;
overflow: initial;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
&:before,
&:after {
content: normal;
}
}
kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
color: #555;
vertical-align: middle;
background-color: #fcfcfc;
border: solid 1px #ccc;
border-bottom-color: #bbb;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #bbb;
}
}

37
coffeelint.json

@ -0,0 +1,37 @@
{
"max_line_length": {
"level": "ignore"
},
"no_empty_param_list": {
"level": "error"
},
"arrow_spacing": {
"level": "error"
},
"no_interpolation_in_single_quotes": {
"level": "error"
},
"no_debugger": {
"level": "error"
},
"prefer_english_operator": {
"level": "error"
},
"colon_assignment_spacing": {
"spacing": {
"left": 0,
"right": 1
},
"level": "error"
},
"braces_spacing": {
"spaces": 0,
"level": "error"
},
"spacing_after_comma": {
"level": "error"
},
"no_stand_alone_at": {
"level": "error"
}
}

18
keymaps/markdown-preview.cson

@ -0,0 +1,18 @@
'atom-text-editor':
'ctrl-shift-m': 'markdown-preview:toggle'
'.platform-darwin .markdown-preview':
'cmd-a': 'markdown-preview:select-all'
'cmd-+': 'markdown-preview:zoom-in'
'cmd-=': 'markdown-preview:zoom-in'
'cmd--': 'markdown-preview:zoom-out'
'cmd-_': 'markdown-preview:zoom-out'
'cmd-0': 'markdown-preview:reset-zoom'
'.platform-win32 .markdown-preview, .platform-linux .markdown-preview':
'ctrl-a': 'markdown-preview:select-all'
'ctrl-+': 'markdown-preview:zoom-in'
'ctrl-=': 'markdown-preview:zoom-in'
'ctrl--': 'markdown-preview:zoom-out'
'ctrl-_': 'markdown-preview:zoom-out'
'ctrl-0': 'markdown-preview:reset-zoom'

203
lib/definitions.js

@ -0,0 +1,203 @@
module.exports = class Definitions {
constructor (editor) {
this.editor = editor
this.alreadyAlerted = ''
this.init()
}
init () {
this.definitions = {}
this.definitionParagraphsInClause = []
this.usedDefinitions = {}
this.definedDefinitions = {}
this.undefinedDefinitions = {}
this.otherDefinitions = {}
this.definitionClauseContent = ''
this.definitionClauseEndIndex = null
}
boot () {
this.init()
const success = this.getDefinitionClause()
if (!success) {
return
}
this.setUsedDefinitions()
this.setDefinitions()
this.setDefinedDefinitions()
}
getDefinitionClause () {
const content = this.editor.getText()
const reg = /(# Definitions)([a-z]*[A-Z]*[ ]*)*/g
const result = [...content.matchAll(reg)]
if (result.length === 0) {
const message = 'There is no definition clause defined.'
if (!this.alreadyAlerted || message !== this.alreadyAlerted) {
alert(message)
this.alreadyAlerted = 'message'
}
return false
}
if (result.length > 1) {
const message = `There are ${result.length} definition clauses. There should only be one.`
if (!this.alreadyAlerted || message !== this.alreadyAlerted) {
alert(message)
this.alreadyAlerted = message
}
return false
}
// Definition clause index
const index = result[0].index
let text = content.substring(index)
// Text containing the Definition clause heading
let startText = result[0][0]
this.definitionClauseEndIndex = index + startText.length
// Remove the text containing the Definition clause heading so we can look for the next clause
text = text.substring(startText.length)
const nextClauseReg = /[^#]# ([a-z]*[A-Z]*)*/g
const nextClauseResult = [...text.matchAll(nextClauseReg)]
let nextClauseIndex = nextClauseResult.length > 0 ? nextClauseResult[0].index : text.length
text = text.substring(0, nextClauseIndex)
// Build the full content of the Definition clause
this.definitionClauseContent = startText.concat(text)
return true
}
setUsedDefinitions () {
const content = this.editor.getText()
const reg = /_([^_\n]*)_/g
for (let match of content.matchAll(reg)) {
if (!match[1]) {
continue
}
if (!this.usedDefinitions.hasOwnProperty(match[1])) {
this.usedDefinitions[match[1]] = []
}
this.usedDefinitions[match[1]].push(match.index)
}
}
setDefinitions () {
const content = this.editor.getText()
const reg = /\*\*([^**\n]*)\*\*/g
for (let match of content.matchAll(reg)) {
if (!match[1]) {
continue
}
if (!this.definitions.hasOwnProperty(match[1])) {
this.definitions[match[1]] = []
}
this.definitions[match[1]].push(match.index)
}
}
setDefinedDefinitions () {
const regex = /## (.)*\n/g
const definitionParagraphsInClause = this.definitionClauseContent.matchAll(regex)
const definitionsMapper = JSON.parse(JSON.stringify(this.definitions))
for (let definitionParagraph of definitionParagraphsInClause) {
if (definitionParagraph && definitionParagraph[0]) {
this.definitionParagraphsInClause.push(definitionParagraph)
let definitionContent = definitionParagraph[0]
let found = false
for (let definition of Object.keys(definitionsMapper)) {
const definitionToSearch = `**${definition}** means `
let index = definitionContent.indexOf(definitionToSearch)
if (index !== -1) {
let definitionExtracted = definitionContent.substring(index + definitionToSearch.length)
definitionExtracted = definitionExtracted.replace('[', '')
definitionExtracted = definitionExtracted.replace(']', '')
definitionExtracted = definitionExtracted.trim()
if (definitionExtracted.length > 0) {
this.definedDefinitions[definition] = definitionExtracted
} else {
this.undefinedDefinitions[definition] = true
}
delete definitionsMapper[definition]
found = true
break
}
// let results = definitionContent.match(new RegExp(`(${definition} means )([[(.)*]])`))
// if (results && results[0]) {
//
//
// }
}
if (!found) {
this.otherDefinitions[definitionContent] = true
}
}
}
if (Object.keys(definitionsMapper).length > 0) {
this.insertDefinitions(Object.keys(definitionsMapper))
// Refresh this because it contains more definition paragraphs
this.getDefinitionClause()
}
}
insertDefinitions (definitions) {
let currentRow = this.getRowPositionToInsertDefinitions()
const currentPosition = this.editor.getCursorBufferPosition()
for (let definition of definitions) {
this.editor.setCursorBufferPosition([currentRow, 0])
this.editor.insertText(`## **${definition}** means []\n`)
currentRow = currentRow + 1
this.undefinedDefinitions[definition] = true
}
this.editor.setCursorBufferPosition(currentPosition)
}
getRowPositionToInsertDefinitions () {
const content = this.editor.getText()
const reg = /\n/g
const results = content.matchAll(reg)
let linesCount = 0
for (let result of results) {
if (result.index === this.definitionClauseEndIndex) {
return linesCount + 1 + this.definitionParagraphsInClause.length
} else {
linesCount = linesCount + 1
}
}
return linesCount
}
getDefinedDefinitions () {
return Object.keys(this.definedDefinitions)
}
}

50
lib/extension-helper.js

@ -0,0 +1,50 @@
const scopesByFenceName = {
bash: 'source.shell',
sh: 'source.shell',
powershell: 'source.powershell',
ps1: 'source.powershell',
c: 'source.c',
'c++': 'source.cpp',
cpp: 'source.cpp',
coffee: 'source.coffee',
'coffee-script': 'source.coffee',
coffeescript: 'source.coffee',
cs: 'source.cs',
csharp: 'source.cs',
css: 'source.css',
sass: 'source.sass',
scss: 'source.css.scss',
erlang: 'source.erl',
go: 'source.go',
html: 'text.html.basic',
java: 'source.java',
javascript: 'source.js',
js: 'source.js',
json: 'source.json',
less: 'source.less',
mustache: 'text.html.mustache',
objc: 'source.objc',
'objective-c': 'source.objc',
php: 'text.html.php',
py: 'source.python',
python: 'source.python',
rb: 'source.ruby',
ruby: 'source.ruby',
text: 'text.plain',
toml: 'source.toml',
ts: 'source.ts',
typescript: 'source.ts',
xml: 'text.xml',
yaml: 'source.yaml',
yml: 'source.yaml'
}
module.exports = {
scopeForFenceName (fenceName) {
fenceName = fenceName.toLowerCase()
return scopesByFenceName.hasOwnProperty(fenceName)
? scopesByFenceName[fenceName]
: `source.${fenceName}`
}
}

279
lib/main.js

@ -0,0 +1,279 @@
const fs = require('fs-plus')
const { CompositeDisposable } = require('atom')
const SuggestedDefinition = require('./suggested-definition')
let MarkdownPreviewView = null
let renderer = null
let Definitions = null
let definitionsMapper = {}
const isMarkdownPreviewView = function (object) {
if (MarkdownPreviewView == null) {
MarkdownPreviewView = require('./markdown-preview-view')
}
return object instanceof MarkdownPreviewView
}
const getDefinitionsHandler = function (editor) {
if (Definitions == null) {
Definitions = require('./definitions')
}
return new Definitions(editor)
}
module.exports = {
suggestedDefinitionView: null,
modalPanel: null,
activate (state) {
this.disposables = new CompositeDisposable()
this.commandSubscriptions = new CompositeDisposable()
this.disposables.add(
atom.config.observe('markdown-preview.grammars', grammars => {
this.commandSubscriptions.dispose()
this.commandSubscriptions = new CompositeDisposable()
if (grammars == null) {
grammars = []
}
for (const grammar of grammars.map(grammar =>
grammar.replace(/\./g, ' ')
)) {
this.commandSubscriptions.add(
atom.commands.add(`atom-text-editor[data-grammar='${grammar}']`, {
'markdown-preview:toggle': () => this.toggle(),
'markdown-preview:copy-html': {
displayName: 'Markdown Preview: Copy HTML',
didDispatch: () => this.copyHTML()
},
'markdown-preview:save-as-html': {
displayName: 'Markdown Preview: Save as HTML',
didDispatch: () => this.saveAsHTML()
},
'markdown-preview:toggle-break-on-single-newline': () => {
const keyPath = 'markdown-preview.breakOnSingleNewline'
atom.config.set(keyPath, !atom.config.get(keyPath))
},
'markdown-preview:toggle-github-style': () => {
const keyPath = 'markdown-preview.useGitHubStyle'
atom.config.set(keyPath, !atom.config.get(keyPath))
}
})
)
}
})
)
const previewFile = this.previewFile.bind(this)
for (const extension of [
'markdown',
'md',
'mdown',
'mkd',
'mkdown',
'ron',
'txt'
]) {
this.disposables.add(
atom.commands.add(
`.tree-view .file .name[data-name$=\\.${extension}]`,
'markdown-preview:preview-file',
previewFile
)
)
}
this.disposables.add(
atom.workspace.addOpener(uriToOpen => {
let [protocol, path] = uriToOpen.split('://')
if (protocol !== 'markdown-preview') {
return
}
try {
path = decodeURI(path)
} catch (error) {
return
}
if (path.startsWith('editor/')) {
return this.createMarkdownPreviewView({ editorId: path.substring(7) })
} else {
return this.createMarkdownPreviewView({ filePath: path })
}
})
)
this.suggestedDefinitionView = new SuggestedDefinition()
this.disposables.add(
atom.workspace.observeTextEditors(editor => {
if (editor.buffer && editor.buffer.file) {
const path = editor.buffer.file.path.split('.').pop()
if (path === 'md') { // @TODO Custom extension? Otherwise all markdown files will be handled.
this.bootContractHandler(editor)
}
}
})
)
},
deactivate () {
this.modalPanel.destroy()
this.disposables.dispose()
this.commandSubscriptions.dispose()
this.suggestedDefinitionView.destroy()
},
serialize () {},
createMarkdownPreviewView (state) {
if (state.editorId || fs.isFileSync(state.filePath)) {
if (MarkdownPreviewView == null) {
MarkdownPreviewView = require('./markdown-preview-view')
}
return new MarkdownPreviewView(state)
}
},
toggle () {
if (isMarkdownPreviewView(atom.workspace.getActivePaneItem())) {
atom.workspace.destroyActivePaneItem()
return
}
const editor = atom.workspace.getActiveTextEditor()
if (editor == null) {
return
}
const grammars = atom.config.get('markdown-preview.grammars') || []
if (!grammars.includes(editor.getGrammar().scopeName)) {
return
}
if (!this.removePreviewForEditor(editor)) {
return this.addPreviewForEditor(editor)
}
},
uriForEditor (editor) {
return `markdown-preview://editor/${editor.id}`
},
removePreviewForEditor (editor) {
const uri = this.uriForEditor(editor)
const previewPane = atom.workspace.paneForURI(uri)
if (previewPane != null) {
previewPane.destroyItem(previewPane.itemForURI(uri))
return true
} else {
return false
}
},
addPreviewForEditor (editor) {
const uri = this.uriForEditor(editor)
const previousActivePane = atom.workspace.getActivePane()
const options = { searchAllPanes: true }
if (atom.config.get('markdown-preview.openPreviewInSplitPane')) {
options.split = 'right'
}
return atom.workspace
.open(uri, options)
.then(function (markdownPreviewView) {
if (isMarkdownPreviewView(markdownPreviewView)) {
previousActivePane.activate()
}
})
},
previewFile ({ target }) {
const filePath = target.dataset.path
if (!filePath) {
return
}
for (const editor of atom.workspace.getTextEditors()) {
if (editor.getPath() === filePath) {
return this.addPreviewForEditor(editor)
}
}
atom.workspace.open(`markdown-preview://${encodeURI(filePath)}`, {
searchAllPanes: true
})
},
async copyHTML () {
const editor = atom.workspace.getActiveTextEditor()
if (editor == null) {
return
}
if (renderer == null) {
renderer = require('./renderer')
}
const text = editor.getSelectedText() || editor.getText()
const html = await renderer.toHTML(
text,
editor.getPath(),
editor.getGrammar()
)
atom.clipboard.write(html)
},
saveAsHTML () {
const activePaneItem = atom.workspace.getActivePaneItem()
if (isMarkdownPreviewView(activePaneItem)) {
atom.workspace.getActivePane().saveItemAs(activePaneItem)
return
}
const editor = atom.workspace.getActiveTextEditor()
if (editor == null) {
return
}
const grammars = atom.config.get('markdown-preview.grammars') || []
if (!grammars.includes(editor.getGrammar().scopeName)) {
return
}
const uri = this.uriForEditor(editor)
const markdownPreviewPane = atom.workspace.paneForURI(uri)
const markdownPreviewPaneItem =
markdownPreviewPane != null
? markdownPreviewPane.itemForURI(uri)
: undefined
if (isMarkdownPreviewView(markdownPreviewPaneItem)) {
return markdownPreviewPane.saveItemAs(markdownPreviewPaneItem)
}
},
bootContractHandler (editor) {
definitionsMapper[editor.id] = getDefinitionsHandler(editor)
definitionsMapper[editor.id].boot()
setInterval(() => {
definitionsMapper[editor.id].boot()
}, 5000)
const editorView = atom.views.getView(editor)
editorView.addEventListener('keypress', (event) => {
if (event.keyCode !== 95) {
return
}
this.suggestedDefinitionView.show(editor, definitionsMapper[editor.id].getDefinedDefinitions())
})
}
}

513
lib/markdown-preview-view.js

@ -0,0 +1,513 @@
const path = require('path')
const { Emitter, Disposable, CompositeDisposable, File } = require('atom')
const _ = require('underscore-plus')
const fs = require('fs-plus')
const renderer = require('./renderer')
module.exports = class MarkdownPreviewView {
static deserialize (params) {
return new MarkdownPreviewView(params)
}
constructor ({ editorId, filePath }) {
this.editorId = editorId
this.filePath = filePath
this.element = document.createElement('div')
this.element.classList.add('markdown-preview')
this.element.tabIndex = -1
this.emitter = new Emitter()
this.loaded = false
this.disposables = new CompositeDisposable()
this.registerScrollCommands()
if (this.editorId != null) {
this.resolveEditor(this.editorId)
} else if (atom.packages.hasActivatedInitialPackages()) {
this.subscribeToFilePath(this.filePath)
} else {
this.disposables.add(
atom.packages.onDidActivateInitialPackages(() => {
this.subscribeToFilePath(this.filePath)
})
)
}
}
serialize () {
return {
deserializer: 'MarkdownPreviewView',
filePath: this.getPath() != null ? this.getPath() : this.filePath,
editorId: this.editorId
}
}
copy () {
return new MarkdownPreviewView({
editorId: this.editorId,
filePath: this.getPath() != null ? this.getPath() : this.filePath
})
}
destroy () {
this.disposables.dispose()
this.element.remove()
}
registerScrollCommands () {
this.disposables.add(
atom.commands.add(this.element, {
'core:move-up': () => {
this.element.scrollTop -= document.body.offsetHeight / 20
},
'core:move-down': () => {
this.element.scrollTop += document.body.offsetHeight / 20
},
'core:page-up': () => {
this.element.scrollTop -= this.element.offsetHeight
},
'core:page-down': () => {
this.element.scrollTop += this.element.offsetHeight
},
'core:move-to-top': () => {
this.element.scrollTop = 0
},
'core:move-to-bottom': () => {
this.element.scrollTop = this.element.scrollHeight
}
})
)
}
onDidChangeTitle (callback) {
return this.emitter.on('did-change-title', callback)
}
onDidChangeModified (callback) {
// No op to suppress deprecation warning
return new Disposable()
}
onDidChangeMarkdown (callback) {
return this.emitter.on('did-change-markdown', callback)
}
subscribeToFilePath (filePath) {
this.file = new File(filePath)
this.emitter.emit('did-change-title')
this.disposables.add(
this.file.onDidRename(() => this.emitter.emit('did-change-title'))
)
this.handleEvents()
return this.renderMarkdown()
}
resolveEditor (editorId) {
const resolve = () => {
this.editor = this.editorForId(editorId)
if (this.editor != null) {
this.emitter.emit('did-change-title')
this.disposables.add(
this.editor.onDidDestroy(() =>
this.subscribeToFilePath(this.getPath())
)
)
this.handleEvents()
this.renderMarkdown()
} else {
this.subscribeToFilePath(this.filePath)
}
}
if (atom.packages.hasActivatedInitialPackages()) {
resolve()
} else {
this.disposables.add(atom.packages.onDidActivateInitialPackages(resolve))
}
}
editorForId (editorId) {
for (const editor of atom.workspace.getTextEditors()) {
if (editor.id != null && editor.id.toString() === editorId.toString()) {
return editor
}
}
return null
}
handleEvents () {
const lazyRenderMarkdown = _.debounce(() => this.renderMarkdown(), 250)
this.disposables.add(
atom.grammars.onDidAddGrammar(() => lazyRenderMarkdown())
)
if (typeof atom.grammars.onDidRemoveGrammar === 'function') {
this.disposables.add(
atom.grammars.onDidRemoveGrammar(() => lazyRenderMarkdown())
)
} else {
// TODO: Remove onDidUpdateGrammar hook once onDidRemoveGrammar is released
this.disposables.add(
atom.grammars.onDidUpdateGrammar(() => lazyRenderMarkdown())
)
}
atom.commands.add(this.element, {
'core:copy': event => {
event.stopPropagation()
return this.copyToClipboard()
},
'markdown-preview:select-all': () => {
this.selectAll()
},
'markdown-preview:zoom-in': () => {
const zoomLevel = parseFloat(getComputedStyle(this.element).zoom)
this.element.style.zoom = zoomLevel + 0.1
},
'markdown-preview:zoom-out': () => {
const zoomLevel = parseFloat(getComputedStyle(this.element).zoom)
this.element.style.zoom = zoomLevel - 0.1
},
'markdown-preview:reset-zoom': () => {
this.element.style.zoom = 1
},
'markdown-preview:toggle-break-on-single-newline' () {
const keyPath = 'markdown-preview.breakOnSingleNewline'
atom.config.set(keyPath, !atom.config.get(keyPath))
},
'markdown-preview:toggle-github-style' () {
const keyPath = 'markdown-preview.useGitHubStyle'
atom.config.set(keyPath, !atom.config.get(keyPath))
}
})
const changeHandler = () => {
this.renderMarkdown()
const pane = atom.workspace.paneForItem(this)
if (pane != null && pane !== atom.workspace.getActivePane()) {
pane.activateItem(this)
}
}
if (this.file) {
this.disposables.add(this.file.onDidChange(changeHandler))
} else if (this.editor) {
this.disposables.add(
this.editor.getBuffer().onDidStopChanging(function () {
if (atom.config.get('markdown-preview.liveUpdate')) {
changeHandler()
}
})
)
this.disposables.add(
this.editor.onDidChangePath(() => this.emitter.emit('did-change-title'))
)
this.disposables.add(
this.editor.getBuffer().onDidSave(function () {
if (!atom.config.get('markdown-preview.liveUpdate')) {
changeHandler()
}
})
)
this.disposables.add(
this.editor.getBuffer().onDidReload(function () {
if (!atom.config.get('markdown-preview.liveUpdate')) {
changeHandler()
}
})
)
}
this.disposables.add(
atom.config.onDidChange(
'markdown-preview.breakOnSingleNewline',
changeHandler
)
)
this.disposables.add(
atom.config.observe('markdown-preview.useGitHubStyle', useGitHubStyle => {
if (useGitHubStyle) {
this.element.setAttribute('data-use-github-style', '')
} else {
this.element.removeAttribute('data-use-github-style')
}
})
)
document.onselectionchange = () => {
const selection = window.getSelection()
const selectedNode = selection.baseNode
if (
selectedNode === null ||
this.element === selectedNode ||
this.element.contains(selectedNode)
) {
if (selection.isCollapsed) {
this.element.classList.remove('has-selection')
} else {
this.element.classList.add('has-selection')
}
}
}
}
renderMarkdown () {
if (!this.loaded) {
this.showLoading()
}
return this.getMarkdownSource()
.then(source => {
if (source != null) {
return this.renderMarkdownText(source)
}
})
.catch(reason => this.showError({ message: reason }))
}
getMarkdownSource () {
if (this.file && this.file.getPath()) {
return this.file
.read()
.then(source => {
if (source === null) {
return Promise.reject(
new Error(`${this.file.getBaseName()} could not be found`)
)
} else {
return Promise.resolve(source)
}
})
.catch(reason => Promise.reject(reason))
} else if (this.editor != null) {
return Promise.resolve(this.editor.getText())
} else {
return Promise.reject(new Error('No editor found'))
}
}
async getHTML () {
const source = await this.getMarkdownSource()
if (source == null) {
return
}
return renderer.toHTML(source, this.getPath(), this.getGrammar())
}
async renderMarkdownText (text) {
const { scrollTop } = this.element
try {
const domFragment = await renderer.toDOMFragment(
text,
this.getPath(),
this.getGrammar()
)
this.loading = false
this.loaded = true
this.element.textContent = ''
this.element.appendChild(domFragment)
this.emitter.emit('did-change-markdown')
this.element.scrollTop = scrollTop
} catch (error) {
this.showError(error)
}
}
getTitle () {
if (this.file != null && this.getPath() != null) {
return `${path.basename(this.getPath())} Preview`
} else if (this.editor != null) {
return `${this.editor.getTitle()} Preview`
} else {
return 'Markdown Preview'
}
}
getIconName () {
return 'markdown'
}
getURI () {
if (this.file != null) {
return `markdown-preview://${this.getPath()}`
} else {
return `markdown-preview://editor/${this.editorId}`
}
}
getPath () {
if (this.file != null) {
return this.file.getPath()
} else if (this.editor != null) {
return this.editor.getPath()
}
}
getGrammar () {
return this.editor != null ? this.editor.getGrammar() : undefined
}
getDocumentStyleSheets () {
// This function exists so we can stub it
return document.styleSheets
}
getTextEditorStyles () {
const textEditorStyles = document.createElement('atom-styles')
textEditorStyles.initialize(atom.styles)
textEditorStyles.setAttribute('context', 'atom-text-editor')
document.body.appendChild(textEditorStyles)
// Extract style elements content
return Array.prototype.slice
.apply(textEditorStyles.childNodes)
.map(styleElement => styleElement.innerText)
}
getMarkdownPreviewCSS () {
const markdownPreviewRules = []
const ruleRegExp = /\.markdown-preview/
const cssUrlRegExp = /url\(atom:\/\/markdown-preview\/assets\/(.*)\)/
for (const stylesheet of this.getDocumentStyleSheets()) {
if (stylesheet.rules != null) {
for (const rule of stylesheet.rules) {
// We only need `.markdown-review` css
if (rule.selectorText && rule.selectorText.match(ruleRegExp)) {
markdownPreviewRules.push(rule.cssText)
}
}
}
}
return markdownPreviewRules
.concat(this.getTextEditorStyles())
.join('\n')
.replace(/atom-text-editor/g, 'pre.editor-colors')
.replace(/:host/g, '.host') // Remove shadow-dom :host selector causing problem on FF
.replace(cssUrlRegExp, function (match, assetsName, offset, string) {
// base64 encode assets
const assetPath = path.join(__dirname, '../assets', assetsName)
const originalData = fs.readFileSync(assetPath, 'binary')
const base64Data = Buffer.from(originalData, 'binary').toString(
'base64'
)
return `url('data:image/jpeg;base64,${base64Data}')`
})
}
showError (result) {
this.element.textContent = ''
const h2 = document.createElement('h2')
h2.textContent = 'Previewing Markdown Failed'
this.element.appendChild(h2)
if (result) {
const h3 = document.createElement('h3')
h3.textContent = result.message
this.element.appendChild(h3)
}
}
showLoading () {
this.loading = true
this.element.textContent = ''
const div = document.createElement('div')
div.classList.add('markdown-spinner')
div.textContent = 'Loading Markdown\u2026'
this.element.appendChild(div)
}
selectAll () {
if (this.loading) {
return
}
const selection = window.getSelection()
selection.removeAllRanges()
const range = document.createRange()
range.selectNodeContents(this.element)
selection.addRange(range)
}
async copyToClipboard () {
if (this.loading) {
return
}
const selection = window.getSelection()
const selectedText = selection.toString()
const selectedNode = selection.baseNode
// Use default copy event handler if there is selected text inside this view
if (
selectedText &&
selectedNode != null &&
(this.element === selectedNode || this.element.contains(selectedNode))
) {
atom.clipboard.write(selectedText)
} else {
try {
const html = await this.getHTML()
atom.clipboard.write(html)
} catch (error) {
atom.notifications.addError('Copying Markdown as HTML failed', {
dismissable: true,
detail: error.message
})
}
}
}
getSaveDialogOptions () {
let defaultPath = this.getPath()
if (defaultPath) {
defaultPath += '.html'
} else {
let projectPath
defaultPath = 'untitled.md.html'
if ((projectPath = atom.project.getPaths()[0])) {
defaultPath = path.join(projectPath, defaultPath)
}
}
return { defaultPath }
}
async saveAs (htmlFilePath) {
if (this.loading) {
atom.notifications.addWarning(
'Please wait until the Markdown Preview has finished loading before saving'
)
return
}
const filePath = this.getPath()
let title = 'Markdown to HTML'
if (filePath) {
title = path.parse(filePath).name
}
const htmlBody = await this.getHTML()
const html =
`\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>${title}</title>
<style>${this.getMarkdownPreviewCSS()}</style>
</head>
<body class='markdown-preview' data-use-github-style>${htmlBody}</body>
</html>` + '\n' // Ensure trailing newline
fs.writeFileSync(htmlFilePath, html)
return atom.workspace.open(htmlFilePath)
}
}

245
lib/renderer.js

@ -0,0 +1,245 @@
const { TextEditor } = require('atom')
const path = require('path')
const createDOMPurify = require('dompurify')
const emoji = require('emoji-images')
const fs = require('fs-plus')
let commonmark = null // Defer until used
let renderer = null
let cheerio = null
let yamlFrontMatter = null
const { scopeForFenceName } = require('./extension-helper')
const { resourcePath } = atom.getLoadSettings()
const packagePath = path.dirname(__dirname)
const emojiFolder = path.join(
path.dirname(require.resolve('emoji-images')),
'pngs'
)
exports.toDOMFragment = async function (text, filePath, grammar, callback) {
if (text == null) {
text = ''
}
const domFragment = render(text, filePath)
await highlightCodeBlocks(domFragment, grammar, makeAtomEditorNonInteractive)
return domFragment
}
exports.toHTML = async function (text, filePath, grammar) {
if (text == null) {
text = ''
}
const domFragment = render(text, filePath)
const div = document.createElement('div')
div.appendChild(domFragment)
document.body.appendChild(div)
await highlightCodeBlocks(div, grammar, convertAtomEditorToStandardElement)
const result = div.innerHTML
div.remove()
return result
}
var render = function (text, filePath) {
if (commonmark == null || yamlFrontMatter == null || cheerio == null) {
commonmark = require('commonmark')
yamlFrontMatter = require('yaml-front-matter')
cheerio = require('cheerio')
renderer = new commonmark.Renderer()
renderer.listitem = function (text, isTask) {
const listAttributes = isTask ? ' class="task-list-item"' : ''
return `<li ${listAttributes}>${text}</li>\n`
}
}
// commonmark.setOptions({
// sanitize: false,
// breaks: atom.config.get('markdown-preview.breakOnSingleNewline'),
// renderer
// })
const { __content, ...vars } = yamlFrontMatter.loadFront(text)
const parser = new commonmark.Parser()
const writer = new commonmark.HtmlRenderer()
const parsed = parser.parse(renderYamlTable(vars) + __content)
let html = writer.render(parsed)
// emoji-images is too aggressive, so replace images in monospace tags with the actual emoji text.
const $ = cheerio.load(emoji(html, emojiFolder, 20))
$('pre img').each((index, element) =>
$(element).replaceWith($(element).attr('title'))
)
$('code img').each((index, element) =>
$(element).replaceWith($(element).attr('title'))
)
html = $.html()
html = createDOMPurify().sanitize(html, {
ALLOW_UNKNOWN_PROTOCOLS: atom.config.get(
'markdown-preview.allowUnsafeProtocols'
)
})
const template = document.createElement('template')
template.innerHTML = html.trim()
const fragment = template.content.cloneNode(true)
resolveImagePaths(fragment, filePath)
return fragment
}
function renderYamlTable (variables) {
const entries = Object.entries(variables)
if (!entries.length) {
return ''
}
const markdownRows = [
entries.map(entry => entry[0]),
entries.map(entry => '--'),
entries.map(entry => entry[1])
]
return (
markdownRows.map(row => '| ' + row.join(' | ') + ' |').join('\n') + '\n'
)
}
var resolveImagePaths = function (element, filePath) {
const [rootDirectory] = atom.project.relativizePath(filePath)
const result = []
for (const img of element.querySelectorAll('img')) {
// We use the raw attribute instead of the .src property because the value
// of the property seems to be transformed in some cases.
let src
if ((src = img.getAttribute('src'))) {
if (src.match(/^(https?|atom):\/\//)) {
continue
}
if (src.startsWith(process.resourcesPath)) {
continue
}
if (src.startsWith(resourcePath)) {
continue
}
if (src.startsWith(packagePath)) {
continue
}
if (src[0] === '/') {
if (!fs.isFileSync(src)) {
if (rootDirectory) {
result.push((img.src = path.join(rootDirectory, src.substring(1))))
} else {
result.push(undefined)
}
} else {
result.push(undefined)
}
} else {
result.push((img.src = path.resolve(path.dirname(filePath), src)))
}
} else {
result.push(undefined)
}
}
return result
}
var highlightCodeBlocks = function (domFragment, grammar, editorCallback) {
let defaultLanguage, fontFamily
if (
(grammar != null ? grammar.scopeName : undefined) === 'source.litcoffee'
) {
defaultLanguage = 'coffee'
} else {
defaultLanguage = 'text'
}
if ((fontFamily = atom.config.get('editor.fontFamily'))) {
for (const codeElement of domFragment.querySelectorAll('code')) {
codeElement.style.fontFamily = fontFamily
}
}
const promises = []
for (const preElement of domFragment.querySelectorAll('pre')) {
const codeBlock =
preElement.firstElementChild != null
? preElement.firstElementChild
: preElement
const className = codeBlock.getAttribute('class')
const fenceName =
className != null ? className.replace(/^language-/, '') : defaultLanguage
const editor = new TextEditor({
readonly: true,
keyboardInputEnabled: false
})
const editorElement = editor.getElement()
preElement.classList.add('editor-colors', `lang-${fenceName}`)
editorElement.setUpdatedSynchronously(true)
preElement.innerHTML = ''
preElement.parentNode.insertBefore(editorElement, preElement)
editor.setText(codeBlock.textContent.replace(/\r?\n$/, ''))
atom.grammars.assignLanguageMode(editor, scopeForFenceName(fenceName))
editor.setVisible(true)
promises.push(editorCallback(editorElement, preElement))
}
return Promise.all(promises)
}
var makeAtomEditorNonInteractive = function (editorElement, preElement) {
preElement.remove()
editorElement.setAttributeNode(document.createAttribute('gutter-hidden')) // Hide gutter
editorElement.removeAttribute('tabindex') // Make read-only
// Remove line decorations from code blocks.
for (const cursorLineDecoration of editorElement.getModel()
.cursorLineDecorations) {
cursorLineDecoration.destroy()
}
}
var convertAtomEditorToStandardElement = (editorElement, preElement) => {
return new Promise(function (resolve) {
const editor = editorElement.getModel()
const done = () =>
editor.component.getNextUpdatePromise().then(function () {
for (const line of editorElement.querySelectorAll(
'.line:not(.dummy)'
)) {
const line2 = document.createElement('div')
line2.className = 'line'
line2.innerHTML = line.firstChild.innerHTML
preElement.appendChild(line2)
}
editorElement.remove()
resolve()
})
const languageMode = editor.getBuffer().getLanguageMode()
if (languageMode.fullyTokenized || languageMode.tree) {
done()
} else {
editor.onDidTokenize(done)
}
})
}

64
lib/suggested-definition.js

@ -0,0 +1,64 @@
const {SelectListView} = require('atom-space-pen-views')
module.exports = class SuggestedDefinition extends SelectListView {
constructor () {
super()
this.editor = null
this.addClass('overlay from-top')
this.panel = atom.workspace.addModalPanel({
item: this.getElement(),
visible: false
})
}
viewForItem (item) {
return `<li>${item}</li>`
}
confirmed (item) {
this.hide()
const position = this.editor.getCursorBufferPosition()
const definition = `${item}_ `
this.editor.insertText(definition)
position.column = position.column + definition.length
this.editor.setCursorBufferPosition(position)
}
cancelled () {
this.hide()
this.editor.component.didFocus()
}
// Returns an object that can be retrieved when package is activated
serialize () {}
// Tear down any state and detach
destroy () {
this.element.remove()
}
getElement () {
return this.element
}
show (editor, items) {
this.editor = editor
this.setItems(items)
this.panel.show()
this.focusFilterEditor()
}
hide () {
this.panel.hide()
}
}

37
menus/markdown-preview.cson

@ -0,0 +1,37 @@
menu: [
label: 'Packages'
submenu: [
label: 'Markdown Preview'
submenu: [
{label: 'Toggle Preview', command: 'markdown-preview:toggle'}
{label: 'Toggle Break on Single Newline', command: 'markdown-preview:toggle-break-on-single-newline'}
{label: 'Toggle GitHub Style', command: 'markdown-preview:toggle-github-style'}
]
]
]
'context-menu':
'.markdown-preview': [
{label: 'Select All', command: 'markdown-preview:select-all'}
{label: 'Save As HTML\u2026', command: 'core:save-as'}
]
'.markdown-preview.has-selection': [
{label: 'Copy', command: 'core:copy'}
]
'.markdown-preview:not(.has-selection)': [
{label: 'Copy As HTML', command: 'core:copy'}
]
'.tree-view .file .name[data-name$=\\.markdown]':
[{label: 'Markdown Preview', command: 'markdown-preview:preview-file'}]
'.tree-view .file .name[data-name$=\\.md]':
[{label: 'Markdown Preview', command: 'markdown-preview:preview-file'}]
'.tree-view .file .name[data-name$=\\.mdown]':
[{label: 'Markdown Preview', command: 'markdown-preview:preview-file'}]
'.tree-view .file .name[data-name$=\\.mkd]':
[{label: 'Markdown Preview', command: 'markdown-preview:preview-file'}]
'.tree-view .file .name[data-name$=\\.mkdown]':
[{label: 'Markdown Preview', command: 'markdown-preview:preview-file'}]
'.tree-view .file .name[data-name$=\\.ron]':
[{label: 'Markdown Preview', command: 'markdown-preview:preview-file'}]
'.tree-view .file .name[data-name$=\\.txt]':
[{label: 'Markdown Preview', command: 'markdown-preview:preview-file'}]

4480
package-lock.json
File diff suppressed because it is too large
View File

81
package.json

@ -0,0 +1,81 @@
{
"name": "markdown-preview",
"version": "0.160.2",
"main": "./lib/main",
"description": "Open a rendered version of the Markdown in the current editor with `ctrl-shift-m`.",
"repository": "https://github.com/atom/markdown-preview",
"license": "MIT",
"engines": {
"atom": "*"
},
"dependencies": {
"atom-space-pen-views": "^2.2.0",
"cheerio": "^1.0.0-rc.3",
"commonmark": "^0.29.3",
"dompurify": "^2.0.7",
"emoji-images": "^0.1.1",
"fs-plus": "^3.0.0",
"marked": "^0.7.0",
"underscore-plus": "^1.0.0",
"yaml-front-matter": "^4.0.0"
},
"devDependencies": {
"coffeelint": "^1.9.7",
"standard": "^10.0.3",
"temp": "^0.8.1"
},
"deserializers": {
"MarkdownPreviewView": "createMarkdownPreviewView"
},
"configSchema": {
"breakOnSingleNewline": {
"type": "boolean",
"default": false,
"description": "In Markdown, a single newline character doesn't cause a line break in the generated HTML. In GitHub Flavored Markdown, that is not true. Enable this config option to insert line breaks in rendered HTML for single newlines in Markdown source."
},
"liveUpdate": {
"type": "boolean",
"default": true,
"description": "Re-render the preview as the contents of the source changes, without requiring the source buffer to be saved. If disabled, the preview is re-rendered only when the buffer is saved to disk."
},
"openPreviewInSplitPane": {
"type": "boolean",
"default": true,
"description": "Open the preview in a split pane. If disabled, the preview is opened in a new tab in the same pane."
},
"allowUnsafeProtocols": {
"type": "boolean",
"default": false,
"description": "Allow HTML attributes to use protocols normally considered unsafe such as `file://` and absolute paths on Windows."
},
"grammars": {
"type": "array",
"default": [
"source.gfm",
"source.litcoffee",
"text.html.basic",
"text.md",
"text.plain",
"text.plain.null-grammar"
],
"description": "List of scopes for languages for which previewing is enabled. See [this README](https://github.com/atom/spell-check#spell-check-package-) for more information on finding the correct scope for a specific language."
},
"useGitHubStyle": {
"title": "Use GitHub.com style",
"type": "boolean",
"default": false,
"description": "Use the same CSS styles for preview as the ones used on GitHub.com."
}
},
"standard": {
"env": {
"atomtest": true,
"browser": true,
"jasmine": true,
"node": true
},
"globals": [
"atom"
]
}
}

1
spec/fixtures/subdir/áccéntéd.md

@ -0,0 +1 @@
# Testing

9
spec/fixtures/subdir/code-block.md

@ -0,0 +1,9 @@
# Code Block
```javascript
if a === 3 {
b = 5
}
```
encoding → issue

4
spec/fixtures/subdir/doctype-tag.md

@ -0,0 +1,4 @@
<!doctype html>
content
<!doctype html>

5
spec/fixtures/subdir/evil.md

@ -0,0 +1,5 @@
hello
<script src="index.js"></script>
<script>alert('rm -fr')</script>
<img onload="alert('rm -rf')" onerror="alert('rm -fr')">
world

1
spec/fixtures/subdir/file with space.md

@ -0,0 +1 @@
# Testing

51
spec/fixtures/subdir/file.markdown

@ -0,0 +1,51 @@
---
variable1: value1
array:
- foo
- bar
---
## File.markdown
:cool:
```
function f(x) {
return x++;
}
```
```Ruby
def func
x = 1
end
```
* ```javascript
if a === 3 {
b = 5
}
```
```kombucha
drink-that-stuff:
tastes-weird~
```
```python
def foo()
bar
baz
```
![Image1](image1.png)
![Image2](/tmp/image2.png)
![Image3](http://github.com/image3.png)
lorem
ipsum

1
spec/fixtures/subdir/html-tag.md

@ -0,0 +1 @@
<html>content</html>

1
spec/fixtures/subdir/pre-tag.md

@ -0,0 +1 @@
<pre>hey</pre>

5
spec/fixtures/subdir/simple.md

@ -0,0 +1,5 @@
*italic*
**bold**
encoding → issue

1
spec/fixtures/subdir/áccéntéd.md

@ -0,0 +1 @@
# Testing

839
spec/markdown-preview-spec.js

@ -0,0 +1,839 @@
const path = require('path')
const fs = require('fs-plus')
const temp = require('temp').track()
const MarkdownPreviewView = require('../lib/markdown-preview-view')
const { TextEditor } = require('atom')
const TextMateLanguageMode = new TextEditor().getBuffer().getLanguageMode()
.constructor
describe('Markdown Preview', function () {
let preview = null
beforeEach(function () {
const fixturesPath = path.join(__dirname, 'fixtures')
const tempPath = temp.mkdirSync('atom')
fs.copySync(fixturesPath, tempPath)
atom.project.setPaths([tempPath])
jasmine.unspy(TextMateLanguageMode.prototype, 'tokenizeInBackground')
jasmine.useRealClock()
jasmine.attachToDOM(atom.views.getView(atom.workspace))
waitsForPromise(() => atom.packages.activatePackage('markdown-preview'))
waitsForPromise(() => atom.packages.activatePackage('language-gfm'))
runs(() =>
spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true)
)
})
const expectPreviewInSplitPane = function () {
waitsFor(() => atom.workspace.getCenter().getPanes().length === 2)
waitsFor(
'markdown preview to be created',
() =>
(preview = atom.workspace
.getCenter()
.getPanes()[1]
.getActiveItem())
)
runs(() => {
expect(preview).toBeInstanceOf(MarkdownPreviewView)
expect(preview.getPath()).toBe(
atom.workspace.getActivePaneItem().getPath()
)
})
}
describe('when a preview has not been created for the file', function () {
it('displays a markdown preview in a split pane', function () {
waitsForPromise(() => atom.workspace.open('subdir/file.markdown'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() => {
const [editorPane] = atom.workspace.getCenter().getPanes()
expect(editorPane.getItems()).toHaveLength(1)
expect(editorPane.isActive()).toBe(true)
})
})
describe("when the editor's path does not exist", function () {
it('splits the current pane to the right with a markdown preview for the file', function () {
waitsForPromise(() => atom.workspace.open('new.markdown'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
})
})
describe('when the editor does not have a path', function () {
it('splits the current pane to the right with a markdown preview for the file', function () {
waitsForPromise(() => atom.workspace.open(''))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
})
})
describe('when the path contains a space', function () {
it('renders the preview', function () {
waitsForPromise(() => atom.workspace.open('subdir/file with space.md'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
})
})
describe('when the path contains accented characters', function () {
it('renders the preview', function () {
waitsForPromise(() => atom.workspace.open('subdir/áccéntéd.md'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
})
})
})
describe('when a preview has been created for the file', function () {
beforeEach(function () {
waitsForPromise(() => atom.workspace.open('subdir/file.markdown'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
})
it('closes the existing preview when toggle is triggered a second time on the editor', function () {
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
const [editorPane, previewPane] = atom.workspace.getCenter().getPanes()
expect(editorPane.isActive()).toBe(true)
expect(previewPane.getActiveItem()).toBeUndefined()
})
it('closes the existing preview when toggle is triggered on it and it has focus', function () {
const [editorPane, previewPane] = atom.workspace.getCenter().getPanes()
previewPane.activate()
atom.commands.dispatch(
editorPane.getActiveItem().getElement(),
'markdown-preview:toggle'
)
expect(previewPane.getActiveItem()).toBeUndefined()
})
describe('when the editor is modified', function () {
it('re-renders the preview', function () {
spyOn(preview, 'showLoading')
const markdownEditor = atom.workspace.getActiveTextEditor()
markdownEditor.setText('Hey!')
waitsFor(() => preview.element.textContent.includes('Hey!'))
runs(() => expect(preview.showLoading).not.toHaveBeenCalled())
})
it('invokes ::onDidChangeMarkdown listeners', function () {
let listener
const markdownEditor = atom.workspace.getActiveTextEditor()
preview.onDidChangeMarkdown(
(listener = jasmine.createSpy('didChangeMarkdownListener'))
)
runs(() => markdownEditor.setText('Hey!'))
waitsFor(
'::onDidChangeMarkdown handler to be called',
() => listener.callCount > 0
)
})
describe('when the preview is in the active pane but is not the active item', function () {
it('re-renders the preview but does not make it active', function () {
const markdownEditor = atom.workspace.getActiveTextEditor()
const previewPane = atom.workspace.getCenter().getPanes()[1]
previewPane.activate()
waitsForPromise(() => atom.workspace.open())
runs(() => markdownEditor.setText('Hey!'))
waitsFor(() => preview.element.textContent.includes('Hey!'))
runs(() => {
expect(previewPane.isActive()).toBe(true)
expect(previewPane.getActiveItem()).not.toBe(preview)
})
})
})
describe('when the preview is not the active item and not in the active pane', function () {
it('re-renders the preview and makes it active', function () {
const markdownEditor = atom.workspace.getActiveTextEditor()
const [
editorPane,
previewPane
] = atom.workspace.getCenter().getPanes()
previewPane.splitRight({ copyActiveItem: true })
previewPane.activate()
waitsForPromise(() => atom.workspace.open())
runs(() => {
editorPane.activate()
markdownEditor.setText('Hey!')
})
waitsFor(() => preview.element.textContent.includes('Hey!'))
runs(() => {
expect(editorPane.isActive()).toBe(true)
expect(previewPane.getActiveItem()).toBe(preview)
})
})
})
describe('when the liveUpdate config is set to false', function () {
it('only re-renders the markdown when the editor is saved, not when the contents are modified', function () {
atom.config.set('markdown-preview.liveUpdate', false)
const didStopChangingHandler = jasmine.createSpy(
'didStopChangingHandler'
)
atom.workspace
.getActiveTextEditor()
.getBuffer()
.onDidStopChanging(didStopChangingHandler)
atom.workspace.getActiveTextEditor().setText('ch ch changes')
waitsFor(() => didStopChangingHandler.callCount > 0)
runs(() => {
expect(preview.element.textContent).not.toMatch('ch ch changes')
atom.workspace.getActiveTextEditor().save()
})
waitsFor(() => preview.element.textContent.includes('ch ch changes'))
})
})
})
describe('when the original preview is split', function () {
it('renders another preview in the new split pane', function () {
atom.workspace
.getCenter()
.getPanes()[1]
.splitRight({ copyActiveItem: true })
expect(atom.workspace.getCenter().getPanes()).toHaveLength(3)
waitsFor(
'split markdown preview to be created',
() =>
(preview = atom.workspace
.getCenter()
.getPanes()[2]
.getActiveItem())
)
runs(() => {
expect(preview).toBeInstanceOf(MarkdownPreviewView)
expect(preview.getPath()).toBe(
atom.workspace.getActivePaneItem().getPath()
)
})
})
})
describe('when the editor is destroyed', function () {
beforeEach(() =>
atom.workspace
.getCenter()
.getPanes()[0]
.destroyActiveItem()
)
it('falls back to using the file path', function () {
atom.workspace
.getCenter()
.getPanes()[1]
.activate()
expect(preview.file.getPath()).toBe(
atom.workspace.getActivePaneItem().getPath()
)
})
it('continues to update the preview if the file is changed on #win32 and #darwin', function () {
let listener
const titleChangedCallback = jasmine.createSpy('titleChangedCallback')
runs(() => {
expect(preview.getTitle()).toBe('file.markdown Preview')
preview.onDidChangeTitle(titleChangedCallback)
fs.renameSync(
preview.getPath(),
path.join(path.dirname(preview.getPath()), 'file2.md')
)
})
waitsFor(
'title to update',
() => preview.getTitle() === 'file2.md Preview'
)
runs(() => expect(titleChangedCallback).toHaveBeenCalled())
spyOn(preview, 'showLoading')
runs(() => fs.writeFileSync(preview.getPath(), 'Hey!'))
waitsFor('contents to update', () =>
preview.element.textContent.includes('Hey!')
)
runs(() => expect(preview.showLoading).not.toHaveBeenCalled())
preview.onDidChangeMarkdown(
(listener = jasmine.createSpy('didChangeMarkdownListener'))
)
runs(() => fs.writeFileSync(preview.getPath(), 'Hey!'))
waitsFor(
'::onDidChangeMarkdown handler to be called',
() => listener.callCount > 0
)
})
it('allows a new split pane of the preview to be created', function () {
atom.workspace
.getCenter()
.getPanes()[1]
.splitRight({ copyActiveItem: true })
expect(atom.workspace.getCenter().getPanes()).toHaveLength(3)
waitsFor(
'split markdown preview to be created',
() =>
(preview = atom.workspace
.getCenter()
.getPanes()[2]
.getActiveItem())
)
runs(() => {
expect(preview).toBeInstanceOf(MarkdownPreviewView)
expect(preview.getPath()).toBe(
atom.workspace.getActivePaneItem().getPath()
)
})
})
})
})
describe('when the markdown preview view is requested by file URI', function () {
it('opens a preview editor and watches the file for changes', function () {
waitsForPromise('atom.workspace.open promise to be resolved', () =>
atom.workspace.open(
`markdown-preview://${atom.project
.getDirectories()[0]
.resolve('subdir/file.markdown')}`
)
)
runs(() => {
preview = atom.workspace.getActivePaneItem()
expect(preview).toBeInstanceOf(MarkdownPreviewView)
spyOn(preview, 'renderMarkdownText')
preview.file.emitter.emit('did-change')
})
waitsFor(
'markdown to be re-rendered after file changed',
() => preview.renderMarkdownText.callCount > 0
)
})
})
describe("when the editor's grammar it not enabled for preview", function () {
it('does not open the markdown preview', function () {
atom.config.set('markdown-preview.grammars', [])
waitsForPromise(() => atom.workspace.open('subdir/file.markdown'))
runs(() => {
spyOn(atom.workspace, 'open').andCallThrough()
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
expect(atom.workspace.open).not.toHaveBeenCalled()
})
})
})
describe("when the editor's path changes on #win32 and #darwin", function () {
it("updates the preview's title", function () {
const titleChangedCallback = jasmine.createSpy('titleChangedCallback')
waitsForPromise(() => atom.workspace.open('subdir/file.markdown'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() => {
expect(preview.getTitle()).toBe('file.markdown Preview')
preview.onDidChangeTitle(titleChangedCallback)
fs.renameSync(
atom.workspace.getActiveTextEditor().getPath(),
path.join(
path.dirname(atom.workspace.getActiveTextEditor().getPath()),
'file2.md'
)
)
})
waitsFor(() => preview.getTitle() === 'file2.md Preview')
runs(() => expect(titleChangedCallback).toHaveBeenCalled())
})
})
describe('when the URI opened does not have a markdown-preview protocol', function () {
it('does not throw an error trying to decode the URI (regression)', function () {
waitsForPromise(() => atom.workspace.open('%'))
runs(() => expect(atom.workspace.getActiveTextEditor()).toBeTruthy())
})
})
describe('markdown-preview:toggle', function () {
beforeEach(() =>
waitsForPromise(() => atom.workspace.open('code-block.md'))
)
it('does not exist for text editors that are not set to a grammar defined in `markdown-preview.grammars`', function () {
atom.config.set('markdown-preview.grammars', ['source.weird-md'])
const editorElement = atom.workspace.getActiveTextEditor().getElement()
const commands = atom.commands
.findCommands({ target: editorElement })
.map(command => command.name)
expect(commands).not.toContain('markdown-preview:toggle')
})
it('exists for text editors that are set to a grammar defined in `markdown-preview.grammars`', function () {
atom.config.set('markdown-preview.grammars', ['source.gfm'])
const editorElement = atom.workspace.getActiveTextEditor().getElement()
const commands = atom.commands
.findCommands({ target: editorElement })
.map(command => command.name)
expect(commands).toContain('markdown-preview:toggle')
})
it('updates whenever the list of grammars changes', function () {
// Last two tests combined
atom.config.set('markdown-preview.grammars', ['source.gfm', 'text.plain'])
const editorElement = atom.workspace.getActiveTextEditor().getElement()
let commands = atom.commands
.findCommands({ target: editorElement })
.map(command => command.name)
expect(commands).toContain('markdown-preview:toggle')
atom.config.set('markdown-preview.grammars', [
'source.weird-md',
'text.plain'
])
commands = atom.commands
.findCommands({ target: editorElement })
.map(command => command.name)
expect(commands).not.toContain('markdown-preview:toggle')
})
})
describe('when markdown-preview:copy-html is triggered', function () {
it('copies the HTML to the clipboard', function () {
waitsForPromise(() => atom.workspace.open('subdir/simple.md'))
waitsForPromise(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:copy-html'
)
)
runs(() => {
expect(atom.clipboard.read()).toBe(`\
<p><em>italic</em></p>
<p><strong>bold</strong></p>
<p>encoding \u2192 issue</p>\
`)
atom.workspace
.getActiveTextEditor()
.setSelectedBufferRange([[0, 0], [1, 0]])
})
waitsForPromise(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:copy-html'
)
)
runs(() =>
expect(atom.clipboard.read()).toBe(`\
<p><em>italic</em></p>\
`)
)
})
describe('code block tokenization', function () {
beforeEach(function () {
waitsForPromise(() => atom.packages.activatePackage('language-ruby'))
waitsForPromise(() => atom.packages.activatePackage('markdown-preview'))
waitsForPromise(() => atom.workspace.open('subdir/file.markdown'))
waitsForPromise(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:copy-html'
)
)
runs(() => {
preview = document.createElement('div')
preview.innerHTML = atom.clipboard.read()
})
})
describe("when the code block's fence name has a matching grammar", function () {
it('tokenizes the code block with the grammar', function () {
expect(
preview.querySelector('pre span.entity.name.function.ruby')
).toBeDefined()
})
})
describe("when the code block's fence name doesn't have a matching grammar", function () {
it('does not tokenize the code block', function () {
expect(
preview.querySelectorAll(
'pre.lang-kombucha .line .syntax--null-grammar'
).length
).toBe(2)
})
})
describe('when the code block contains empty lines', function () {
it("doesn't remove the empty lines", function () {
expect(preview.querySelector('pre.lang-python').children.length).toBe(
6
)
expect(
preview
.querySelector('pre.lang-python div:nth-child(2)')
.textContent.trim()
).toBe('')
expect(
preview
.querySelector('pre.lang-python div:nth-child(4)')
.textContent.trim()
).toBe('')
expect(
preview
.querySelector('pre.lang-python div:nth-child(5)')
.textContent.trim()
).toBe('')
})
})
describe('when the code block is nested in a list', function () {
it('detects and styles the block', function () {
expect(preview.querySelector('pre.lang-javascript')).toHaveClass(
'editor-colors'
)
})
})
})
})
describe('sanitization', function () {
it('removes script tags and attributes that commonly contain inline scripts', function () {
waitsForPromise(() => atom.workspace.open('subdir/evil.md'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() =>
expect(preview.element.innerHTML).toBe(`\
<p>hello</p>
<img>
world\
`)
)
})
it('remove any <!doctype> tag on markdown files', function () {
waitsForPromise(() => atom.workspace.open('subdir/doctype-tag.md'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() =>
expect(preview.element.innerHTML).toBe(`\
<p>content
</p>\
`)
)
})
})
describe('when the markdown contains an <html> tag', function () {
it('does not throw an exception', function () {
waitsForPromise(() => atom.workspace.open('subdir/html-tag.md'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() => expect(preview.element.innerHTML).toBe('content'))
})
})
describe('when the markdown contains a <pre> tag', function () {
it('does not throw an exception', function () {
waitsForPromise(() => atom.workspace.open('subdir/pre-tag.md'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() =>
expect(preview.element.querySelector('atom-text-editor')).toBeDefined()
)
})
})
describe('when there is an image with a relative path and no directory', function () {
it('does not alter the image src', function () {
for (let projectPath of atom.project.getPaths()) {
atom.project.removePath(projectPath)
}
const filePath = path.join(temp.mkdirSync('atom'), 'bar.md')
fs.writeFileSync(filePath, '![rel path](/foo.png)')
waitsForPromise(() => atom.workspace.open(filePath))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() =>
expect(preview.element.innerHTML).toBe(`\
<p><img alt="rel path" src="/foo.png"></p>\
`)
)
})
})
describe('GitHub style markdown preview', function () {
beforeEach(() => atom.config.set('markdown-preview.useGitHubStyle', false))
it('renders markdown using the default style when GitHub styling is disabled', function () {
waitsForPromise(() => atom.workspace.open('subdir/simple.md'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() =>
expect(preview.element.getAttribute('data-use-github-style')).toBeNull()
)
})
it('renders markdown using the GitHub styling when enabled', function () {
atom.config.set('markdown-preview.useGitHubStyle', true)
waitsForPromise(() => atom.workspace.open('subdir/simple.md'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() =>
expect(preview.element.getAttribute('data-use-github-style')).toBe('')
)
})
it('updates the rendering style immediately when the configuration is changed', function () {
waitsForPromise(() => atom.workspace.open('subdir/simple.md'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
runs(() => {
expect(preview.element.getAttribute('data-use-github-style')).toBeNull()
atom.config.set('markdown-preview.useGitHubStyle', true)
expect(
preview.element.getAttribute('data-use-github-style')
).not.toBeNull()
atom.config.set('markdown-preview.useGitHubStyle', false)
expect(preview.element.getAttribute('data-use-github-style')).toBeNull()
})
})
})
describe('when markdown-preview:save-as-html is triggered', function () {
beforeEach(function () {
waitsForPromise(() => atom.workspace.open('subdir/simple.markdown'))
runs(() =>
atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:toggle'
)
)
expectPreviewInSplitPane()
})
it('saves the HTML when it is triggered and the editor has focus', function () {
const [editorPane] = atom.workspace.getCenter().getPanes()
editorPane.activate()
const outputPath = temp.path({ suffix: '.html' })
expect(fs.existsSync(outputPath)).toBe(false)
runs(() => {
spyOn(preview, 'getSaveDialogOptions').andReturn({
defaultPath: outputPath
})
spyOn(atom.applicationDelegate, 'showSaveDialog').andCallFake(function (
options,
callback
) {
if (typeof callback === 'function') {
callback(options.defaultPath)
}
// TODO: When https://github.com/atom/atom/pull/16245 lands remove the return
// and the existence check on the callback
return options.defaultPath
})
return atom.commands.dispatch(
atom.workspace.getActiveTextEditor().getElement(),
'markdown-preview:save-as-html'
)
})
waitsFor(() => fs.existsSync(outputPath))
runs(() => expect(fs.existsSync(outputPath)).toBe(true))
})
it('saves the HTML when it is triggered and the preview pane has focus', function () {
const [editorPane, previewPane] = atom.workspace.getCenter().getPanes()
previewPane.activate()
const outputPath = temp.path({ suffix: '.html' })
expect(fs.existsSync(outputPath)).toBe(false)
runs(() => {
spyOn(preview, 'getSaveDialogOptions').andReturn({
defaultPath: outputPath
})
spyOn(atom.applicationDelegate, 'showSaveDialog').andCallFake(function (
options,
callback
) {
if (typeof callback === 'function') {
callback(options.defaultPath)
}
// TODO: When https://github.com/atom/atom/pull/16245 lands remove the return
// and the existence check on the callback
return options.defaultPath
})
return atom.commands.dispatch(
editorPane.getActiveItem().getElement(),
'markdown-preview:save-as-html'
)
})
waitsFor(() => fs.existsSync(outputPath))
runs(() => expect(fs.existsSync(outputPath)).toBe(true))
})
})
})

604
spec/markdown-preview-view-spec.js

@ -0,0 +1,604 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const path = require('path')
const fs = require('fs-plus')
const temp = require('temp').track()
const url = require('url')
const { TextEditor } = require('atom')
const MarkdownPreviewView = require('../lib/markdown-preview-view')
const TextMateLanguageMode = new TextEditor().getBuffer().getLanguageMode()
.constructor
describe('MarkdownPreviewView', function () {
let preview = null
beforeEach(function () {
// Makes _.debounce work
jasmine.useRealClock()
jasmine.unspy(TextMateLanguageMode.prototype, 'tokenizeInBackground')
spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true)
const filePath = atom.project
.getDirectories()[0]
.resolve('subdir/file.markdown')
preview = new MarkdownPreviewView({ filePath })
jasmine.attachToDOM(preview.element)
waitsForPromise(() => atom.packages.activatePackage('language-ruby'))
waitsForPromise(() => atom.packages.activatePackage('language-javascript'))
waitsForPromise(() => atom.packages.activatePackage('markdown-preview'))
})
afterEach(() => preview.destroy())
describe('::constructor', function () {
it('shows a loading spinner and renders the markdown', function () {
preview.showLoading()
expect(preview.element.querySelector('.markdown-spinner')).toBeDefined()
waitsForPromise(() => preview.renderMarkdown())
runs(() => expect(preview.element.querySelector('.emoji')).toBeDefined())
})
it('shows an error message when there is an error', function () {
preview.showError('Not a real file')
expect(preview.element.textContent).toMatch('Failed')
})
it('rerenders the markdown and the scrollTop stays the same', function () {
waitsForPromise(() => preview.renderMarkdown())
runs(function () {
preview.element.style.maxHeight = '10px'
preview.element.scrollTop = 24
expect(preview.element.scrollTop).toBe(24)
})
waitsForPromise(() => preview.renderMarkdown())
runs(() => expect(preview.element.scrollTop).toBe(24))
})
})
describe('serialization', function () {
let newPreview = null
afterEach(function () {
if (newPreview) {
newPreview.destroy()
}
})
it('recreates the preview when serialized/deserialized', function () {
newPreview = atom.deserializers.deserialize(preview.serialize())
jasmine.attachToDOM(newPreview.element)
expect(newPreview.getPath()).toBe(preview.getPath())
})
it('does not recreate a preview when the file no longer exists', function () {
const filePath = path.join(temp.mkdirSync('markdown-preview-'), 'foo.md')
fs.writeFileSync(filePath, '# Hi')
preview.destroy()
preview = new MarkdownPreviewView({ filePath })
const serialized = preview.serialize()
fs.removeSync(filePath)
newPreview = atom.deserializers.deserialize(serialized)
expect(newPreview).toBeUndefined()
})
it('serializes the editor id when opened for an editor', function () {
preview.destroy()
waitsForPromise(() => atom.workspace.open('new.markdown'))
runs(function () {
preview = new MarkdownPreviewView({
editorId: atom.workspace.getActiveTextEditor().id
})
jasmine.attachToDOM(preview.element)
expect(preview.getPath()).toBe(
atom.workspace.getActiveTextEditor().getPath()
)
newPreview = atom.deserializers.deserialize(preview.serialize())
jasmine.attachToDOM(newPreview.element)
expect(newPreview.getPath()).toBe(preview.getPath())
})
})
})
describe('code block conversion to atom-text-editor tags', function () {
beforeEach(function () {
waitsForPromise(() => preview.renderMarkdown())
})
it('removes line decorations on rendered code blocks', function () {
const editor = preview.element.querySelector(
"atom-text-editor[data-grammar='text plain null-grammar']"
)
const decorations = editor
.getModel()
.getDecorations({ class: 'cursor-line', type: 'line' })
expect(decorations.length).toBe(0)
})
it('sets the editors as read-only', function () {
preview.element
.querySelectorAll('atom-text-editor')
.forEach(editorElement =>
expect(editorElement.getAttribute('tabindex')).toBeNull()
)
})
describe("when the code block's fence name has a matching grammar", function () {
it('assigns the grammar on the atom-text-editor', function () {
const rubyEditor = preview.element.querySelector(
"atom-text-editor[data-grammar='source ruby']"
)
expect(rubyEditor.getModel().getText()).toBe(`\
def func
x = 1
end\
`)
// nested in a list item
const jsEditor = preview.element.querySelector(
"atom-text-editor[data-grammar='source js']"
)
expect(jsEditor.getModel().getText()).toBe(`\
if a === 3 {
b = 5
}\
`)
})
})
describe("when the code block's fence name doesn't have a matching grammar", function () {
it('does not assign a specific grammar', function () {
const plainEditor = preview.element.querySelector(
"atom-text-editor[data-grammar='text plain null-grammar']"
)
expect(plainEditor.getModel().getText()).toBe(`\
function f(x) {
return x++;
}\
`)
})
})
describe('when an editor cannot find the grammar that is later loaded', function () {
it('updates the editor grammar', function () {
let renderSpy = null
if (typeof atom.grammars.onDidRemoveGrammar !== 'function') {
// TODO: Remove once atom.grammars.onDidRemoveGrammar is released
waitsForPromise(() => atom.packages.activatePackage('language-gfm'))
}
runs(
() => (renderSpy = spyOn(preview, 'renderMarkdown').andCallThrough())
)
waitsForPromise(() => atom.packages.deactivatePackage('language-ruby'))
waitsFor(
'renderMarkdown to be called after disabling a language',
() => renderSpy.callCount === 1
)
runs(function () {
const rubyEditor = preview.element.querySelector(
"atom-text-editor[data-grammar='source ruby']"
)
expect(rubyEditor).toBeNull()
})
waitsForPromise(() => atom.packages.activatePackage('language-ruby'))
waitsFor(
'renderMarkdown to be called after enabling a language',
() => renderSpy.callCount === 2
)
runs(function () {
const rubyEditor = preview.element.querySelector(
"atom-text-editor[data-grammar='source ruby']"
)
expect(rubyEditor.getModel().getText()).toBe(`\
def func
x = 1
end\
`)
})
})
})
})
describe('image resolving', function () {
beforeEach(function () {
waitsForPromise(() => preview.renderMarkdown())
})
describe('when the image uses a relative path', function () {
it('resolves to a path relative to the file', function () {
const image = preview.element.querySelector('img[alt=Image1]')
expect(image.getAttribute('src')).toBe(
atom.project.getDirectories()[0].resolve('subdir/image1.png')
)
})
})
describe('when the image uses an absolute path that does not exist', function () {
it('resolves to a path relative to the project root', function () {
const image = preview.element.querySelector('img[alt=Image2]')
expect(image.src).toMatch(
url.parse(atom.project.getDirectories()[0].resolve('tmp/image2.png'))
)
})
})
describe('when the image uses an absolute path that exists', function () {
it("doesn't change the URL when allowUnsafeProtocols is true", function () {
preview.destroy()
atom.config.set('markdown-preview.allowUnsafeProtocols', true)
const filePath = path.join(temp.mkdirSync('atom'), 'foo.md')
fs.writeFileSync(filePath, `![absolute](${filePath})`)
preview = new MarkdownPreviewView({ filePath })
jasmine.attachToDOM(preview.element)
waitsForPromise(() => preview.renderMarkdown())
runs(() =>
expect(
preview.element.querySelector('img[alt=absolute]').src
).toMatch(url.parse(filePath))
)
})
})
it('removes the URL when allowUnsafeProtocols is false', function () {
preview.destroy()
atom.config.set('markdown-preview.allowUnsafeProtocols', false)
const filePath = path.join(temp.mkdirSync('atom'), 'foo.md')
fs.writeFileSync(filePath, `![absolute](${filePath})`)
preview = new MarkdownPreviewView({ filePath })
jasmine.attachToDOM(preview.element)
waitsForPromise(() => preview.renderMarkdown())
runs(() =>
expect(preview.element.querySelector('img[alt=absolute]').src).toMatch(
''
)
)
})
describe('when the image uses a web URL', function () {
it("doesn't change the URL", function () {
const image = preview.element.querySelector('img[alt=Image3]')
expect(image.src).toBe('http://github.com/image3.png')
})
})
})
describe('gfm newlines', function () {
describe('when gfm newlines are not enabled', function () {
it('creates a single paragraph with <br>', function () {
atom.config.set('markdown-preview.breakOnSingleNewline', false)
waitsForPromise(() => preview.renderMarkdown())
runs(() =>
expect(
preview.element.querySelectorAll('p:last-child br').length
).toBe(0)
)
})
})
describe('when gfm newlines are enabled', function () {
it('creates a single paragraph with no <br>', function () {
atom.config.set('markdown-preview.breakOnSingleNewline', true)
waitsForPromise(() => preview.renderMarkdown())
runs(() =>
expect(
preview.element.querySelectorAll('p:last-child br').length
).toBe(1)
)
})
})
})
describe('yaml front matter', function () {
it('creates a table with the YAML variables', function () {
atom.config.set('markdown-preview.breakOnSingleNewline', true)
waitsForPromise(() => preview.renderMarkdown())
runs(() => {
expect(
[...preview.element.querySelectorAll('table th')].map(
el => el.textContent
)
).toEqual(['variable1', 'array'])
expect(
[...preview.element.querySelectorAll('table td')].map(
el => el.textContent
)
).toEqual(['value1', 'foo,bar'])
})
})
})
describe('text selections', function () {
it('adds the `has-selection` class to the preview depending on if there is a text selection', function () {
expect(preview.element.classList.contains('has-selection')).toBe(false)
const selection = window.getSelection()
selection.removeAllRanges()
selection.selectAllChildren(document.querySelector('atom-text-editor'))
waitsFor(
() => preview.element.classList.contains('has-selection') === true
)
runs(() => selection.removeAllRanges())
waitsFor(
() => preview.element.classList.contains('has-selection') === false
)
})
})
describe('when core:save-as is triggered', function () {
beforeEach(function () {
preview.destroy()
const filePath = atom.project
.getDirectories()[0]
.resolve('subdir/code-block.md')
preview = new MarkdownPreviewView({ filePath })
// Add to workspace for core:save-as command to be propagated up to the workspace
waitsForPromise(() => atom.workspace.open(preview))
runs(() => jasmine.attachToDOM(atom.views.getView(atom.workspace)))
})
it('saves the rendered HTML and opens it', function () {
const outputPath = fs.realpathSync(temp.mkdirSync()) + 'output.html'
const createRule = (selector, css) => ({
selectorText: selector,
cssText: `${selector} ${css}`
})
const markdownPreviewStyles = [
{
rules: [createRule('.markdown-preview', '{ color: orange; }')]
},
{
rules: [
createRule('.not-included', '{ color: green; }'),
createRule('.markdown-preview :host', '{ color: purple; }')
]
}
]
const atomTextEditorStyles = [
'atom-text-editor .line { color: brown; }\natom-text-editor .number { color: cyan; }',
'atom-text-editor :host .something { color: black; }',
'atom-text-editor .hr { background: url(atom://markdown-preview/assets/hr.png); }'
]
waitsForPromise(() => preview.renderMarkdown())
runs(() => {
expect(fs.isFileSync(outputPath)).toBe(false)
spyOn(preview, 'getSaveDialogOptions').andReturn({
defaultPath: outputPath
})
spyOn(atom.applicationDelegate, 'showSaveDialog').andCallFake(function (
options,
callback
) {
if (typeof callback === 'function') {
callback(options.defaultPath)
}
// TODO: When https://github.com/atom/atom/pull/16245 lands remove the return
// and the existence check on the callback
return options.defaultPath
})
spyOn(preview, 'getDocumentStyleSheets').andReturn(
markdownPreviewStyles
)
spyOn(preview, 'getTextEditorStyles').andReturn(atomTextEditorStyles)
})
waitsForPromise(() =>
atom.commands.dispatch(preview.element, 'core:save-as')
)
waitsFor(() => {
const activeEditor = atom.workspace.getActiveTextEditor()
return activeEditor && activeEditor.getPath() === outputPath
})
runs(() => {
const element = document.createElement('div')
element.innerHTML = fs.readFileSync(outputPath)
expect(element.querySelector('h1').innerText).toBe('Code Block')
expect(
element.querySelector(
'.line .syntax--source.syntax--js .syntax--constant.syntax--numeric'
).innerText
).toBe('3')
expect(
element.querySelector(
'.line .syntax--source.syntax--js .syntax--keyword.syntax--control'
).innerText
).toBe('if')
expect(
element.querySelector(
'.line .syntax--source.syntax--js .syntax--constant.syntax--numeric'
).innerText
).toBe('3')
})
})
describe('text editor style extraction', function () {
let [extractedStyles] = []
const textEditorStyle = '.editor-style .extraction-test { color: blue; }'
const unrelatedStyle = '.something else { color: red; }'
beforeEach(function () {
atom.styles.addStyleSheet(textEditorStyle, {
context: 'atom-text-editor'
})
atom.styles.addStyleSheet(unrelatedStyle, {
context: 'unrelated-context'
})
return (extractedStyles = preview.getTextEditorStyles())
})
it('returns an array containing atom-text-editor css style strings', function () {
expect(extractedStyles.indexOf(textEditorStyle)).toBeGreaterThan(-1)
})
it('does not return other styles', function () {
expect(extractedStyles.indexOf(unrelatedStyle)).toBe(-1)
})
})
})
describe('when core:copy is triggered', function () {
beforeEach(function () {
preview.destroy()
preview.element.remove()
const filePath = atom.project
.getDirectories()[0]
.resolve('subdir/code-block.md')
preview = new MarkdownPreviewView({ filePath })
jasmine.attachToDOM(preview.element)
waitsForPromise(() => preview.renderMarkdown())
})
describe('when there is no text selected', function () {
it('copies the rendered HTML of the entire Markdown document to the clipboard', function () {
expect(atom.clipboard.read()).toBe('initial clipboard content')
waitsForPromise(() =>
atom.commands.dispatch(preview.element, 'core:copy')
)
runs(() => {
const element = document.createElement('div')
element.innerHTML = atom.clipboard.read()
expect(element.querySelector('h1').innerText).toBe('Code Block')
expect(
element.querySelector(
'.line .syntax--source.syntax--js .syntax--constant.syntax--numeric'
).innerText
).toBe('3')
expect(
element.querySelector(
'.line .syntax--source.syntax--js .syntax--keyword.syntax--control'
).innerText
).toBe('if')
expect(
element.querySelector(
'.line .syntax--source.syntax--js .syntax--constant.syntax--numeric'
).innerText
).toBe('3')
})
})
})
describe('when there is a text selection', function () {
it('directly copies the selection to the clipboard', function () {
const selection = window.getSelection()
selection.removeAllRanges()
const range = document.createRange()
range.setStart(document.querySelector('atom-text-editor'), 0)
range.setEnd(document.querySelector('p').firstChild, 3)
selection.addRange(range)
atom.commands.dispatch(preview.element, 'core:copy')
const clipboardText = atom.clipboard.read()
expect(clipboardText).toBe(`\
if a === 3 {
b = 5
}
enc\
`)
})
})
})
describe('when markdown-preview:select-all is triggered', function () {
it('selects the entire Markdown preview', function () {
const filePath = atom.project
.getDirectories()[0]
.resolve('subdir/code-block.md')
const preview2 = new MarkdownPreviewView({ filePath })
jasmine.attachToDOM(preview2.element)
waitsForPromise(() => preview.renderMarkdown())
runs(function () {
atom.commands.dispatch(preview.element, 'markdown-preview:select-all')
const { commonAncestorContainer } = window.getSelection().getRangeAt(0)
expect(commonAncestorContainer).toEqual(preview.element)
})
waitsForPromise(() => preview2.renderMarkdown())
runs(() => {
atom.commands.dispatch(preview2.element, 'markdown-preview:select-all')
const selection = window.getSelection()
expect(selection.rangeCount).toBe(1)
const { commonAncestorContainer } = selection.getRangeAt(0)
expect(commonAncestorContainer).toEqual(preview2.element)
})
})
})
describe('when markdown-preview:zoom-in or markdown-preview:zoom-out are triggered', function () {
it('increases or decreases the zoom level of the markdown preview element', function () {
jasmine.attachToDOM(preview.element)
waitsForPromise(() => preview.renderMarkdown())
runs(function () {
const originalZoomLevel = getComputedStyle(preview.element).zoom
atom.commands.dispatch(preview.element, 'markdown-preview:zoom-in')
expect(getComputedStyle(preview.element).zoom).toBeGreaterThan(
originalZoomLevel
)
atom.commands.dispatch(preview.element, 'markdown-preview:zoom-out')
expect(getComputedStyle(preview.element).zoom).toBe(originalZoomLevel)
})
})
})
})

156
styles/markdown-preview-default.less

@ -0,0 +1,156 @@
// Default Markdown Preview styles
// These are the default Markdown Preview styles.
// They use the syntax-variables to adapt to the color scheme of syntax themes.
@import "syntax-variables";
.markdown-preview:not([data-use-github-style]) {
@fg: @syntax-text-color;
@bg: @syntax-background-color;
@fg-accent: @syntax-cursor-color;
@fg-strong: contrast(@bg, darken(@fg, 32%), lighten(@fg, 32%));
@fg-subtle: contrast(@fg, lighten(@fg, 16%), darken(@fg, 16%));
@border: contrast(@bg, lighten(@bg, 16%), darken(@bg, 16%));
@margin: 1.5em;
padding: 2em;
font-size: 1.2em;
color: @fg;
background-color: @bg;
overflow: auto;
& > :first-child {
margin-top: 0;
}
// Headings --------------------
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
margin-top: @margin;
margin-bottom: @margin/3;
color: @fg-strong;
}
h1 { font-size: 2.4em; font-weight: 300; }
h2 { font-size: 1.8em; font-weight: 400; }
h3 { font-size: 1.5em; font-weight: 500; }
h4 { font-size: 1.2em; font-weight: 600; }
h5 { font-size: 1.1em; font-weight: 600; }
h6 { font-size: 1.0em; font-weight: 600; }
// Emphasis --------------------
strong {
color: @fg-strong;
}
del {
color: @fg-subtle;
}
// Link --------------------
a,
a code {
color: @fg-accent;
}
// Images --------------------
img {
max-width: 100%;
}
// Paragraph --------------------
& > p {
margin-top: 0;
margin-bottom: @margin;
}
// List --------------------
& > ul,
& > ol {
margin-bottom: @margin;
}
// Blockquotes --------------------
blockquote {
margin: @margin 0;
font-size: inherit;
color: @fg-subtle;
border-color: @border;
border-width: 4px;
}
// HR --------------------
hr {
margin: @margin*2 0;
border-top: 2px dashed @border;
background: none;
}
// Table --------------------
table {
margin: @margin 0;
}
th {
color: @fg-strong;
}
th,
td {
padding: .66em 1em;
border: 1px solid @border;
}
// Code --------------------
code {
color: @fg-strong;
background-color: contrast(@syntax-background-color, lighten(@syntax-background-color, 8%), darken(@syntax-background-color, 6%));
}
atom-text-editor {
margin: @margin 0;
padding: 1em;
font-size: .92em;
border-radius: 3px;
background-color: contrast(@syntax-background-color, lighten(@syntax-background-color, 4%), darken(@syntax-background-color, 4%));
}
// KBD --------------------
kbd {
color: @fg-strong;
border: 1px solid @border;
border-bottom: 2px solid darken(@border, 6%);
background-color: contrast(@syntax-background-color, lighten(@syntax-background-color, 8%), darken(@syntax-background-color, 6%));
}
}

40
styles/markdown-preview-github.less

@ -0,0 +1,40 @@
// GitHub.com styles
// These are the GitHub Flavored Markdown styles also found on github.com.
// They can be anabled in the markdown-preview settings by turning on "Use GitHub.com styles".
@import (reference) "../assets/primer-markdown";
.markdown-preview[data-use-github-style] {
// Includes GitHub.com styles from `../assets/primer-markdown.less`.
// Source: https://github.com/primer/primer/tree/master/modules/primer-markdown
.markdown-body();
// The styles below override/complement the GitHub.com styles
// It's needed because some markup or global styles are different
padding: 30px;
font-size: 16px;
color: #333;
background-color: #fff;
overflow: scroll;
a {
color: #337ab7;
}
code {
color: inherit;
}
atom-text-editor {
padding: .8em 1em;
margin-bottom: 1em;
font-size: .85em;
border-radius: 4px;
overflow: auto;
}
}

129
styles/markdown-preview.less

@ -0,0 +1,129 @@
// Global Markdown Preview styles
.markdown-preview {
atom-text-editor {
// only show scrollbars on hover
.scrollbars-visible-always & {
.vertical-scrollbar,
.horizontal-scrollbar {
visibility: hidden;
}
}
.scrollbars-visible-always &:hover {
.vertical-scrollbar,
.horizontal-scrollbar {
visibility: visible;
}
}
user-select: auto;
}
// move task list checkboxes
.task-list-item input[type=checkbox] {
position: absolute;
margin: .25em 0 0 -1.4em;
}
.task-list-item {
list-style-type: none;
}
}
.markdown-spinner {
margin: auto;
background-image: url(images/octocat-spinner-128.gif);
background-repeat: no-repeat;
background-size: 64px;
background-position: top center;
padding-top: 70px;
text-align: center;
}
// style the background color of the tree view
.tree-view {
// background-color: whitesmoke;
}
// style the background and foreground colors on the atom-text-editor-element itself
atom-text-editor {
// color: white;
// background-color: hsl(180, 24%, 12%);
}
// To style other content in the text editor's shadow DOM, use the ::shadow expression
atom-text-editor::shadow .cursor {
// border-color: red;
}
.markdown-preview-custom {
// please write your custom style here
// eg:
// color: blue; // change font color
// font-size: 14px; // change font size
//
// custom pdf output style
h1 {
counter-increment: h1counter;
counter-reset: h2counter;
}
h2 {
counter-increment: h2counter;
counter-reset: h3counter;
}
h3 {
counter-increment: h3counter;
counter-reset: h4counter;
}
h4 {
counter-increment: h4counter;
counter-reset: h5counter;
}
h1:before {
content: counter(h1counter) ". ";
}
h2:before {
content: counter(h1counter) "." counter(h2counter) ". ";
}
h3:before {
content: counter(h1counter) "." counter(h2counter) "." counter(h3counter) ". ";
}
h4:before {
content: counter(h1counter) "." counter(h2counter) "." counter(h3counter) "." counter(h4counter) ". ";
}
@media print {
}
// custom phantomjs png/jpeg export style
&.phantomjs-image {
}
//custom phantomjs pdf export style
&.phantomjs-pdf {
}
// custom presentation style
.preview-slides .slide,
&[data-presentation-mode] {
// eg
// background-color: #000;
}
}
.markdown-preview {
.markdown-preview-custom() !important;
}
Loading…
Cancel
Save