-
Notifications
You must be signed in to change notification settings - Fork 805
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Markdown Gutenberg block for Jetpack #9705
Changes from all commits
70694dd
271e43b
3571e35
5095d57
f6230c6
71b31ff
834b423
b8815bc
1a0abb0
22e85ab
2d36b2d
da8c861
aa8ab40
f947f3c
328133f
cae0799
e389669
a86e340
329a6d7
f442d27
048b684
c63d2da
c358410
35311f4
f569b49
e153ada
4cf2e0c
317b9d4
50bd3a1
6fb4768
a692bc4
023a37d
bcd11ba
f39083c
fd47ca8
38fe21f
84d1f72
e7a799d
b2e8709
61ea7a7
955570e
95c49f3
0db5ae8
a409432
e8af3af
24b56ae
c9be670
7526277
500d8c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
|
||
.wp-block-jetpack-markdown-block-placeholder { | ||
opacity: 0.5; | ||
pointer-events: none; | ||
} | ||
.wp-block-jetpack-markdown-block-live-preview p { | ||
min-height: 1.8em; | ||
white-space: pre-wrap; | ||
} | ||
.wp-block-jetpack-markdown-block__live-preview__token { | ||
/* $dark-gray-300 from Gutenberg _colors.scss */ | ||
color: #6c7781; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import React from 'react'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import markdownConverter from '../utils/markdown-converter'; | ||
|
||
const { | ||
RawHTML | ||
} = window.wp.element; | ||
|
||
const MarkdownRenderer = function( props ) { | ||
const { className, source } = props; | ||
|
||
let content = ''; | ||
|
||
if ( source ) { | ||
// converts the markdown source to HTML | ||
content = markdownConverter.render( source ); | ||
} | ||
return <RawHTML className={ className }>{ content }</RawHTML>; | ||
}; | ||
|
||
export default MarkdownRenderer; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import React from 'react'; | ||
import classNames from 'classnames'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import MarkdownRenderer from './components/markdown-renderer'; | ||
|
||
const { __ } = window.wp.i18n; | ||
|
||
const { | ||
ButtonGroup | ||
} = window.wp.components; | ||
|
||
const { | ||
BlockControls, | ||
PlainText | ||
} = window.wp.editor; | ||
|
||
const { | ||
Component | ||
} = window.wp.element; | ||
|
||
const PANEL_EDITOR = 'editor'; | ||
const PANEL_PREVIEW = 'preview'; | ||
|
||
class JetpackMarkdownBlockEditor extends Component { | ||
|
||
constructor() { | ||
super( ...arguments ); | ||
|
||
this.updateSource = this.updateSource.bind( this ); | ||
this.showEditor = this.showEditor.bind( this ); | ||
this.showPreview = this.showPreview.bind( this ); | ||
this.isEmpty = this.isEmpty.bind( this ); | ||
|
||
this.state = { | ||
activePanel: PANEL_EDITOR | ||
}; | ||
} | ||
|
||
isEmpty() { | ||
const source = this.props.attributes.source; | ||
return ! source || source.trim() === ''; | ||
} | ||
|
||
updateSource( source ) { | ||
this.props.setAttributes( { source } ); | ||
} | ||
|
||
showEditor() { | ||
this.setState( { activePanel: 'editor' } ); | ||
} | ||
|
||
showPreview() { | ||
this.setState( { activePanel: 'preview' } ); | ||
} | ||
|
||
render() { | ||
const { attributes, className, isSelected } = this.props; | ||
|
||
if ( ! isSelected && this.isEmpty() ) { | ||
return ( | ||
<p className={ `${ className }__placeholder` }> | ||
{ __( 'Write your _Markdown_ **here**...' ) } | ||
</p> | ||
); | ||
} | ||
|
||
// Renders the editor panel or the preview panel based on component's state | ||
const editorOrPreviewPanel = function() { | ||
const source = attributes.source; | ||
|
||
switch ( this.state.activePanel ) { | ||
case PANEL_EDITOR: | ||
return <PlainText | ||
className={ `${ className }__editor` } | ||
onChange={ this.updateSource } | ||
aria-label={ __( 'Markdown' ) } | ||
value={ attributes.source } | ||
/>; | ||
|
||
case PANEL_PREVIEW: | ||
return <MarkdownRenderer | ||
className={ `${ className }__preview` } | ||
source={ source } | ||
/>; | ||
} | ||
}; | ||
|
||
// Manages css classes for each panel based on component's state | ||
const classesForPanel = function( panelName ) { | ||
return classNames( { | ||
'components-tab-button': true, | ||
'is-active': this.state.activePanel === panelName, | ||
[ `${ className }__${ panelName }-button` ]: true | ||
} ); | ||
}; | ||
|
||
return ( | ||
<div> | ||
<BlockControls > | ||
<ButtonGroup> | ||
<button | ||
className={ classesForPanel.call( this, 'editor' ) } | ||
onClick={ this.showEditor } | ||
> | ||
<span>{ __( 'Markdown' ) }</span> | ||
</button> | ||
<button | ||
className={ classesForPanel.call( this, 'preview' ) } | ||
onClick={ this.showPreview } | ||
> | ||
<span>{ __( 'Preview' ) }</span> | ||
</button> | ||
</ButtonGroup> | ||
</BlockControls> | ||
{ ( editorOrPreviewPanel.call( this ) ) } | ||
</div> | ||
); | ||
} | ||
|
||
} | ||
export default JetpackMarkdownBlockEditor; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import React from 'react'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import MarkdownPreview from './components/markdown-renderer'; | ||
|
||
function JetpackMarkdownBlockSave( { attributes, className } ) { | ||
return <MarkdownPreview | ||
className={ `${ className }-renderer` } | ||
source={ attributes.source } | ||
/>; | ||
} | ||
|
||
export default JetpackMarkdownBlockSave; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import React from 'react'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import JetpackMarkdownBlockEditor from './jetpack-markdown-block-editor'; | ||
import JetpackMarkdownBlockSave from './jetpack-markdown-block-save'; | ||
|
||
const { __ } = window.wp.i18n; | ||
|
||
const { | ||
registerBlockType, | ||
} = window.wp.blocks; | ||
|
||
registerBlockType( 'jetpack/markdown-block', { | ||
|
||
title: __( 'Markdown' ), | ||
|
||
description: [ | ||
__( 'Write your content in plain-text Markdown syntax.' ), | ||
( | ||
<p> | ||
<a href="https://en.support.wordpress.com/markdown-quick-reference/"> | ||
Support Reference | ||
</a> | ||
</p> | ||
) | ||
], | ||
|
||
icon: <svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
class="dashicon" | ||
width="20" | ||
height="20" | ||
viewBox="0 0 208 128" | ||
stroke="currentColor" | ||
> | ||
<rect | ||
width="198" | ||
height="118" | ||
x="5" | ||
y="5" | ||
ry="10" | ||
stroke-width="10" | ||
fill="none" | ||
/> | ||
<path d="M30 98v-68h20l20 25 20-25h20v68h-20v-39l-20 25-20-25v39zM155 98l-30-33h20v-35h20v35h20z" /> | ||
</svg>, | ||
|
||
category: 'formatting', | ||
|
||
attributes: { | ||
//The Markdown source is saved in the block content comments delimiter | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What was your reasoning around saving the source in the attributes? Why not in the block's body? Why not in block meta? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought it was the cleanest solution. It makes super simple to link the markdown source with the block's content (in case you add more than one markdown blocks).
I guess you meant in the post's meta, right? I guess one of the uses of the post's meta is to share post's info in another parts of WordPress: other plugins, themes, etc. I didn't think the markdown source would be needed in any other place. It would make it harder to manage each markdown block source: if we store the attribute as it is, it'll return a strings array. If we choose to store as json, it'd have to serialize/parse it every time we needed. It seemed way more complex to do it this way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At a glance, I agree with keeping the Markdown source as a regular (HTML-comment-borne) attribute. Aside from ease on the development side, it also gets us data deduplication for free (that is, when rendering the front-end, assuming Gutenberg is stripping HTML comments, the visitor only gets the generated HTML source). For completion:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the clarification @mcsf ! Especially the last point 👍 |
||
source: { type: 'string' } | ||
}, | ||
|
||
edit: JetpackMarkdownBlockEditor, | ||
|
||
save: JetpackMarkdownBlockSave, | ||
|
||
} ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import MarkdownIt from 'markdown-it'; | ||
|
||
const markdownItFull = new MarkdownIt(); | ||
|
||
const MarkdownConverter = { | ||
|
||
render( source ) { | ||
return markdownItFull.render( source ); | ||
} | ||
|
||
}; | ||
|
||
export default MarkdownConverter; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import React, { createElement } from 'react'; | ||
import { expect } from 'chai'; | ||
import { describe, it } from 'mocha'; | ||
import Enzyme, { shallow } from 'enzyme'; | ||
import Adapter from 'enzyme-adapter-react-15'; | ||
|
||
Enzyme.configure( { adapter: new Adapter() } ); | ||
|
||
const __ = ( literal ) => { | ||
return literal; | ||
}; | ||
|
||
global.window.wp = { | ||
element: { | ||
RawHTML: function( { children } ) { | ||
return createElement( 'div', { | ||
dangerouslySetInnerHTML: { __html: children }, | ||
} ); | ||
} | ||
}, | ||
i18n: { | ||
__: __ | ||
}, | ||
}; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
const MarkdownRenderer = require( '../../js/components/markdown-renderer' ); | ||
|
||
const markdownSource = 'This is *Markdown* __source__.'; | ||
const markdownHTML = `<div><p>This is <em>Markdown</em> <strong>source</strong>.</p> | ||
</div>`; | ||
|
||
describe( 'MarkdownRenderer', () => { | ||
it( 'renders HTML from Markdown source', () => { | ||
const markdownRenderer = shallow( <MarkdownRenderer source={ markdownSource } /> ); | ||
expect( markdownRenderer.html() ).to.equal( markdownHTML ); | ||
} ); | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see mixed usage of JSX and
el()
notation in this file.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ops, my fault, first time using JSX. I'll fix that