Skip to content

Commit

Permalink
feat(extension-codemirror6): first version of extension-codemirror6 (r…
Browse files Browse the repository at this point in the history
  • Loading branch information
ocavue authored Dec 24, 2021
1 parent 03e16d7 commit a289af9
Show file tree
Hide file tree
Showing 21 changed files with 1,472 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-apples-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@remirror/extension-codemirror6': minor
---

First version of `@remirror/extension-codemirror6`.
6 changes: 6 additions & 0 deletions docs/extensions/code-block-extension.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ This extension differs from the `CodeExtension`, which provides code marks on in

:::

:::info

Also check a more powerful code block implementation at [`CodeMirrorExtension`](codemirror-extension).

:::

## Features

### Formatting
Expand Down
73 changes: 73 additions & 0 deletions docs/extensions/codemirror-extension.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
hide_title: true
title: 'CodeMirrorExtension'
---

import Basic from '../../website/extension-examples/extension-codemirror6/basic';
import WithCustomExtension from '../../website/extension-examples/extension-codemirror6/with-custom-extension';

# `CodeMirrorExtension`

## Summary

The CodeMirror extension adds fenced code blocks to your editor powered by [CodeMirror 6](https://codemirror.net/6/). A code block contains a formatted snippets of source code.

:::info

This extension differs from the `CodeExtension`, which provides code marks on inline text.

:::

:::info

Also check a simpler code block implementation at [`CodeBlockExtension`](code-block-extension).

:::

## Features

### Keyboard support

Users can create a new code block by typing <code>&grave;&grave;&grave;&nbsp;</code> (3 ticks and a space). Users can define the language of the code block by typing <code>&grave;&grave;&grave;html&nbsp;</code>.

## Usage

### Installation

This extension is NOT installed for you when you install the main `remirror` package but need to be installed separately:

```bash
npm install @remirror/extension-codemirror6
```

You will probably also want to install some basic CodeMirror packages:

```bash
npm install @codemirror/basic-setup @codemirror/language-data @codemirror/theme-one-dark
```

You can use the imports in the following way:

```ts
import { basicSetup } from '@codemirror/basic-setup';
import { languages } from '@codemirror/language-data';
import { oneDark } from '@codemirror/theme-one-dark';
import { CodeMirrorExtension } from '@remirror/extension-codemirror6';

const codeMirrorExtension = new CodeMirrorExtension({
languages: languages,
extensions: [basicSetup, oneDark],
});
```

### Examples

<Basic />

You can customize your CodeMirror editor by passing your custom extensions.

<WithCustomExtension />

## API

- [CodeMirrorExtension](../api/extension-codemirror6)
63 changes: 63 additions & 0 deletions packages/remirror__extension-codemirror6/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "@remirror/extension-codemirror6",
"version": "0.0.0",
"description": "Add CodeMirror6 to your editor.",
"keywords": [
"remirror",
"remirror-extension",
"CodeMirror",
"editor"
],
"homepage": "https://github.com/remirror/remirror/tree/HEAD/packages/remirror__extension-codemirror6",
"repository": {
"type": "git",
"url": "https://github.com/remirror/remirror.git",
"directory": "packages/remirror__extension-codemirror6"
},
"license": "MIT",
"contributors": [
"Ocavue <[email protected]>"
],
"sideEffects": false,
"exports": {
".": {
"import": "./dist/remirror-extension-codemirror6.esm.js",
"require": "./dist/remirror-extension-codemirror6.cjs.js",
"browser": "./dist/remirror-extension-codemirror6.browser.esm.js",
"types": "./dist/remirror-extension-codemirror6.cjs.d.ts",
"default": "./dist/remirror-extension-codemirror6.esm.js"
},
"./package.json": "./package.json",
"./types/*": "./dist/declarations/src/*.d.ts"
},
"main": "dist/remirror-extension-codemirror6.cjs.js",
"module": "dist/remirror-extension-codemirror6.esm.js",
"browser": {
"./dist/remirror-extension-codemirror6.cjs.js": "./dist/remirror-extension-codemirror6.browser.cjs.js",
"./dist/remirror-extension-codemirror6.esm.js": "./dist/remirror-extension-codemirror6.browser.esm.js"
},
"types": "dist/remirror-extension-codemirror6.cjs.d.ts",
"files": [
"dist"
],
"dependencies": {
"@babel/runtime": "^7.13.10",
"@codemirror/language": "^0.19.7",
"@codemirror/state": "^0.19.6",
"@codemirror/view": "^0.19.37",
"@remirror/core": "^1.3.1"
},
"devDependencies": {
"@remirror/pm": "^1.0.7"
},
"peerDependencies": {
"@remirror/pm": "^1.0.7"
},
"publishConfig": {
"access": "public"
},
"@remirror": {
"sizeLimit": "60 KB"
},
"rn:dev": "src/index.ts"
}
23 changes: 23 additions & 0 deletions packages/remirror__extension-codemirror6/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# @remirror/extension-codemirror6

> Add [CodeMirror](https://codemirror.net/) to your editor.
[![Version][version]][npm] [![Weekly Downloads][downloads-badge]][npm] [![Bundled size][size-badge]][size] [![Typed Codebase][typescript]](#) [![MIT License][license]](#)

[version]: https://flat.badgen.net/npm/v/@remirror/extension-codemirror6
[npm]: https://npmjs.com/package/@remirror/extension-codemirror6
[license]: https://flat.badgen.net/badge/license/MIT/purple
[size]: https://bundlephobia.com/result?p=@remirror/extension-codemirror6@next
[size-badge]: https://flat.badgen.net/bundlephobia/minzip/@remirror/extension-codemirror6@next
[typescript]: https://flat.badgen.net/badge/icon/TypeScript?icon=typescript&label
[downloads-badge]: https://badgen.net/npm/dw/@remirror/extension-codemirror6/red?icon=npm

## Installation

```bash
yarn add @remirror/extension-codemirror6 # yarn
pnpm add @remirror/extension-codemirror6 # pnpm
npm install @remirror/extension-codemirror6 # npm
```

<!-- ## Usage -->
185 changes: 185 additions & 0 deletions packages/remirror__extension-codemirror6/src/codemirror-extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import type { LanguageDescription, LanguageSupport } from '@codemirror/language';
import {
ApplySchemaAttributes,
EditorView,
extension,
GetAttributes,
getMatchString,
InputRule,
isElementDomNode,
isTextSelection,
keyBinding,
KeyBindingProps,
NodeExtension,
NodeExtensionSpec,
nodeInputRule,
NodeSpecOverride,
NodeViewMethod,
PrioritizedKeyBindings,
ProsemirrorNode,
} from '@remirror/core';
import { TextSelection } from '@remirror/pm/state';

import { CodeMirror6NodeView } from './codemirror-node-view';
import { CodeMirrorExtensionOptions } from './codemirror-types';
import { arrowHandler } from './codemirror-utils';

@extension<CodeMirrorExtensionOptions>({
defaultOptions: {
extensions: null,
languages: null,
},
})
export class CodeMirrorExtension extends NodeExtension<CodeMirrorExtensionOptions> {
get name() {
return 'codeMirror' as const;
}

private languageMap: Record<string, LanguageDescription> | null = null;

createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
return {
group: 'block',
content: 'text*',
marks: '',
defining: true,
...override,
code: true,
attrs: {
...extra.defaults(),
language: { default: '' },
},
parseDOM: [
{
tag: 'pre',
getAttrs: (node) => (isElementDomNode(node) ? extra.parse(node) : false),
},
...(override.parseDOM ?? []),
],
toDOM() {
return ['pre', ['code', 0]];
},
isolating: true,
};
}

createNodeViews(): NodeViewMethod {
return (node: ProsemirrorNode, view: EditorView, getPos: boolean | (() => number)) => {
return new CodeMirror6NodeView({
node,
view,
getPos: getPos as () => number,
extensions: this.options.extensions,
loadLanguage: this.loadLanguage.bind(this),
});
};
}

createKeymap(): PrioritizedKeyBindings {
return {
ArrowLeft: arrowHandler('left'),
ArrowRight: arrowHandler('right'),
ArrowUp: arrowHandler('up'),
ArrowDown: arrowHandler('down'),
};
}

/**
* Create an input rule that listens converts the code fence into a code block
* when typing triple back tick followed by a space.
*/
createInputRules(): InputRule[] {
const regexp = /^```(\S+) $/;

const getAttributes: GetAttributes = (match) => {
const language = getMatchString(match, 1) ?? '';
return { language };
};

return [
nodeInputRule({
regexp,
type: this.type,
beforeDispatch: ({ tr, start }) => {
const $pos = tr.doc.resolve(start);
tr.setSelection(new TextSelection($pos));
},
getAttributes: getAttributes,
}),
];
}

@keyBinding({ shortcut: 'Enter' })
enterKey({ dispatch, tr }: KeyBindingProps): boolean {
if (!(isTextSelection(tr.selection) && tr.selection.empty)) {
return false;
}

const { nodeBefore, parent } = tr.selection.$anchor;

if (!nodeBefore || !nodeBefore.isText || !parent.type.isTextblock) {
return false;
}

const regex = /^```(\S*)?$/;
const { text, nodeSize } = nodeBefore;
const { textContent } = parent;

if (!text) {
return false;
}

const matchesNodeBefore = text.match(regex);
const matchesParent = textContent.match(regex);

if (!matchesNodeBefore || !matchesParent) {
return false;
}

const language = getMatchString(matchesNodeBefore, 1) ?? '';

const pos = tr.selection.$from.before();
const end = pos + nodeSize + 1; // +1 to account for the extra pos a node takes up
tr.replaceWith(pos, end, this.type.create({ language }));

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

if (dispatch) {
dispatch(tr);
}

return true;
}

private getLanguageMap(): Record<string, LanguageDescription> {
if (!this.languageMap) {
this.languageMap = {};

for (const language of this.options.languages ?? []) {
for (const alias of language.alias) {
this.languageMap[alias] = language;
}
}
}

return this.languageMap;
}

private loadLanguage(
languageName: string,
): Promise<LanguageSupport> | LanguageSupport | undefined {
if (typeof languageName !== 'string') {
return undefined;
}

const languageMap = this.getLanguageMap();
const language = languageMap[languageName.toLowerCase()];

if (!language) {
return undefined;
}

return language.support || language.load();
}
}
Loading

0 comments on commit a289af9

Please sign in to comment.