Skip to content

Commit

Permalink
feat: enable formatter for codeBlock contents
Browse files Browse the repository at this point in the history
  • Loading branch information
ifiokjr committed Jul 20, 2019
1 parent fb1d060 commit ad330a9
Show file tree
Hide file tree
Showing 16 changed files with 155 additions and 70 deletions.
6 changes: 3 additions & 3 deletions @remirror/core-extensions/src/nodes/hard-break.ts
Expand Up @@ -19,10 +19,10 @@ export class HardBreakExtension extends NodeExtension {

public keys({ type }: SchemaNodeTypeParams) {
const command = chainCommands(exitCode, (state, dispatch) => {
if (!dispatch) {
return true;
if (dispatch) {
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView());
}
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView());

return true;
});
return {
Expand Down
6 changes: 3 additions & 3 deletions @remirror/core-extensions/src/nodes/horizontal-rule.ts
Expand Up @@ -25,10 +25,10 @@ export class HorizontalRuleExtension extends NodeExtension<NodeExtensionOptions,
public commands({ type }: CommandNodeTypeParams) {
return {
horizontalRule: (): CommandFunction => (state, dispatch) => {
if (!dispatch) {
return false;
if (dispatch) {
dispatch(state.tr.replaceSelectionWith(type.create()));
}
dispatch(state.tr.replaceSelectionWith(type.create()));

return true;
},
};
Expand Down
9 changes: 5 additions & 4 deletions @remirror/core-extensions/src/nodes/image.ts
Expand Up @@ -53,14 +53,15 @@ export class ImageExtension extends NodeExtension<NodeExtensionOptions, 'image',
public commands({ type }: CommandNodeTypeParams) {
return {
image: (attrs?: Attrs): CommandFunction => (state, dispatch) => {
if (!dispatch) {
return false;
}
const { selection } = state;
const position = hasCursor(selection) ? selection.$cursor.pos : selection.$to.pos;
const node = type.create(attrs);
const transaction = state.tr.insert(position, node);
dispatch(transaction);

if (dispatch) {
dispatch(transaction);
}

return true;
},
};
Expand Down
14 changes: 7 additions & 7 deletions @remirror/core/src/helpers/__tests__/utils.spec.ts
Expand Up @@ -18,7 +18,7 @@ import { NodeSelection, TextSelection } from 'prosemirror-state';
import { omit } from '../base';
import {
cloneTransaction,
findDOMRefAtPos,
findElementAtPosition,
findParentNode,
findParentNodeOfType,
findPositionOfNodeBefore,
Expand Down Expand Up @@ -149,36 +149,36 @@ describe('findPositionOfNodeBefore', () => {
});
});

describe('findDomRefAtPos', () => {
describe('findElementAtPosition', () => {
it('should return DOM reference of a top level block leaf node', () => {
const { view } = createEditor(doc(p('text'), atomBlock()));
const ref = findDOMRefAtPos(6, view);
const ref = findElementAtPosition(6, view);
expect(ref instanceof HTMLDivElement).toBe(true);
expect((ref as HTMLElement).getAttribute('data-node-type')).toEqual('atomBlock');
});

it('should return DOM reference of a nested inline leaf node', () => {
const { view } = createEditor(doc(p('one', atomInline(), 'two')));
const ref = findDOMRefAtPos(4, view);
const ref = findElementAtPosition(4, view);
expect(ref instanceof HTMLSpanElement).toBe(true);
expect((ref as HTMLElement).getAttribute('data-node-type')).toEqual('atomInline');
});

it('should return DOM reference of a content block node', () => {
const { view } = createEditor(doc(p('one'), blockquote(p('two'))));
const ref = findDOMRefAtPos(5, view);
const ref = findElementAtPosition(5, view);
expect(ref instanceof HTMLQuoteElement).toBe(true);
});

it('should return DOM reference of a text node when offset=0', () => {
const { view } = createEditor(doc(p('text')));
const ref = findDOMRefAtPos(1, view);
const ref = findElementAtPosition(1, view);
expect(ref instanceof HTMLParagraphElement).toBe(true);
});

it('should return DOM reference of a paragraph if cursor is inside of a text node', () => {
const { view } = createEditor(doc(p(atomInline(), 'text')));
const ref = findDOMRefAtPos(3, view);
const ref = findElementAtPosition(3, view);
expect(ref instanceof HTMLParagraphElement).toBe(true);
});
});
Expand Down
1 change: 1 addition & 0 deletions @remirror/core/src/helpers/rules.ts
Expand Up @@ -113,4 +113,5 @@ interface NodeInputRuleParams extends Partial<GetAttrsParams>, RegExpParams, Nod
*/
updateSelection?: boolean;
}

interface MarkInputRuleParams extends Partial<GetAttrsParams>, RegExpParams, MarkTypeParams {}
9 changes: 4 additions & 5 deletions @remirror/core/src/helpers/utils.ts
Expand Up @@ -7,7 +7,6 @@ import {
NodeTypeParams,
NodeTypesParams,
PMNodeParams,
PosParams,
PredicateParams,
ProsemirrorNode,
Selection,
Expand Down Expand Up @@ -67,15 +66,15 @@ export const removeNodeAtPos = (position: number) => (tr: Transaction) => {
* A simple use case
*
* ```ts
* const ref = findDOMRefAtPos($from.pos, view);
* const element = findElementAtPosition($from.pos, view);
* ```
*
* @param position - the prosemirror position
* @param view - the editor view
*
* @public
*/
export const findDOMRefAtPos = (position: number, view: EditorView): HTMLElement => {
export const findElementAtPosition = (position: number, view: EditorView): HTMLElement => {
const dom = view.domAtPos(position);
const node = dom.node.childNodes[dom.offset];

Expand Down Expand Up @@ -144,14 +143,14 @@ export const findSelectedNodeOfType = ({
return undefined;
};

export interface FindParentNode extends PosParams, PMNodeParams {
export interface FindParentNode extends PMNodeParams {
/**
* The start position of the node.
*/
start: number;

/**
* Points to directly before the node.
* Points to position directly before the node.
*/
pos: number;
}
Expand Down
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`commands updateAttrs updates the language 1`] = `"<code data-code-block-language=\\"javascript\\"><span class=\\"token keyword\\">const</span><span> a </span><span class=\\"token operator\\">=</span><span> </span><span class=\\"token string\\">'test'</span><span class=\\"token punctuation\\">;</span></code>"`;
exports[`commands updateCodeBlock updates the language 1`] = `"<code data-code-block-language=\\"javascript\\"><span class=\\"token keyword\\">const</span><span> a </span><span class=\\"token operator\\">=</span><span> </span><span class=\\"token string\\">'test'</span><span class=\\"token punctuation\\">;</span></code>"`;
exports[`plugin can be updated 1`] = `"<span class=\\"token keyword\\">const</span><span> a </span><span class=\\"token operator\\">=</span><span> </span><span class=\\"token string\\">'test'</span><span class=\\"token punctuation\\">;</span>"`;
Expand Down
54 changes: 53 additions & 1 deletion @remirror/extension-code-block/src/__tests__/code-block.spec.ts
Expand Up @@ -2,10 +2,14 @@ import { fromHTML, toHTML } from '@remirror/core';
import { createBaseTestManager } from '@test-fixtures/schema-helpers';
import { pmBuild } from 'jest-prosemirror';
import { renderEditor } from 'jest-remirror';
import typescriptPlugin from 'prettier/parser-typescript';
import { formatWithCursor } from 'prettier/standalone';
import javascript from 'refractor/lang/javascript';
import markdown from 'refractor/lang/markdown';
import typescript from 'refractor/lang/typescript';
import { CodeBlockExtension, CodeBlockExtensionOptions } from '../';
import { CodeBlockFormatter } from '../code-block-types';
import { getLanguage } from '../code-block-utils';

describe('schema', () => {
const { schema } = createBaseTestManager([{ extension: new CodeBlockExtension(), priority: 1 }]);
Expand Down Expand Up @@ -182,6 +186,8 @@ describe('commands', () => {
nodes: { doc },
} = create();

let tsBlock = codeBlock({ language: 'typescript' });

beforeEach(() => {
({
view,
Expand All @@ -191,7 +197,7 @@ describe('commands', () => {
} = create());
});

describe('updateAttrs ', () => {
describe('updateCodeBlock ', () => {
it('updates the language', () => {
const markupBlock = codeBlock({ language: 'markup' });
const { actions } = add(doc(markupBlock(`const a = 'test';<cursor>`)));
Expand All @@ -204,4 +210,50 @@ describe('commands', () => {
expect(view.dom.querySelector('.language-javascript code')!.outerHTML).toMatchSnapshot();
});
});

describe('formatCodeBlock', () => {
const formatter: CodeBlockFormatter = ({ cursorOffset, language, source }) => {
if (getLanguage({ fallback: 'text', language, supportedLanguages }) === 'typescript') {
return formatWithCursor(source, {
cursorOffset,
plugins: [typescriptPlugin],
parser: 'typescript',
singleQuote: true,
});
}
return;
};

beforeEach(() => {
({
view,
add,
attrNodes: { codeBlock },
nodes: { doc },
} = create({ formatter }));

tsBlock = codeBlock({ language: 'typescript' });
});

it('can format the codebase', () => {
const { actions } = add(
doc(tsBlock(`const a: string\n = 'test' ;\n\n\nconsole.log("welcome friends")<cursor>`)),
);
actions.formatCodeBlock();
expect(view.state.doc).toEqualRemirrorDocument(
doc(tsBlock(`const a: string = 'test';\n\nconsole.log('welcome friends');\n`)),
);
});

it('maintains cursor position after formatting', () => {
const { actions, insertText } = add(
doc(tsBlock(`const a: string\n = 'test<cursor>' ;\n\n\nconsole.log("welcome friends")`)),
);
actions.formatCodeBlock();
insertText('ing');
expect(view.state.doc).toEqualRemirrorDocument(
doc(tsBlock(`const a: string = 'testing';\n\nconsole.log('welcome friends');\n`)),
);
});
});
});
14 changes: 6 additions & 8 deletions @remirror/extension-code-block/src/code-block-extension.ts
Expand Up @@ -29,7 +29,7 @@ export const codeBlockDefaultOptions: CodeBlockExtensionOptions = {
supportedLanguages: [],
syntaxTheme: 'atomDark' as SyntaxTheme,
defaultLanguage: 'markup',
formatter: () => false,
formatter: () => undefined,
};

export class CodeBlockExtension extends NodeExtension<
Expand Down Expand Up @@ -119,12 +119,13 @@ export class CodeBlockExtension extends NodeExtension<
}

public commands({ type, schema }: CommandNodeTypeParams) {
const { defaultLanguage, supportedLanguages, formatter } = this.options;
return {
toggleCodeBlock: (attrs?: Attrs) =>
toggleBlockItem({ type, toggleType: schema.nodes.paragraph, attrs }),
createCodeBlock: (attrs?: Attrs) => setBlockType(type, attrs),
updateCodeBlock: updateNodeAttrs(type),
formatCodeBlock: formatCodeBlockFactory(type, this.options.formatter),
formatCodeBlock: formatCodeBlockFactory({ type, formatter, defaultLanguage, supportedLanguages }),
};
}

Expand Down Expand Up @@ -225,15 +226,12 @@ export class CodeBlockExtension extends NodeExtension<
tr.replaceWith(pos, end, type.create({ language }));

// Set the selection to within the codeBlock
const $pos = tr.doc.resolve(pos + 1);
tr.setSelection(new TextSelection($pos));
tr.setSelection(TextSelection.create(tr.doc, pos + 1));

if (!dispatch) {
return false;
if (dispatch) {
dispatch(tr);
}

dispatch(tr);

return true;
},
};
Expand Down
4 changes: 2 additions & 2 deletions @remirror/extension-code-block/src/code-block-types.ts
Expand Up @@ -75,7 +75,7 @@ export interface CodeBlockExtensionOptions extends NodeExtensionOptions {
onHover?(params: any): void;
}

export type CodeBlockFormatter = (params: FormatterParams) => FormatterReturn | false;
export type CodeBlockFormatter = (params: FormatterParams) => FormatterReturn | undefined;

export interface FormatterParams {
/**
Expand All @@ -101,7 +101,7 @@ export interface FormatterReturn {
/**
* The transformed source.
*/
output: string;
formatted: string;

/**
* The new cursor position after formatting
Expand Down
53 changes: 39 additions & 14 deletions @remirror/extension-code-block/src/code-block-utils.ts
Expand Up @@ -19,9 +19,10 @@ import {
} from '@remirror/core';
import { Decoration } from 'prosemirror-view';
import refractor, { RefractorNode, RefractorSyntax } from 'refractor/core';
import { CodeBlockAttrs, CodeBlockFormatter } from './code-block-types';
import { CodeBlockAttrs, CodeBlockExtensionOptions } from './code-block-types';

// Refractor languages
import { TextSelection } from 'prosemirror-state';
import clike from 'refractor/lang/clike';
import css from 'refractor/lang/css';
import js from 'refractor/lang/javascript';
Expand Down Expand Up @@ -163,32 +164,56 @@ export const updateNodeAttrs = (type: NodeType) => (attrs?: Attrs): CommandFunct

tr.setNodeMarkup(parent.pos, type, attrs);

if (!dispatch) {
return true;
if (dispatch) {
dispatch(tr);
}

dispatch(tr);
return true;
};

export const formatCodeBlockFactory = (
_type: NodeType,
formatter: CodeBlockFormatter,
) => (): CommandFunction => state => {
// Check if block is type is active, if not return false
interface FormatCodeBlockFactoryParams
extends NodeTypeParams,
Required<Pick<CodeBlockExtensionOptions, 'formatter' | 'supportedLanguages' | 'defaultLanguage'>> {}

export const formatCodeBlockFactory = ({
type,
formatter,
supportedLanguages,
defaultLanguage: fallback,
}: FormatCodeBlockFactoryParams) => (): CommandFunction => (state, dispatch) => {
const { tr, selection } = state;

// Find the current codeBlock the cursor is positioned in.
const codeBlock = findParentNodeOfType({ types: type, selection });

if (!codeBlock) {
return false;
}

// Get the `language`, `source` and `cursorOffset` for the block and run the formatter
const {
node: { attrs, textContent, nodeSize },
start,
} = codeBlock;
const language = getLanguage({ language: attrs.language, fallback, supportedLanguages });
const format = formatter({ source: textContent, language, cursorOffset: selection.from });

// Get the `language`, `source` and `cursorOffset` for the block
// For cursor position it might be okay to just use the current position.
const format = formatter({ source: '', language: '', cursorOffset: state.selection.from });
if (!format) {
return false;
}

// const { cursorOffset, output } = format;
const { cursorOffset, formatted } = format;

// Replace the node content with the transformed text.
// Replace the codeBlock content with the transformed text.
tr.insertText(formatted, start, start + nodeSize - 2);

// Set the new selection
tr.setSelection(TextSelection.create(tr.doc, cursorOffset));

if (dispatch) {
dispatch(tr);
}

return true;
};

Expand Down

0 comments on commit ad330a9

Please sign in to comment.