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>