Skip to content

Commit

Permalink
Merge pull request #20 from SilverStripers/feature/editor-preview
Browse files Browse the repository at this point in the history
Feature/editor preview
  • Loading branch information
fonsekaean committed Nov 2, 2017
2 parents 6580109 + 3dae8c8 commit 90dd9a6
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 36 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,39 @@ SilverStripe\Core\Injector\Injector:
class: SilverStripers\markdown\db\MarkdownText
```

## Add preview styles

You can add your own CSS styles to the editor previews. This would let the users to check how their content will be displayed before they save in.

To achived this create a css file in `mysite/css/` and name it as `editor.css`.

Your CSS rules have to be nested in a class so it wont affect other areas of the CMS.

```
.markdown-preview {
background-color: white;
padding: 20px;
font-size: 20px;
}
.markdown-preview h1 {
font-size: 30px;
}
````

If you are using a separate config and wanting to add styles to that EditorConfig you just add a new class name. This is possible because the fields adds
the EditorConfig's identifier on to the preview pane. The below is an example for the default configs.

```
.markdown-preview.default {
background-color: white;
padding: 20px;
font-size: 14px;
line-height: 20px;
}
.markdown-preview.default h1 {
font-size: 24px;
line-height: 30px;
}
````
23 changes: 12 additions & 11 deletions client/dist/bundle.min.js

Large diffs are not rendered by default.

39 changes: 30 additions & 9 deletions client/src/components/MarkdownEditorField/MarkdownEditorField.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@ const SimpleMDE = require('simplemde');
import ReactSimpleMDE from 'react-simplemde-editor';
import { provideInjector } from 'lib/Injector';
import jQuery from 'jquery';

import ShortcodeParser from '../ShortCodeParser/ShortcodeParser';
const parser = new ShortcodeParser();

var ss = typeof window.ss !== 'undefined' ? window.ss : {};
if(typeof ss.markdownConfigs == 'undefined') {
ss.markdownConfigs = {};
}


ss.markdownConfigs.readToolbarConfigs = function(feed) {
let data = JSON.parse(feed);
ss.markdownConfigs.readToolbarConfigs = function(data) {
let toolbar = [];

for (var key in data) {
var element = data[key];
if(typeof element == 'string') {
Expand Down Expand Up @@ -48,6 +47,14 @@ ss.markdownConfigs.readToolbarConfigs = function(feed) {
return toolbar;
}

parser.registerShortCode('image_link', function(buffer, opts) {
return opts.url;
});


parser.registerShortCode('embed', function(buffer, opts) {
return '<img src="' + opts.thumbnail + '" width="' + opts.width + '" height="' + opts.height + '">';
});


class MarkdownEditorField extends React.Component {
Expand All @@ -60,20 +67,33 @@ class MarkdownEditorField extends React.Component {
this.props.textarea.value = value;
}

previewRender(plainText, preview){
preview.classList.add('markdown-preview');
preview.classList.add(this.identifier);
let parsedText = parser.parse(plainText);
return this.parent.markdown(parsedText);
}

static addCustomAction(key, action) {
ss.markdownConfigs[key] = action;
};

static registerShortCodes(key, callback) {

}

render() {
return (<div className="editor-container">
<ReactSimpleMDE
value = {this.props.textarea.value}
onChange={this.handleChange.bind(this)}
options={{
spellChecker: true,
dragDrop: false,
keyMap: "sublime",
toolbar: this.props.toolbar
dragDrop : false,
keyMap : "sublime",
toolbar : this.props.toolbar,
previewRender: this.previewRender,
identifier : this.props.identifier
}}
></ReactSimpleMDE>
</div>);
Expand Down Expand Up @@ -130,10 +150,11 @@ jQuery.entwine('ss', ($) => {
},
refresh() {
let textArea = $(this).parent().find('textarea')[0];
let toolbar = ss.markdownConfigs.readToolbarConfigs(textArea.dataset.config);
let data = JSON.parse(textArea.dataset.config);
let toolbar = ss.markdownConfigs.readToolbarConfigs(data.toolbar);

ReactDOM.render(
<MarkdownEditorField textarea={textArea} toolbar={toolbar}></MarkdownEditorField>,
<MarkdownEditorField textarea={textArea} toolbar={toolbar} identifier={data.identifier}></MarkdownEditorField>,
this[0]
);
}
Expand Down
136 changes: 136 additions & 0 deletions client/src/components/ShortCodeParser/ShortcodeParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use strict';


var util = require('util');

const SHORTCODE_ATTRS = /(\s+([a-z0-9\-_]+|([a-z0-9\-_]+)\s*=\s*([a-z0-9\-_]+|\d+\.\d+|'[^']*'|"[^"]*")))*/.toString().slice(1,-1);
const SHORTCODE_SLASH = /\s*\/?\s*/.toString().slice(1,-1);
const SHORTCODE_OPEN = /\[\s*%s/.toString().slice(1,-1);
const SHORTCODE_RIGHT_BRACKET = '\\]';
const SHORTCODE_CLOSE = /\[\s*\/\s*%s\s*\]/.toString().slice(1,-1);
const SHORTCODE_CONTENT = /(.|\n|)*?/.toString().slice(1,-1);
const SHORTCODE_SPACE = /\s*/.toString().slice(1,-1);


class ShortcodeParser {

construct() {
this.shortCodes = {};
}

registerShortCode(key, callback) {
this.shortCodes[key] = callback;
}

typecast(val) {
val = val.trim().replace(/(^['"]|['"]$)/g, '');
if (/^\d+$/.test(val)) {
return parseInt(val, 10);
} else if (/^\d+\.\d+$/.test(val)) {
return parseFloat(val);
} else if (/^(true|false)$/.test(val)) {
return (val === 'true');
} else if (/^undefined$/.test(val)) {
return undefined;
} else if (/^null$/i.test(val)) {
return null;
} else {
return val;
}
}

closeTagString(name) {
return /^[^a-z0-9]/.test(name) ? util.format('[%s]?%s', name[0].replace('$', '\\$'), name.slice(1)) : name;
}


parseShortcode(name, buf, inline) {
var regex, match, data = {}, attr = {};

if (inline) {
regex = new RegExp('^' + util.format(SHORTCODE_OPEN, name)
+ SHORTCODE_ATTRS
+ SHORTCODE_SPACE
+ SHORTCODE_SLASH
+ SHORTCODE_RIGHT_BRACKET, 'i');
} else {
regex = new RegExp('^' + util.format(SHORTCODE_OPEN, name)
+ SHORTCODE_ATTRS
+ SHORTCODE_SPACE
+ SHORTCODE_RIGHT_BRACKET, 'i');
}

while ((match = buf.match(regex)) !== null) {
var key = match[3] || match[2];
var val = match[4] || match[3];
var pattern = match[1];
if (pattern) {
var idx = buf.lastIndexOf(pattern);
attr[key] = (val !== undefined) ? this.typecast(val) : true;
buf = buf.slice(0, idx) + buf.slice(idx + pattern.length);
} else {
break;
}
}

attr = Object.keys(attr).reverse().reduce(function(prev, current) {
prev[current] = attr[current]; return prev;
}, {});

buf = buf.replace(regex, '').replace(new RegExp(util.format(SHORTCODE_CLOSE, this.closeTagString(name))), '');

return {
attr: attr,
content: inline ? buf : buf.replace(/(^\n|\n$)/g, '')
}

}

parse(plainText) {

for (var name in this.shortCodes) {
var regex = {
wrapper: new RegExp(util.format(SHORTCODE_OPEN, name)
+ SHORTCODE_ATTRS
+ SHORTCODE_RIGHT_BRACKET
+ SHORTCODE_CONTENT
+ util.format(SHORTCODE_CLOSE, this.closeTagString(name)), 'gi'),
inline: new RegExp(util.format(SHORTCODE_OPEN, name)
+ SHORTCODE_ATTRS
+ SHORTCODE_SLASH
+ SHORTCODE_RIGHT_BRACKET, 'gi')
}


let matches = plainText.match(regex.wrapper);

if (matches) {
for (let m,data,i=0,len=matches.length; i < len; i++) {
m = matches[i];
data = this.parseShortcode(name, m);
plainText = plainText.replace(m, this.shortCodes[name].call(null, data.content, data.attr));
}
}

matches = plainText.match(regex.inline);
if (matches) {
let m = null;
while((m = matches.shift()) !== undefined) {
let data = this.parseShortcode(name, m, true);
plainText = plainText.replace(m, this.shortCodes[name].call(null, data.content, data.attr));
}

}


}

return plainText;
}

}


ShortcodeParser.prototype.shortCodes = {};

export default ShortcodeParser;
3 changes: 1 addition & 2 deletions client/src/entwine/Markdown_ssmedia.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,9 @@ jQuery.entwine('ss', ($) => {
}
const data = this.getData();
const extraData = this.getExtraData();

let markdown = '!['
+ (extraData.CaptionText ? extraData.CaptionText : data.title)
+ ']([image_link id=' + data.ID +' width=' + data.width + ' height=' + data.height + '] "'
+ ']([image_link id=' + data.ID +' width=' + data.InsertWidth + ' height=' + data.InsertHeight + ' url=\''+ data.url +'\'] "'
+ data.title
+ '")';

Expand Down
4 changes: 1 addition & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-simplemde-editor": "3.6.11",
"bootstrap": "4.0.0-beta"
"bootstrap": "4.0.0-beta",
"util": "0.10.3"
},
"devDependencies": {
"@silverstripe/webpack-config": "0.3.0",
Expand Down
6 changes: 4 additions & 2 deletions src/db/MarkdownText.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ public function ParseMarkdown($bCache = true, $strValue = '')

// shortcodes
$regexes = [
'/\[image_link*\s[a-z|A-Z|0-9\s\=]*\]/',
'/\[file_link\,[a-z|A-Z|0-9\s\=]*\]/'
'/\[image_link(.+?)\]/',
'/\[file_link(.+?)\]/'
];

foreach ($regexes as $pattern) {
Expand All @@ -79,6 +79,8 @@ public function ParseMarkdown($bCache = true, $strValue = '')
}
}



$parseDown = new GithubMarkdown();
$parsed = $parseDown->parse($parsed);

Expand Down
Loading

0 comments on commit 90dd9a6

Please sign in to comment.