Skip to content

Commit

Permalink
Merge pull request #168 from fjcalzado/feature/#156_Real_time_preview…
Browse files Browse the repository at this point in the history
…_markdown

Feature/#156 real time preview markdown
  • Loading branch information
brauliodiez authored Mar 9, 2018
2 parents dc21945 + 393597e commit 9c782ef
Show file tree
Hide file tree
Showing 24 changed files with 1,016 additions and 111 deletions.
3 changes: 2 additions & 1 deletion config/webpack/app/webpack.config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ module.exports = merge(common, {
vendor: [
'core-js',
'lc-form-validation',
'marksy',
'markdown-it',
'highlight.js',
'moment',
'react',
'react-addons-shallow-compare',
Expand Down
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
"@types/core-js": "^0.9.41",
"@types/deep-freeze": "0.0.29",
"@types/enzyme": "^2.5.38",
"@types/highlight.js": "^9.1.10",
"@types/history": "^3.2.1",
"@types/karma-chai-sinon": "^0.1.5",
"@types/markdown-it": "0.0.3",
"@types/mocha": "^2.2.33",
"@types/node": "^6.0.45",
"@types/react": "^15.0.33",
Expand Down Expand Up @@ -102,9 +104,19 @@
"core-js": "^2.4.1",
"express": "^4.15.3",
"font-awesome": "^4.7.0",
"highlight.js": "^9.12.0",
"jquery": "^3.1.1",
"lc-form-validation": "^1.0.0",
"marksy": "^0.3.0",
"lodash.debounce": "^4.0.8",
"markdown-it": "^8.3.2",
"markdown-it-abbr": "^1.0.4",
"markdown-it-checkbox": "^1.1.0",
"markdown-it-emoji": "^1.4.0",
"markdown-it-footnote": "^3.0.1",
"markdown-it-ins": "^2.0.0",
"markdown-it-mark": "^2.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"moment": "^2.18.1",
"react": "^15.4.1",
"react-addons-shallow-compare": "^15.4.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.droppable {
margin-top: 5px;
margin-bottom: 15px;
border: 2px dashed #ccc;
height: 200px;
display: flex;
Expand Down
7 changes: 2 additions & 5 deletions src/common/components/markdownViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
import { MarkDownViewerComponent } from './markdownViewerComponent';

export {
MarkDownViewerComponent
};
export { MarkDownViewerComponent } from './markdownViewerComponent';
export { mapOffsetToLine, mapLineToOffset, SOURCE_LINE_CLASSNAME } from './syncScroll';
63 changes: 42 additions & 21 deletions src/common/components/markdownViewer/markdownViewerComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
import * as React from 'react';
import { marksy } from 'marksy';
import * as ReactDOM from 'react-dom';
import { withRouter } from 'react-router';
import { CreateMarkdownRender, Mdr } from './render';

const compile = marksy({
// TODO: extract into a new <DynamicLink /> that analyze "href" and render a <Link /> or <a />
a: ({ target, children, ...other }) => (
<a target="_self" {...other}>{children}</a>
),
});
/**
* TODO:
* Custom rule to analyze "href" and render a <Link /> (react router) or <a />
*/

export interface MarkDownViewerComponentProps {
interface Props {
content: string;
registerRef?: (ref: HTMLElement) => void;
className?: string;
location?: any; // Router HOC injected.
}

const getMarkDownChildren = (content: string): React.ReactNode => {
let childrenComponent: React.ReactNode = null;
interface State {
mdr: Mdr;
}

class MarkDownViewer extends React.Component<Props, State> {
constructor(props) {
super(props);

if (content) {
childrenComponent = compile(content).tree;
this.state = {
mdr: CreateMarkdownRender({routerLocation: props.location ? props.location.pathname : ''}),
};
}

return childrenComponent;
};
private markdownToHTML = () => {
return {
__html: this.state.mdr.render(this.props.content || ''),
};
}

export const MarkDownViewerComponent: React.StatelessComponent<MarkDownViewerComponentProps> = ({ content }) => {
return (
<div>
{getMarkDownChildren(content)}
</div>
);
public render() {
const {className = ''} = this.props;
return(
<div className={className} ref={this.props.registerRef || (() => {})}
dangerouslySetInnerHTML={this.markdownToHTML()} // See Footnote [1].
/>
);
}
};

MarkDownViewerComponent.displayName = 'MarkDownViewerComponent';
const MarkDownViewerComponent = withRouter(MarkDownViewer);
export { MarkDownViewerComponent, Props as MarkDownViewerComponentProps }

// [1] WARNING: This conversion from plain HTML to JSX with
// dangerouslySetInnerHTML could be unsafe (script injection, XSS)
// depending whether markdown engine blocks malicious code or not.
// Markdonw-it is supposed to be XSS safe, but if you plan to
// change engine, ensure safety first!
3 changes: 3 additions & 0 deletions src/common/components/markdownViewer/render/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { CreateMarkdownRender, Mdr, MdrFactory, MdrSetup } from './markdownRender';
export { MdrOptions } from './markdownRenderOptions';
export { MdrCodeStyle } from './markdownRenderCodeStyle';
50 changes: 50 additions & 0 deletions src/common/components/markdownViewer/render/markdownRender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { MarkdownIt as Mdr } from 'markdown-it';
import * as MdrObject from 'markdown-it';
import * as mdrPluginAbbr from 'markdown-it-abbr';
import * as mdrPluginEmoji from 'markdown-it-emoji';
import * as mdrPluginFootnote from 'markdown-it-footnote';
import * as mdrPluginIns from 'markdown-it-ins';
import * as mdrPluginMark from 'markdown-it-mark';
import * as mdrPluginSub from 'markdown-it-sub';
import * as mdrPluginSup from 'markdown-it-sup';
import * as mdrPluginCheckbox from 'markdown-it-checkbox';
import { loadCustomRules } from './markdownRenderRules';
import { MdrOptions, defaultOptions, codeHighlighter } from './markdownRenderOptions';
import { MdrCodeStyle, loadCodeStyle, defaultCodeStyle } from './markdownRenderCodeStyle';

interface MdrSetup {
routerLocation: string;
options?: MdrOptions;
codeStyle?: MdrCodeStyle;
}

type MdrFactory = (setupParams: MdrSetup) => Mdr;

// Markdown Render Factory. Create render instance and set it up.
const CreateMarkdownRender: MdrFactory = ({
routerLocation,
options = defaultOptions,
codeStyle = defaultCodeStyle}) => {
// Create instance with options+highlighter & load plugins.
const mdr = new MdrObject({
...options,
highlight: (str, lang) => codeHighlighter(mdr, str, lang),
}).use(mdrPluginAbbr)
.use(mdrPluginEmoji)
.use(mdrPluginFootnote)
.use(mdrPluginIns)
.use(mdrPluginMark)
.use(mdrPluginSub)
.use(mdrPluginSup)
.use(mdrPluginCheckbox);

// Load custom rules.
loadCustomRules(mdr, routerLocation);

// Load code highlight style.
loadCodeStyle(codeStyle);

return mdr;
};

export { CreateMarkdownRender, MdrFactory, MdrSetup, Mdr }
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// List of available code styles.
enum MdrCodeStyle {
agate = 'agate',
androidstudio = 'androidstudio',
arduinoLight = 'arduino-light',
arta = 'arta',
ascetic = 'ascetic',
atelierCaveDark = 'atelier-cave-dark',
atelierCaveLight = 'atelier-cave-light',
atelierDuneDark = 'atelier-dune-dark',
atelierDuneLight = 'atelier-dune-light',
atelierEstuaryDark = 'atelier-estuary-dark',
atelierEstuaryLight = 'atelier-estuary-light',
atelierForestDark = 'atelier-forest-dark',
atelierForestLight = 'atelier-forest-light',
atelierHeathDark = 'atelier-heath-dark',
atelierHeathLight = 'atelier-heath-light',
atelierLakesideDark = 'atelier-lakeside-dark',
atelierLakesideLight = 'atelier-lakeside-light',
atelierPlateauDark = 'atelier-plateau-dark',
atelierPlateauLight = 'atelier-plateau-light',
atelierSavannaDark = 'atelier-savanna-dark',
atelierSavannaLight = 'atelier-savanna-light',
atelierSeasideDark = 'atelier-seaside-dark',
atelierSeasideLight = 'atelier-seaside-light',
atelierSulphurpoolDark = 'atelier-sulphurpool-dark',
atelierSulphurpoolLight = 'atelier-sulphurpool-light',
atomOneDark = 'atom-one-dark',
atomOneLight = 'atom-one-light',
brownPaper = 'brown-paper',
codepenEmbed = 'codepen-embed',
colorBrewer = 'color-brewer',
darcula = 'darcula',
dark = 'dark',
darkula = 'darkula',
default = 'default',
docco = 'docco',
dracula = 'dracula',
far = 'far',
foundation = 'foundation',
githubGist = 'github-gist',
github = 'github',
googlecode = 'googlecode',
grayscale = 'grayscale',
gruvboxDark = 'gruvbox-dark',
gruvboxLight = 'gruvbox-light',
hopscotch = 'hopscotch',
hybrid = 'hybrid',
idea = 'idea',
irBlack = 'ir-black',
kimbieDark = 'kimbie.dark',
kimbieLight = 'kimbie.light',
magula = 'magula',
monoBlue = 'mono-blue',
monokaiSublime = 'monokai-sublime',
monokai = 'monokai',
obsidian = 'obsidian',
ocean = 'ocean',
paraisoDark = 'paraiso-dark',
paraisoLight = 'paraiso-light',
pojoaque = 'pojoaque',
purebasic = 'purebasic',
qtcreator_dark = 'qtcreator_dark',
qtcreator_light = 'qtcreator_light',
railscasts = 'railscasts',
rainbow = 'rainbow',
routeros = 'routeros',
schoolBook = 'school-book',
solarizedDark = 'solarized-dark',
solarizedLight = 'solarized-light',
sunburst = 'sunburst',
tomorrowNightNlue = 'tomorrow-night-blue',
tomorrowNightNright = 'tomorrow-night-bright',
tomorrowNightEighties = 'tomorrow-night-eighties',
tomorrowNight = 'tomorrow-night',
tomorrow = 'tomorrow',
vs = 'vs',
vs2015 = 'vs2015',
xcode = 'xcode',
xt256 = 'xt256',
zenburn = 'zenburn',
}

// Change here code style by default.
const defaultCodeStyle = MdrCodeStyle.atomOneLight;

// Code style dynamic loader.
const loadCodeStyle = (style: MdrCodeStyle) => {
return require(`highlight.js/styles/${style}.css`);
};

export { MdrCodeStyle, loadCodeStyle, defaultCodeStyle }
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { MarkdownIt as Mdr, Options as MdrOptions } from 'markdown-it';
import highlightjs from 'highlight.js/lib/highlight';

// Markdown Render options by default.
const defaultOptions: MdrOptions = {
html: false, // Enable HTML tags in source. This could be unsafe if enabled (XSS).
xhtmlOut: false, // Use '/' to close single tags (<br />)
breaks: false, // Convert '\n' in paragraphs into <br>
langPrefix: 'language-', // CSS language prefix for fenced blocks
linkify: true, // autoconvert URL-like texts to links
// Enable some language-neutral replacements + quotes beautification
typographer: true,

// Double + single quotes replacement pairs, when typographer enabled,
// and smartquotes on. Could be either a String or an Array.
//
// For example, you can use '«»„“' for Russian, '„“‚‘' for German,
// and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp).
quotes: '\u201c\u201d\u2018\u2019', /* “”‘’ */
};

// Markdown Render code highlighter.
const codeHighlighter = (mdr: Mdr, str, lang) => {
const highlighter = () => {
if (lang && highlightjs.getLanguage(lang)) {
try {
return highlightjs.highlight(lang, str, true).value;
} catch (__) {}
}
return mdr.utils.escapeHtml(str);
};
return '<pre class="hljs"><code>' + highlighter() + '</code></pre>';
};

export { MdrOptions, defaultOptions, codeHighlighter }
Loading

0 comments on commit 9c782ef

Please sign in to comment.