Skip to content
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

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
70694dd
Adds initial jetpack markdown block php class
Ferdev Jun 7, 2018
271e43b
Markdown block: renders markdown code as html
Ferdev Jun 11, 2018
3571e35
Markdown block: adds a preview panel
Ferdev Jun 11, 2018
5095d57
Markdown block: adds a markdown preview component
Ferdev Jun 11, 2018
f6230c6
Markdown block: adds a live preview component
Ferdev Jun 15, 2018
71b31ff
Markdown block: changes markdown parser
Ferdev Jun 15, 2018
834b423
Markdown block: emphasis tokens are now previewed.
Ferdev Jun 15, 2018
b8815bc
Markdown block: previews strong tokens
Ferdev Jun 15, 2018
1a0abb0
Markdown block: inline code can be previewed
Ferdev Jun 15, 2018
22e85ab
Markdown block: fixes caret in live preview
Ferdev Jun 15, 2018
2d36b2d
Markdown block: previews headings
Ferdev Jun 15, 2018
da8c861
Markdown block live preview: fixes bugs with caret
Ferdev Jun 15, 2018
aa8ab40
Markdown block live preview: source from props
Ferdev Jun 15, 2018
f947f3c
Markdown block live preview: new lines issue fixed
Ferdev Jun 17, 2018
328133f
Markdown block: fixes bug propagating onBlur event
Ferdev Jun 18, 2018
cae0799
Markdown block: improves empty state
Ferdev Jun 18, 2018
e389669
Markdown block: displays preview when not selected
Ferdev Jun 19, 2018
a86e340
Loads only the the markdown support code needed
Ferdev Jun 20, 2018
329a6d7
Markdown block: fixes issues in IE 11
Ferdev Jun 20, 2018
f442d27
Markdown block: consolidates jsx notation
Ferdev Jun 21, 2018
048b684
Markdown block: converts component to function
Ferdev Jun 21, 2018
c63d2da
Markdown block: adds parentheses to return
Ferdev Jun 21, 2018
c358410
Markdown block: unwraps code from function
Ferdev Jun 21, 2018
35311f4
Markdown block: changes function order
Ferdev Jun 21, 2018
f569b49
Markdown block: improved caret position saving
Ferdev Jun 21, 2018
e153ada
Markdown block: sets const only once
Ferdev Jun 21, 2018
4cf2e0c
Markdown block: moves chars to constants
Ferdev Jun 21, 2018
317b9d4
Markdown block: renames function
Ferdev Jun 21, 2018
50bd3a1
Markdown block: renames component
Ferdev Jun 21, 2018
6fb4768
Markdown block live preview: improved contrast
Ferdev Jun 22, 2018
a692bc4
Markdonw block: fixes issue pasting Markdown
Ferdev Jun 25, 2018
023a37d
Markdown block live preview: refactored
Ferdev Jun 25, 2018
bcd11ba
Markdown block live preview: fixed bug in lists
Ferdev Jun 25, 2018
f39083c
Markdown block: fixed icon
Ferdev Jun 25, 2018
fd47ca8
Markdown block live preview: bug pasting markdown
Ferdev Jun 25, 2018
38fe21f
Markdown block live preview: improves performance
Ferdev Jun 29, 2018
84d1f72
Markdown block live preview: uses JSX notation
Ferdev Jun 29, 2018
e7a799d
Markdown block: pins Markdown-It version to v8.4.1
Ferdev Jun 30, 2018
b2e8709
Markdown block: fixed a performance issue
Ferdev Jul 4, 2018
61ea7a7
Markdown block: add test for block file
Ferdev Jul 10, 2018
955570e
Markdown block: added test for save component
Ferdev Jul 11, 2018
95c49f3
Markdown block: adds test for MarkdownRenderer
Ferdev Jul 11, 2018
0db5ae8
Markdown block: tests the MarkdownConverter util
Ferdev Jul 11, 2018
a409432
Markdown block: tests the editor component
Ferdev Jul 12, 2018
e8af3af
Markdown block: tests the live preview component
Ferdev Jul 16, 2018
24b56ae
Markdown block live preview: adds more tests
Ferdev Jul 16, 2018
c9be670
Markdown block: removes a React warning in tests
Ferdev Jul 16, 2018
7526277
Markdown block: fixes jshint errors
Ferdev Jul 16, 2018
500d8c5
Markdown block: Live preview component replaced
Ferdev Jul 17, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .jshintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ _inc/jquery.spin.js
_inc/postmessage.js
_inc/spin.js
modules/custom-css/custom-css/js/codemirror.min.js
modules/markdown/assets/js/**/*.js
modules/shortcodes/js/jmpress.js
modules/shortcodes/js/jquery.cycle.min.js
modules/theme-tools/responsive-videos/responsive-videos.min.js
Expand Down
29 changes: 17 additions & 12 deletions modules/markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,23 @@
* Additional Search Queries: md, markdown
*/

include dirname( __FILE__ ) . '/markdown/easy-markdown.php';
if ( function_exists( 'register_block_type' ) ) {
include dirname( __FILE__ ) . '/markdown/class-jetpack-markdown-block.php';
} else {
include dirname( __FILE__ ) . '/markdown/easy-markdown.php';

/**
* Remove checkbox set in modules/markdown/easy-markdown.php.
* We don't just remove the register_setting call there because the checkbox is
* needed on WordPress.com, where the file is sync'ed verbatim.
*/
function jetpack_markdown_posting_always_on() {
// why oh why isn't there a remove_settings_field?
global $wp_settings_fields;
if ( isset( $wp_settings_fields['writing']['default'][ WPCom_Markdown::POST_OPTION ] ) ) {
unset( $wp_settings_fields['writing']['default'][ WPCom_Markdown::POST_OPTION ] );
/**
* Remove checkbox set in modules/markdown/easy-markdown.php.
* We don't just remove the register_setting call there because the checkbox is
* needed on WordPress.com, where the file is sync'ed verbatim.
*/
function jetpack_markdown_posting_always_on() {
// why oh why isn't there a remove_settings_field?
global $wp_settings_fields;
if ( isset( $wp_settings_fields['writing']['default'][ WPCom_Markdown::POST_OPTION ] ) ) {
unset( $wp_settings_fields['writing']['default'][ WPCom_Markdown::POST_OPTION ] );
}
}
add_action( 'admin_init', 'jetpack_markdown_posting_always_on', 11 );
}
add_action( 'admin_init', 'jetpack_markdown_posting_always_on', 11 );

13 changes: 13 additions & 0 deletions modules/markdown/assets/css/jetpack-markdown-block.css
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;
}
27 changes: 27 additions & 0 deletions modules/markdown/assets/js/components/markdown-renderer.jsx
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;
127 changes: 127 additions & 0 deletions modules/markdown/assets/js/jetpack-markdown-block-editor.jsx
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;
18 changes: 18 additions & 0 deletions modules/markdown/assets/js/jetpack-markdown-block-save.jsx
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;
64 changes: 64 additions & 0 deletions modules/markdown/assets/js/jetpack-markdown-block.jsx
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>
Copy link
Member

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.

Copy link
Contributor Author

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

<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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

@Ferdev Ferdev Jun 14, 2018

Choose a reason for hiding this comment

The 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).
When making blocks I guess we should strive to save the attributes in the block's content, that way we avoid data duplication, but I couldn't find a way to store the markdown source in the block's content and then hide it in the published post (but I'm a total WordPress newbie, maybe there's a way)...

Why not in block meta?

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.

Copy link
Member

Choose a reason for hiding this comment

The 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:

  • Keeping the Markdown source in the block's body could be achieved using old techniques such as using custom script tags like <script type="text/markdown"> that the browser can ignore. Gutenberg can be told how to source attributes from these. The downside is that this information is wastefully sent to the visitor.

  • It's possible to intercept the block's body to decide what gets sent with the rendered page, but that requires making the block dynamic (and having a PHP render callback to render the block on every page load). I don't see how this option could be better than defaulting to HTML comments for the Markdown source, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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,

} );
16 changes: 16 additions & 0 deletions modules/markdown/assets/js/utils/markdown-converter.js
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;
43 changes: 43 additions & 0 deletions modules/markdown/assets/test/components/test-markdown-renderer.js
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 );
} );
} );
Loading