├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── index.html ├── main.css ├── main.js └── value.js ├── lib ├── changes │ ├── dedentLines.js │ ├── indentLines.js │ ├── index.js │ ├── toggleCodeBlock.js │ ├── unwrapCodeBlock.js │ ├── unwrapCodeBlockByKey.js │ ├── wrapCodeBlock.js │ └── wrapCodeBlockByKey.js ├── core.js ├── handlers │ ├── index.js │ ├── onBackspace.js │ ├── onEnter.js │ ├── onKeyDown.js │ ├── onModEnter.js │ ├── onPaste.js │ ├── onSelectAll.js │ ├── onShiftTab.js │ └── onTab.js ├── index.js ├── options.js ├── utils │ ├── deserializeCode.js │ ├── getCurrentCode.js │ ├── getCurrentIndent.js │ ├── getIndent.js │ ├── index.js │ └── isInCodeBlock.js └── validation │ ├── index.js │ └── schema.js ├── package.json ├── tests ├── all.js ├── backspace-empty-code │ ├── change.js │ ├── expected.js │ └── input.js ├── backspace-start │ ├── change.js │ ├── expected.js │ └── input.js ├── enter-end-offset │ ├── change.js │ ├── expected.js │ └── input.js ├── enter-mid-offset │ ├── change.js │ ├── expected.js │ └── input.js ├── enter-start-offset │ ├── change.js │ ├── expected.js │ └── input.js ├── enter-with-indent │ ├── change.js │ ├── expected.js │ └── input.js ├── hyperscript.js ├── isincodeblock-true │ ├── change.js │ ├── expected.js │ └── input.js ├── on-exit-block │ ├── change.js │ ├── expected.js │ └── input.js ├── paste-middle │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-multiline-text │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-no-inlines │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-no-marks │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-no-multiple-orphan-lines │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-no-orphan-lines-wrapped │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-no-single-orphan-line │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-only-lines-empty │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-only-lines │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-only-text-join │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-only-text-with-blocks │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-only-text │ ├── change.js │ ├── expected.js │ └── input.js ├── schema-single-text │ ├── change.js │ ├── expected.js │ └── input.js ├── select-all │ ├── change.js │ ├── expected.js │ └── input.js ├── shift-tab-middle-offset │ ├── change.js │ ├── expected.js │ └── input.js ├── shift-tab-multilines │ ├── change.js │ ├── expected.js │ └── input.js ├── simulate-key.js ├── tab-middle-offset │ ├── change.js │ ├── expected.js │ └── input.js ├── tab-multilines │ ├── change.js │ ├── expected.js │ └── input.js ├── tab-start-offset │ ├── change.js │ ├── expected.js │ └── input.js ├── togglecodeblock-code │ ├── change.js │ ├── expected.js │ └── input.js ├── togglecodeblock-normal │ ├── change.js │ ├── expected.js │ └── input.js ├── unwrapcodeblock-multi-lines │ ├── change.js │ ├── expected.js │ └── input.js ├── unwrapcodeblock-normal │ ├── change.js │ ├── expected.js │ └── input.js ├── unwrapcodeblock-selection │ ├── change.js │ ├── expected.js │ └── input.js ├── wrapcodeblock-normal │ ├── change.js │ ├── expected.js │ └── input.js ├── wrapcodeblock-selection │ ├── change.js │ ├── expected.js │ └── input.js └── wrapcodeblock-with-inline │ ├── change.js │ ├── expected.js │ └── input.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-flow-strip-types" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | example/bundle.js 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "gitbook" 5 | ], 6 | "rules": { 7 | "no-param-reassign": 1, 8 | "import/no-commonjs": 2 9 | }, 10 | "plugins": [ 11 | "flowtype" 12 | ], 13 | "env": { 14 | "mocha": true 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [options] 2 | emoji=true 3 | esproposal.decorators=ignore 4 | unsafe.enable_getters_and_setters=true 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | dist 40 | example/bundle.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | !dist 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "stable" 5 | 6 | script: 7 | - npm run test 8 | - npm run lint 9 | # - npm run flow -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | All notable changes to this project will be documented in this file. This 4 | project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ### 0.15.7 7 | 8 | * Accept `immutable@4` as peer dependency 9 | 10 | ### 0.15.6 11 | 12 | * Improve normalization of non text inside code lines 13 | 14 | ### 0.15.4, 0.15.5 15 | 16 | * Fix normalization of non lines inside code container. 17 | 18 | ### 0.15.3 19 | 20 | * Reduce the likelihood of Maximum call stack exceeded, by avoiding 21 | `change.withoutNormalization`. 22 | 23 | ### 0.15.2 24 | 25 | * Small monkey patch to support the GitBook's fork of Slate. 26 | 27 | ### 0.15.1 28 | 29 | * Fix and improve orphan lines normalization. 30 | 31 | ### 0.15.0 32 | 33 | * Upgrade to be compatible with Slate > 0.33.x 34 | * Use the new schema definition for improved performance 35 | 36 | ### 0.14.0 37 | 38 | * Upgrade to be compatible with Slate > 0.32.x 39 | 40 | ### 0.13.3 41 | 42 | * Normalize multiline texts in code to be split into the appropriate number of 43 | code lines. 44 | 45 | ### 0.13.2 46 | 47 | * Add `getIndent` option to customize the indent unit used. 48 | * Fix Ctrl shortcuts on Windows/Linux 49 | 50 | ### 0.13.1 51 | 52 | * Properly declare `immutable` as a peer dependency (already a peer dependency 53 | of slate) 54 | 55 | ### 0.13.0 56 | 57 | * Upgrade to be compatible with Slate > 0.29.x 58 | 59 | ### 0.12.0 60 | 61 | * Upgrade to be compatible with Slate > 0.27.x 62 | 63 | ### 0.11.0 64 | 65 | * Upgrade to be compatable with Slate after the `expose-transform` branch went 66 | in. 67 | * change all instances of `transform` to `change` 68 | * change the namespace of `plugin.transforms` to `plugin.changes` 69 | 70 | ### 0.10.4 71 | 72 | * Added `onExit(transform: Transform): ?Transform` option 73 | 74 | ### 0.10.2 75 | 76 | * Upgrade to slate^0.19.7 77 | 78 | ### 0.10.1 79 | 80 | * Added `isInCodeBlock` utils 81 | * Added `wrapCodeBlock` and `wrapCodeBlockByKey` transforms 82 | * Added `unwrapCodeBlock` and `unwrapCodeBlockByKey` transforms 83 | 84 | ### 0.10.0 85 | 86 | * Added: Backspace in empty code container will convert it to default 87 | `exitBlockType` 88 | 89 | ### 0.9.2 90 | 91 | * Fix case-insensitive slate require 92 | 93 | ### 0.9.1 94 | 95 | * Export utils.deserializeCode that deserialize a text into a code block 96 | 97 | ### 0.9.0 98 | 99 | * _Breaking change_ Renamed option `shiftEnterBlockType` to `exitBlockType`. 100 | * Shift+Enter shortcut is now assigned to Mod+Enter, as before. 101 | 102 | ### 0.8.2 103 | 104 | * Fixed onPaste 105 | 106 | ### 0.8.1 107 | 108 | * Removed unused dependency 109 | 110 | ### 0.8.0 111 | 112 | * _Breaking change_ : Changed the structure of code blocks. A code block is now 113 | made of a container, and a list of lines. 114 | * Removed option `onlyIn` 115 | * Add option `containerType` 116 | * Add option `lineType` 117 | * Added option `shiftEnterBlockType` to determine the default block type when 118 | exiting a code block. 119 | * Added support for multi-lines Tab and Shift+Tab 120 | 121 | ### 0.7.0 122 | 123 | * Add option `selectAll` 124 | 125 | ### 0.6.2 126 | 127 | * Update slate peed dependency to prevent NPM warnings when used with `0.15.x` 128 | 129 | ### 0.6.1 130 | 131 | * Move slate to `peerDependencies` 132 | 133 | ### 0.6.0 134 | 135 | * Adapt for Slate 0.15 136 | 137 | ### 0.5.0 138 | 139 | * Pressing Ctrl+A / Cmd+A in a code block, select only the text in 140 | the block 141 | 142 | ### 0.4.0 143 | 144 | * Add schema to normalize code blocks 145 | 146 | ### 0.3.0 147 | 148 | * Pressing Tab with a extended selection will indent all lines in the 149 | selection 150 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ This repository is archived and has moved to [GitBook's fork](https://github.com/GitbookIO/slate) of [ianstormtaylor/slate](https://github.com/ianstormtaylor/slate). 2 | > Previous versions are still [available on NPM](https://www.npmjs.com/package/slate-edit-table) 3 | > All the versions using GitBook's fork of slate are now published under the `@gitbook` NPM scope. 4 | > To learn more about why we forked Slate, read [our manifest](https://github.com/GitbookIO/slate/blob/master/Forked.md) 5 | 6 | # slate-edit-code 7 | 8 | [![NPM version](https://badge.fury.io/js/slate-edit-code.svg)](http://badge.fury.io/js/slate-edit-code) 9 | [![Linux Build Status](https://travis-ci.org/GitbookIO/slate-edit-code.png?branch=master)](https://travis-ci.org/GitbookIO/slate-edit-code) 10 | 11 | A Slate plugin to handle code block editing. 12 | 13 | ### Install 14 | 15 | ```js 16 | npm install slate-edit-code 17 | ``` 18 | 19 | ### Features 20 | 21 | - Pressing Enter insert a new line starting with the right indentation 22 | - Pressing Tab insert the right indentation if selection is collapsed or indent all lines in selection 23 | - Pressing Delete remove the indentation before cursor if possible 24 | - Pressing Mod+Enter exits the code block 25 | - Pressing Mod+A selects all the text in the block 26 | 27 | > Mod means Ctrl on Windows/Linux and Command on Mac. 28 | 29 | ### Structure 30 | 31 | This plugin uses the following structure for code blocks: 32 | 33 | ```html 34 | 35 | A code block is made of 36 | several code lines 37 | 38 | ``` 39 | 40 | Texts inside `code_blocks` that contain newlines `\n` are automatically split into the appropriate number of `code_lines`. 41 | 42 | 43 | ### Simple Usage 44 | 45 | ```js 46 | import EditCode from 'slate-edit-code' 47 | 48 | const plugins = [ 49 | EditCode() 50 | ] 51 | ``` 52 | 53 | #### Options arguments 54 | 55 | - `containerType = 'code_block' : string` — The type of the code containers 56 | - `lineType = 'code_line' : string` — The type of the code lines 57 | - `exitBlockType = 'paragraph' : null | string` — Mod+Enter will exit the code container, into the given block type. Backspace at start of an empty code container will convert it to the given block type. Pass `null` to disable this behavior. 58 | - `onExit: (Change) => void | Change` — Change to do when the user hits Mod+Enter. Defaults to exiting the code block, into a new `exitBlockType` block. 59 | - `selectAll = true : boolean` — True to select all code inside a code container on Mod+A 60 | - `allowMarks = false : boolean` — False disallow marks in code blocks by normalizing them away. 61 | - `getIndent: (Value) => string` — Returns the indent unit as a string. The current value is passed as context. 62 | 63 | #### Suppressing onKeyDown behavior 64 | 65 | Some behavior implemented by this plugins have no corresponding option. While there is an option `selectAll` to disable the behavior on `Mod+A`, If you would like to fine tune these behavior, you can always redefine the exported `onKeyDown` function. 66 | 67 | The following example disable all indent behavior 68 | 69 | ```js 70 | import EditCode from 'slate-edit-code' 71 | 72 | const options = { ... }; 73 | 74 | const basePlugin = EditCode(options); 75 | 76 | const customPlugin = { 77 | ...basePlugin, 78 | onKeyDown(event, change, editor) { 79 | if (event.key === 'Tab') { 80 | // Bypass the original plugin behavior on `Tab` 81 | return; 82 | } else { 83 | return basePlugin.onKeyDown(event, change, editor); 84 | } 85 | } 86 | } 87 | 88 | // Use customPlugin later on 89 | ``` 90 | 91 | ### Utilities and Changes 92 | 93 | `slate-edit-code` exports utilities, accessible like so: 94 | 95 | ``` js 96 | const plugin = EditCode() 97 | 98 | // Access exported utilities there 99 | plugin.utils 100 | ``` 101 | 102 | #### `utils.deserializeCode` 103 | 104 | `plugin.utils.deserializeCode(text: String) => Block` 105 | 106 | Split a text string into lines, and deserialize them to a `code_container` `Block`, with one children `code_line` `Block` per line. 107 | 108 | 109 | #### `changes.toggleCodeBlock` 110 | 111 | `plugin.changes.toggleCodeBlock(change: Change, type: String) => Change` 112 | 113 | Toggle a block into a code block or a normal block (defined by `type`). 114 | 115 | #### `changes.wrapCodeBlockByKey` 116 | 117 | `plugin.changes.wrapCodeBlockByKey(change: Change, key: String) => Change` 118 | 119 | Convert a block (paragraph, etc) into a code block. 120 | 121 | #### `changes.wrapCodeBlock` 122 | 123 | `plugin.changes.wrapCodeBlock(change: Change) => Change` 124 | 125 | Convert current block (paragraph, etc) into a code block. 126 | 127 | #### `changes.unwrapCodeBlockByKey` 128 | `plugin.changes.unwrapCodeBlockByKey(change: Change, key: String, type: String) => Change` 129 | 130 | Convert a code block into a normal block (paragraph, etc). 131 | 132 | #### `changes.unwrapCodeBlock` 133 | 134 | `plugin.changes.unwrapCodeBlock(change: Change, type: String) => Change` 135 | 136 | Convert current code block into a normal block (paragraph, etc). 137 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Slate • Code Edition 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/main.css: -------------------------------------------------------------------------------- 1 | #example { 2 | width: 100%; 3 | max-width: 600px; 4 | margin: 20px auto; 5 | } 6 | 7 | #example .code { 8 | background: #f5f5f5; 9 | padding: 5px; 10 | margin-bottom: 20px; 11 | } 12 | 13 | #example pre { 14 | background: #f5f5f5; 15 | margin: 0; 16 | } 17 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* global document */ 4 | import * as React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { Editor } from '@gitbook/slate-react'; 7 | import PluginEditCode from '../lib/'; 8 | 9 | import INITIAL_VALUE from './value'; 10 | 11 | const plugin = PluginEditCode(); 12 | const plugins = [plugin]; 13 | 14 | function renderNode(props: *) { 15 | const { node, children, attributes } = props; 16 | 17 | switch (node.type) { 18 | case 'code_block': 19 | return ( 20 |
21 | {children} 22 |
23 | ); 24 | case 'code_line': 25 | return
{children}
; 26 | case 'paragraph': 27 | return

{children}

; 28 | case 'heading': 29 | return

{children}

; 30 | default: 31 | return null; 32 | } 33 | } 34 | 35 | class Example extends React.Component<*, *> { 36 | state = { 37 | value: INITIAL_VALUE 38 | }; 39 | 40 | onChange = ({ value }) => { 41 | this.setState({ 42 | value 43 | }); 44 | }; 45 | 46 | onToggleCode = () => { 47 | const { value } = this.state; 48 | 49 | this.onChange( 50 | plugin.changes.toggleCodeBlock(value.change(), 'paragraph').focus() 51 | ); 52 | }; 53 | 54 | render() { 55 | const { value } = this.state; 56 | 57 | return ( 58 |
59 | 64 | 71 |
72 | ); 73 | } 74 | } 75 | 76 | // $FlowFixMe 77 | ReactDOM.render(, document.getElementById('example')); 78 | -------------------------------------------------------------------------------- /example/value.js: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | // @flow 3 | // eslint-disable-next-line 4 | import { createHyperscript } from '@gitbook/slate-hyperscript'; 5 | 6 | const h = createHyperscript({ 7 | blocks: { 8 | heading: 'heading', 9 | paragraph: 'paragraph', 10 | code_block: 'code_block', 11 | code_line: 'code_line' 12 | } 13 | }); 14 | 15 | const value = ( 16 | 17 | 18 | Slate + Code Editing 19 | 20 | { 21 | 'This page is a basic example of Slate + slate-edit-code plugin. Press Tab to indent code. Shift+Tab to unindent. Press Enter to carry indentation onto the newline. Press Mod (Cmd on Mac, Ctrl on Windows) + Enter to exit the code block.' 22 | } 23 | 24 | 25 | {'// Some JavaScript'} 26 | {'function hello() {'} 27 | {" console.log('Hello World')"} 28 | {'}'} 29 | 30 | End paragraph 31 | 32 | 33 | ); 34 | 35 | export default value; 36 | -------------------------------------------------------------------------------- /lib/changes/dedentLines.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import type Options from '../options'; 5 | 6 | // Return the index of the first character that differs between both string, or 7 | // the smallest string length otherwise. 8 | function firstDifferentCharacter(a: string, b: string): number { 9 | if (a.length > b.length) { 10 | return firstDifferentCharacter(b, a); 11 | } 12 | 13 | const indexes = Array(a.length) 14 | .fill() 15 | .map((v, i) => i); 16 | const index = indexes.find(i => a[i] !== b[i]); 17 | 18 | return index == null ? a.length : index; 19 | } 20 | 21 | /** 22 | * Dedent all lines in selection 23 | */ 24 | function dedentLines( 25 | opts: Options, 26 | change: Change, 27 | // Indent to remove 28 | indent: string 29 | ): Change { 30 | const { value } = change; 31 | const { document, selection } = value; 32 | const lines = document 33 | .getBlocksAtRange(selection) 34 | .filter(node => node.type === opts.lineType); 35 | 36 | return lines.reduce((c, line) => { 37 | // Remove a level of indent from the start of line 38 | const textNode = line.nodes.first(); 39 | const lengthToRemove = firstDifferentCharacter(textNode.text, indent); 40 | return c.removeTextByKey(textNode.key, 0, lengthToRemove); 41 | }, change); 42 | } 43 | 44 | export default dedentLines; 45 | -------------------------------------------------------------------------------- /lib/changes/indentLines.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import type Options from '../options'; 5 | 6 | /** 7 | * Indent all lines in selection 8 | */ 9 | function indentLines( 10 | opts: Options, 11 | change: Change, 12 | // Indent to add 13 | indent: string 14 | ): Change { 15 | const { value } = change; 16 | const { document, selection } = value; 17 | const lines = document 18 | .getBlocksAtRange(selection) 19 | .filter(node => node.type === opts.lineType); 20 | 21 | return lines.reduce((c, line) => { 22 | // Insert an indent at start of line 23 | const text = line.nodes.first(); 24 | return c.insertTextByKey(text.key, 0, indent); 25 | }, change); 26 | } 27 | 28 | export default indentLines; 29 | -------------------------------------------------------------------------------- /lib/changes/index.js: -------------------------------------------------------------------------------- 1 | import dedentLines from './dedentLines'; 2 | import indentLines from './indentLines'; 3 | import toggleCodeBlock from './toggleCodeBlock'; 4 | import unwrapCodeBlock from './unwrapCodeBlock'; 5 | import unwrapCodeBlockByKey from './unwrapCodeBlockByKey'; 6 | import wrapCodeBlock from './wrapCodeBlock'; 7 | import wrapCodeBlockByKey from './wrapCodeBlockByKey'; 8 | 9 | export { 10 | dedentLines, 11 | indentLines, 12 | toggleCodeBlock, 13 | unwrapCodeBlock, 14 | unwrapCodeBlockByKey, 15 | wrapCodeBlock, 16 | wrapCodeBlockByKey 17 | }; 18 | -------------------------------------------------------------------------------- /lib/changes/toggleCodeBlock.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import type Options from '../options'; 5 | import { isInCodeBlock } from '../utils'; 6 | 7 | import wrapCodeBlock from './wrapCodeBlock'; 8 | import unwrapCodeBlock from './unwrapCodeBlock'; 9 | 10 | /** 11 | * Toggle code block / paragraph. 12 | */ 13 | function toggleCodeBlock( 14 | opts: Options, 15 | change: Change, 16 | // When toggling a code block off, type to convert to 17 | type: string 18 | ): Change { 19 | if (isInCodeBlock(opts, change.value)) { 20 | return unwrapCodeBlock(opts, change, type); 21 | } 22 | return wrapCodeBlock(opts, change); 23 | } 24 | 25 | export default toggleCodeBlock; 26 | -------------------------------------------------------------------------------- /lib/changes/unwrapCodeBlock.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import type Options from '../options'; 5 | import { getCurrentCode } from '../utils'; 6 | 7 | import unwrapCodeBlockByKey from './unwrapCodeBlockByKey'; 8 | 9 | /** 10 | * Convert a code block to a normal block. 11 | */ 12 | function unwrapCodeBlock(opts: Options, change: Change, type: string): Change { 13 | const { value } = change; 14 | 15 | const codeBlock = getCurrentCode(opts, value); 16 | 17 | if (!codeBlock) { 18 | return change; 19 | } 20 | 21 | // Convert to paragraph 22 | unwrapCodeBlockByKey(opts, change, codeBlock.key, type); 23 | 24 | return change; 25 | } 26 | 27 | export default unwrapCodeBlock; 28 | -------------------------------------------------------------------------------- /lib/changes/unwrapCodeBlockByKey.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import type Options from '../options'; 5 | 6 | /** 7 | * Unwrap a code block into a normal block. 8 | */ 9 | function unwrapCodeBlockByKey( 10 | opts: Options, 11 | change: Change, 12 | key: string, 13 | type: string 14 | ): Change { 15 | const { value } = change; 16 | const { document } = value; 17 | 18 | // Get the code block 19 | const codeBlock = document.getDescendant(key); 20 | 21 | if (!codeBlock || codeBlock.type != opts.containerType) { 22 | throw new Error( 23 | 'Block passed to unwrapCodeBlockByKey should be a code block container' 24 | ); 25 | } 26 | 27 | // change lines into paragraph 28 | codeBlock.nodes.forEach(line => 29 | change 30 | .setNodeByKey(line.key, { type }, { normalize: false }) 31 | .unwrapNodeByKey(line.key, { normalize: false }) 32 | ); 33 | 34 | return change; 35 | } 36 | 37 | export default unwrapCodeBlockByKey; 38 | -------------------------------------------------------------------------------- /lib/changes/wrapCodeBlock.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import type Options from '../options'; 5 | 6 | import wrapCodeBlockByKey from './wrapCodeBlockByKey'; 7 | 8 | /** 9 | * Wrap current block into a code block. 10 | */ 11 | function wrapCodeBlock(opts: Options, change: Change): Change { 12 | const { value } = change; 13 | const { startBlock, selection } = value; 14 | 15 | // Convert to code block 16 | wrapCodeBlockByKey(opts, change, startBlock.key); 17 | 18 | // Move selection back in the block 19 | change 20 | .collapseToStartOf(change.value.document.getDescendant(startBlock.key)) 21 | .moveOffsetsTo(selection.startOffset); 22 | 23 | return change; 24 | } 25 | 26 | export default wrapCodeBlock; 27 | -------------------------------------------------------------------------------- /lib/changes/wrapCodeBlockByKey.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import type Options from '../options'; 5 | import { deserializeCode } from '../utils'; 6 | 7 | /** 8 | * Wrap a block into a code block. 9 | */ 10 | function wrapCodeBlockByKey( 11 | opts: Options, 12 | change: Change, 13 | key: string 14 | ): Change { 15 | const { value } = change; 16 | const { document } = value; 17 | 18 | const startBlock = document.getDescendant(key); 19 | const text = startBlock.text; 20 | 21 | // Remove all child 22 | startBlock.nodes.forEach(node => { 23 | change.removeNodeByKey(node.key, { normalize: false }); 24 | }); 25 | 26 | // Insert new text 27 | const toInsert = deserializeCode(opts, text); 28 | 29 | toInsert.nodes.forEach((node, i) => { 30 | change.insertNodeByKey(startBlock.key, i, node, { normalize: false }); 31 | }); 32 | 33 | // Set node type 34 | change.setNodeByKey(startBlock.key, { 35 | type: opts.containerType 36 | }); 37 | 38 | return change; 39 | } 40 | 41 | export default wrapCodeBlockByKey; 42 | -------------------------------------------------------------------------------- /lib/core.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Options, { type OptionsFormat } from './options'; 3 | import { deserializeCode, isInCodeBlock } from './utils'; 4 | import { 5 | wrapCodeBlockByKey, 6 | unwrapCodeBlockByKey, 7 | wrapCodeBlock, 8 | unwrapCodeBlock, 9 | toggleCodeBlock 10 | } from './changes'; 11 | 12 | import { schema } from './validation'; 13 | 14 | /** 15 | * The core of the plugin, which does not relies on `slate-react`, and includes 16 | * everything but behavior and rendering logic. 17 | */ 18 | function core(optsParam: OptionsFormat): Object { 19 | const opts = new Options(optsParam); 20 | 21 | return { 22 | schema: schema(opts), 23 | 24 | changes: { 25 | unwrapCodeBlockByKey: unwrapCodeBlockByKey.bind(null, opts), 26 | wrapCodeBlockByKey: wrapCodeBlockByKey.bind(null, opts), 27 | wrapCodeBlock: wrapCodeBlock.bind(null, opts), 28 | unwrapCodeBlock: unwrapCodeBlock.bind(null, opts), 29 | toggleCodeBlock: toggleCodeBlock.bind(null, opts) 30 | }, 31 | 32 | utils: { 33 | isInCodeBlock: isInCodeBlock.bind(null, opts), 34 | deserializeCode: deserializeCode.bind(null, opts) 35 | } 36 | }; 37 | } 38 | 39 | export default core; 40 | -------------------------------------------------------------------------------- /lib/handlers/index.js: -------------------------------------------------------------------------------- 1 | import onTab from './onTab'; 2 | import onShiftTab from './onShiftTab'; 3 | import onEnter from './onEnter'; 4 | import onModEnter from './onModEnter'; 5 | import onBackspace from './onBackspace'; 6 | import onSelectAll from './onSelectAll'; 7 | import onPaste from './onPaste'; 8 | import onKeyDown from './onKeyDown'; 9 | 10 | export { 11 | onTab, 12 | onShiftTab, 13 | onEnter, 14 | onModEnter, 15 | onBackspace, 16 | onSelectAll, 17 | onPaste, 18 | onKeyDown 19 | }; 20 | -------------------------------------------------------------------------------- /lib/handlers/onBackspace.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { type Change } from '@gitbook/slate'; 4 | import endsWith from 'ends-with'; 5 | 6 | import { getCurrentIndent, getCurrentCode } from '../utils'; 7 | import type Options from '../options'; 8 | 9 | /** 10 | * User pressed Delete in an editor: 11 | * Remove last idnentation before cursor 12 | */ 13 | function onBackspace( 14 | opts: Options, 15 | event: *, 16 | change: Change, 17 | editor: * 18 | ): void | Change { 19 | const { value } = change; 20 | if (value.isExpanded) { 21 | return undefined; 22 | } 23 | 24 | const { startOffset, startText } = value; 25 | 26 | const currentLine = value.startBlock; 27 | 28 | // Detect and remove indentation at cursor 29 | const indent = getCurrentIndent(opts, value); 30 | const beforeSelection = currentLine.text.slice(0, startOffset); 31 | 32 | // If the line before selection ending with the indentation? 33 | if (endsWith(beforeSelection, indent)) { 34 | // Remove indent 35 | event.preventDefault(); 36 | 37 | return change.deleteBackward(indent.length).focus(); 38 | } else if (opts.exitBlockType) { 39 | // Otherwise check if we are in an empty code container... 40 | const currentCode = getCurrentCode(opts, value); 41 | const isStartOfCode = 42 | startOffset === 0 && currentCode.getFirstText() === startText; 43 | // PERF: avoid checking for whole currentCode.text 44 | const isEmpty = 45 | currentCode.nodes.size === 1 && currentLine.text.length === 0; 46 | 47 | if (isStartOfCode && isEmpty) { 48 | event.preventDefault(); 49 | // Convert it to default exit type 50 | return change 51 | .setBlocks(opts.exitBlockType, { normalize: false }) 52 | .unwrapNodeByKey(currentLine.key); 53 | } 54 | } 55 | return undefined; 56 | } 57 | 58 | export default onBackspace; 59 | -------------------------------------------------------------------------------- /lib/handlers/onEnter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import { getIndent } from '../utils'; 5 | import type Options from '../options'; 6 | 7 | /** 8 | * User pressed Enter in an editor: 9 | * Insert a new code line and start it with the indentation from previous line 10 | */ 11 | function onEnter( 12 | opts: Options, 13 | event: *, 14 | change: Change, 15 | editor: * 16 | ): void | Change { 17 | const { value } = change; 18 | if (!value.isCollapsed) { 19 | return undefined; 20 | } 21 | 22 | event.preventDefault(); 23 | 24 | const { startBlock } = value; 25 | const currentLineText = startBlock.text; 26 | const indent = getIndent(currentLineText, ''); 27 | 28 | return change 29 | .splitBlock() 30 | .insertText(indent) 31 | .focus(); 32 | } 33 | 34 | export default onEnter; 35 | -------------------------------------------------------------------------------- /lib/handlers/onKeyDown.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isKeyHotkey } from 'is-hotkey'; 3 | import { type Change } from '@gitbook/slate'; 4 | 5 | import { getCurrentCode } from '../utils'; 6 | import type Options from '../options'; 7 | 8 | import onTab from './onTab'; 9 | import onShiftTab from './onShiftTab'; 10 | import onEnter from './onEnter'; 11 | import onModEnter from './onModEnter'; 12 | import onBackspace from './onBackspace'; 13 | import onSelectAll from './onSelectAll'; 14 | 15 | const isModA = isKeyHotkey('mod+a'); 16 | const isShiftTab = isKeyHotkey('shift+tab'); 17 | const isTab = isKeyHotkey('tab'); 18 | const isModEnter = isKeyHotkey('mod+enter'); 19 | const isEnter = isKeyHotkey('enter'); 20 | const isBackspace = isKeyHotkey('backspace'); 21 | 22 | /** 23 | * User is pressing a key in the editor 24 | */ 25 | function onKeyDown( 26 | opts: Options, 27 | event: *, 28 | change: Change, 29 | editor: * 30 | ): void | Change { 31 | const { value } = change; 32 | const currentCode = getCurrentCode(opts, value); 33 | 34 | // Inside code ? 35 | if (!currentCode) { 36 | return undefined; 37 | } 38 | 39 | // Add opts in the argument list 40 | const args = [opts, event, change, editor]; 41 | 42 | // Select all the code in the block (Mod+a) 43 | if (opts.selectAll && isModA(event)) { 44 | return onSelectAll(...args); 45 | } else if (isShiftTab(event)) { 46 | // User is pressing Shift+Tab 47 | return onShiftTab(...args); 48 | } else if (isTab(event)) { 49 | // User is pressing Tab 50 | return onTab(...args); 51 | } else if (opts.exitBlockType && isModEnter(event)) { 52 | // User is pressing Mod+Enter 53 | return onModEnter(...args); 54 | } else if (isEnter(event)) { 55 | // User is pressing Enter 56 | return onEnter(...args); 57 | } else if (isBackspace(event)) { 58 | // User is pressing Backspace 59 | return onBackspace(...args); 60 | } 61 | return undefined; 62 | } 63 | 64 | export default onKeyDown; 65 | -------------------------------------------------------------------------------- /lib/handlers/onModEnter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import type Options from '../options'; 5 | 6 | /** 7 | * User pressed Mod+Enter in an editor 8 | * Exit the current code block 9 | */ 10 | function onModEnter( 11 | opts: Options, 12 | event: *, 13 | change: Change, 14 | editor: * 15 | ): void | Change { 16 | const { value } = change; 17 | if (!value.isCollapsed) { 18 | return undefined; 19 | } 20 | 21 | event.preventDefault(); 22 | 23 | // Exit the code block 24 | return opts.resolvedOnExit(change); 25 | } 26 | 27 | export default onModEnter; 28 | -------------------------------------------------------------------------------- /lib/handlers/onPaste.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Document, type Change } from '@gitbook/slate'; 3 | import { getEventTransfer } from '@gitbook/slate-react'; 4 | import { getCurrentCode, deserializeCode } from '../utils'; 5 | import type Options from '../options'; 6 | 7 | /** 8 | * User is pasting content, insert it as text 9 | */ 10 | function onPaste( 11 | opts: Options, 12 | event: *, 13 | change: Change, 14 | editor: * 15 | ): void | Change { 16 | const { value } = change; 17 | const data = getEventTransfer(event); 18 | const currentCode = getCurrentCode(opts, value); 19 | 20 | // Only handle paste when selection is completely a code block 21 | const { endBlock } = value; 22 | if (!currentCode || !currentCode.hasDescendant(endBlock.key)) { 23 | return undefined; 24 | } 25 | 26 | // Convert to text if needed 27 | let text; 28 | if (data.type === 'fragment') { 29 | text = data.fragment 30 | .getTexts() 31 | .map(t => t.text) 32 | .join('\n'); 33 | } else { 34 | text = data.text; 35 | } 36 | 37 | // Convert the text to code lines 38 | const lines = deserializeCode(opts, text).nodes; 39 | 40 | const fragment = Document.create({ nodes: lines }); 41 | 42 | return change.insertFragment(fragment); 43 | } 44 | 45 | export default onPaste; 46 | -------------------------------------------------------------------------------- /lib/handlers/onSelectAll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import { getCurrentCode } from '../utils'; 5 | import type Options from '../options'; 6 | 7 | /** 8 | * User is Cmd+A to select all text 9 | */ 10 | function onSelectAll( 11 | opts: Options, 12 | event: *, 13 | change: Change, 14 | editor: * 15 | ): void | Change { 16 | const { value } = change; 17 | event.preventDefault(); 18 | 19 | const currentCode = getCurrentCode(opts, value); 20 | return change 21 | .collapseToStartOf(currentCode.getFirstText()) 22 | .extendToEndOf(currentCode.getLastText()); 23 | } 24 | 25 | export default onSelectAll; 26 | -------------------------------------------------------------------------------- /lib/handlers/onShiftTab.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | import { getCurrentIndent } from '../utils'; 4 | import { dedentLines } from '../changes'; 5 | import type Options from '../options'; 6 | 7 | /** 8 | * User pressed Shift+Tab in an editor: 9 | * Reduce indentation in the selected lines. 10 | */ 11 | function onShiftTab( 12 | opts: Options, 13 | event: *, 14 | change: Change, 15 | editor: * 16 | ): void | Change { 17 | const { value } = change; 18 | event.preventDefault(); 19 | event.stopPropagation(); 20 | 21 | const indent = getCurrentIndent(opts, value); 22 | 23 | // We dedent all selected lines 24 | return dedentLines(opts, change, indent); 25 | } 26 | 27 | export default onShiftTab; 28 | -------------------------------------------------------------------------------- /lib/handlers/onTab.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change } from '@gitbook/slate'; 3 | 4 | import { getCurrentIndent } from '../utils'; 5 | import { indentLines } from '../changes'; 6 | 7 | import type Options from '../options'; 8 | 9 | /** 10 | * User pressed Tab in an editor: 11 | * Insert a tab after detecting it from code block content. 12 | */ 13 | function onTab( 14 | opts: Options, 15 | event: *, 16 | change: Change, 17 | editor: * 18 | ): void | Change { 19 | const { value } = change; 20 | event.preventDefault(); 21 | event.stopPropagation(); 22 | 23 | const { isCollapsed } = value; 24 | const indent = getCurrentIndent(opts, value); 25 | 26 | // Selection is collapsed, we just insert an indent at cursor 27 | if (isCollapsed) { 28 | return change.insertText(indent).focus(); 29 | } 30 | 31 | // We indent all selected lines 32 | return indentLines(opts, change, indent); 33 | } 34 | 35 | export default onTab; 36 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Options, { type OptionsFormat } from './options'; 3 | import { onKeyDown, onPaste } from './handlers'; 4 | import core from './core'; 5 | 6 | /** 7 | * A Slate plugin to handle keyboard events in code blocks. 8 | */ 9 | 10 | function EditCode(optsParam?: OptionsFormat = {}): Object { 11 | const opts = new Options(optsParam); 12 | 13 | const corePlugin = core(opts); 14 | return { 15 | ...corePlugin, 16 | 17 | onKeyDown: onKeyDown.bind(null, opts), 18 | onPaste: onPaste.bind(null, opts) 19 | }; 20 | } 21 | 22 | export default EditCode; 23 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Change, type Value, Block, Text } from '@gitbook/slate'; 3 | import { Record } from 'immutable'; 4 | 5 | const DEFAULTS = { 6 | containerType: 'code_block', 7 | lineType: 'code_line', 8 | exitBlockType: 'paragraph', 9 | selectAll: true, 10 | allowMarks: false, 11 | getIndent: null, 12 | onExit: null 13 | }; 14 | 15 | /** 16 | * The plugin options container 17 | */ 18 | class Options extends Record(DEFAULTS) { 19 | containerType: string; 20 | lineType: string; 21 | exitBlockType: string; 22 | selectAll: boolean; 23 | allowMarks: boolean; 24 | getIndent: ?(Value) => string; 25 | onExit: ?(Change) => ?Change; 26 | 27 | resolvedOnExit(change: Change): ?Change { 28 | if (this.onExit) { 29 | // Custom onExit option 30 | return this.onExit(change); 31 | } 32 | // Default behavior: insert an exit block 33 | const range = change.value.selection; 34 | 35 | const exitBlock = Block.create({ 36 | type: this.exitBlockType, 37 | nodes: [Text.create()] 38 | }); 39 | 40 | change.deleteAtRange(range, { normalize: false }); 41 | change.insertBlockAtRange(change.value.selection, exitBlock, { 42 | normalize: false 43 | }); 44 | // Exit the code block 45 | change.unwrapNodeByKey(exitBlock.key); 46 | 47 | return change.collapseToStartOf(exitBlock); 48 | } 49 | } 50 | 51 | export type OptionsFormat = { 52 | // Type of the code containers 53 | containerType?: string, 54 | // Type of the code lines 55 | lineType?: string, 56 | 57 | // Mod+Enter will exit the code container, into the given block type. 58 | // Backspace at start of empty code container, will turn it into the given block type. 59 | exitBlockType?: string, 60 | // Should the plugin handle the select all inside a code container 61 | selectAll?: boolean, 62 | // Allow marks inside code blocks 63 | allowMarks?: boolean, 64 | // Returns the indent unit to use at the given selection, as a string 65 | getIndent?: Value => string, 66 | // Custom exit handler 67 | // exitBlockType option is useless if onExit is provided 68 | onExit?: Change => Change 69 | }; 70 | 71 | export default Options; 72 | -------------------------------------------------------------------------------- /lib/utils/deserializeCode.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Block, Text } from '@gitbook/slate'; 3 | import { List } from 'immutable'; 4 | import detectNewline from 'detect-newline'; 5 | 6 | import type Options from '../options'; 7 | 8 | const DEFAULT_NEWLINE = '\n'; 9 | 10 | /** 11 | * Deserialize a text into a code block 12 | */ 13 | function deserializeCode(opts: Options, text: string): Block { 14 | const sep = detectNewline(text) || DEFAULT_NEWLINE; 15 | 16 | const lines = List(text.split(sep)).map(line => 17 | Block.create({ 18 | type: opts.lineType, 19 | nodes: [Text.create(line)] 20 | }) 21 | ); 22 | 23 | const code = Block.create({ 24 | type: opts.containerType, 25 | nodes: lines 26 | }); 27 | 28 | return code; 29 | } 30 | 31 | export default deserializeCode; 32 | -------------------------------------------------------------------------------- /lib/utils/getCurrentCode.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Value, type Block } from '@gitbook/slate'; 3 | 4 | import type Options from '../options'; 5 | 6 | /** 7 | * Return the current code block, from current selection or from a node key. 8 | */ 9 | function getCurrentCode(opts: Options, value: Value, key?: string): ?Block { 10 | const { document } = value; 11 | 12 | let currentBlock; 13 | if (key) { 14 | currentBlock = value.document.getDescendant(key); 15 | } else { 16 | if (!value.selection.startKey) return null; 17 | currentBlock = value.startBlock; 18 | } 19 | 20 | // The structure is always code_block -> code_line -> text 21 | // So the parent of the currentBlock should be the code_block 22 | const parent = document.getParent(currentBlock.key); 23 | if (parent && parent.type === opts.containerType) { 24 | return parent; 25 | } 26 | return null; 27 | } 28 | 29 | export default getCurrentCode; 30 | -------------------------------------------------------------------------------- /lib/utils/getCurrentIndent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { type Value } from '@gitbook/slate'; 4 | 5 | import type Options from '../options'; 6 | import getIndent from './getIndent'; 7 | import getCurrentCode from './getCurrentCode'; 8 | 9 | /** 10 | * Detect indentation in the current code block 11 | */ 12 | function getCurrentIndent(opts: Options, value: Value): string { 13 | if (opts.getIndent) { 14 | return opts.getIndent(value); 15 | } 16 | 17 | const currentCode = getCurrentCode(opts, value); 18 | if (!currentCode) { 19 | return ''; 20 | } 21 | 22 | const text = currentCode 23 | .getTexts() 24 | .map(t => t.text) 25 | .join('\n'); 26 | return getIndent(text); 27 | } 28 | 29 | export default getCurrentIndent; 30 | -------------------------------------------------------------------------------- /lib/utils/getIndent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import detectIndent from 'detect-indent'; 3 | 4 | const DEFAULT_INDENTATION = ' '; 5 | 6 | /** 7 | * Detect indentation in a text 8 | */ 9 | function getIndent( 10 | text: string, 11 | defaultValue?: string = DEFAULT_INDENTATION 12 | ): string { 13 | return detectIndent(text).indent || defaultValue; 14 | } 15 | 16 | export default getIndent; 17 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | import deserializeCode from './deserializeCode'; 2 | import getCurrentCode from './getCurrentCode'; 3 | import getCurrentIndent from './getCurrentIndent'; 4 | import getIndent from './getIndent'; 5 | import isInCodeBlock from './isInCodeBlock'; 6 | 7 | export { 8 | deserializeCode, 9 | getCurrentCode, 10 | getCurrentIndent, 11 | getIndent, 12 | isInCodeBlock 13 | }; 14 | -------------------------------------------------------------------------------- /lib/utils/isInCodeBlock.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { type Value } from '@gitbook/slate'; 4 | 5 | import type Options from '../options'; 6 | 7 | /** 8 | * Test if current selection is in a code block. 9 | */ 10 | function isInCodeBlock(opts: Options, value: Value): boolean { 11 | const { document, startKey } = value; 12 | const codeBlock = document.getClosest( 13 | startKey, 14 | block => block.type === opts.containerType 15 | ); 16 | 17 | return Boolean(codeBlock); 18 | } 19 | 20 | export default isInCodeBlock; 21 | -------------------------------------------------------------------------------- /lib/validation/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema'; 2 | 3 | export { schema }; 4 | -------------------------------------------------------------------------------- /lib/validation/schema.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Block, type Change, type Node } from '@gitbook/slate'; 4 | import { 5 | CHILD_TYPE_INVALID, 6 | CHILD_INVALID, 7 | PARENT_TYPE_INVALID, 8 | PARENT_INVALID, 9 | CHILD_OBJECT_INVALID 10 | } from '@gitbook/slate-schema-violations'; 11 | import { List } from 'immutable'; 12 | 13 | import type Options from '../options'; 14 | import { deserializeCode } from '../utils'; 15 | 16 | /** 17 | * Create a schema definition with rules to normalize code blocks 18 | */ 19 | function schema(opts: Options): Object { 20 | const baseSchema = { 21 | blocks: { 22 | [opts.containerType]: { 23 | nodes: [{ types: [opts.lineType] }], 24 | normalize(change: Change, violation: string, context: Object) { 25 | switch (violation) { 26 | case CHILD_INVALID: 27 | case CHILD_TYPE_INVALID: 28 | return onlyLine(opts, change, context); 29 | default: 30 | return undefined; 31 | } 32 | } 33 | }, 34 | [opts.lineType]: { 35 | nodes: [{ objects: ['text'], min: 1 }], 36 | parent: { types: [opts.containerType] }, 37 | normalize(change: Change, violation: string, context: Object) { 38 | switch (violation) { 39 | // This constant does not exist yet in 40 | // official Slate, but exists in GitBook's 41 | // fork. Until the PR is merged, we accept both 42 | // https://github.com/ianstormtaylor/slate/pull/1842 43 | case PARENT_INVALID: 44 | case PARENT_TYPE_INVALID: 45 | return noOrphanLine(opts, change, context); 46 | case CHILD_INVALID: 47 | case CHILD_OBJECT_INVALID: 48 | return onlyTextInCode(opts, change, context); 49 | default: 50 | return undefined; 51 | } 52 | } 53 | } 54 | } 55 | }; 56 | 57 | if (!opts.allowMarks) { 58 | baseSchema.blocks[opts.lineType].marks = []; 59 | } 60 | 61 | return baseSchema; 62 | } 63 | 64 | /** 65 | * Return a list of group of nodes matching the given filter. 66 | */ 67 | function getSuccessiveNodes( 68 | nodes: List, 69 | match: Node => boolean 70 | ): List> { 71 | const nonLines = nodes.takeUntil(match); 72 | const afterNonLines = nodes.skip(nonLines.size); 73 | if (afterNonLines.isEmpty()) { 74 | return List(); 75 | } 76 | 77 | const firstGroup = afterNonLines.takeWhile(match); 78 | const restOfNodes = afterNonLines.skip(firstGroup.size); 79 | 80 | return List([firstGroup]).concat(getSuccessiveNodes(restOfNodes, match)); 81 | } 82 | 83 | /** 84 | * A rule that ensure code blocks only contain lines of code, and no marks 85 | */ 86 | function onlyLine(opts: Options, change: Change, context: Object) { 87 | const isNotLine = n => n.type !== opts.lineType; 88 | const nonLineGroups = getSuccessiveNodes(context.node.nodes, isNotLine); 89 | 90 | nonLineGroups.filter(group => !group.isEmpty()).forEach(nonLineGroup => { 91 | // Convert text to code lines 92 | const text = nonLineGroup.map(n => n.text).join(''); 93 | const codeLines = deserializeCode(opts, text).nodes; 94 | 95 | // Insert them in place of the invalid node 96 | const first = nonLineGroup.first(); 97 | const parent = change.value.document.getParent(first.key); 98 | const invalidNodeIndex = parent.nodes.indexOf(first); 99 | 100 | codeLines.forEach((codeLine, index) => { 101 | change.insertNodeByKey( 102 | parent.key, 103 | invalidNodeIndex + index, 104 | codeLine, 105 | { 106 | normalize: false 107 | } 108 | ); 109 | }); 110 | 111 | // Remove the block 112 | nonLineGroup.forEach(n => 113 | change.removeNodeByKey(n.key, { normalize: false }) 114 | ); 115 | }); 116 | 117 | return change; 118 | } 119 | 120 | /** 121 | * A rule that ensure code lines only contain text 122 | */ 123 | function onlyTextInCode( 124 | opts: Options, 125 | change: Change, 126 | context: Object 127 | ): ?Change { 128 | const { node } = context; 129 | 130 | if (node.object === 'inline' || node.object === 'block') { 131 | node.nodes.forEach(child => { 132 | change.unwrapNodeByKey(child.key, { normalize: false }); 133 | }); 134 | 135 | return change; 136 | } 137 | 138 | return undefined; 139 | } 140 | 141 | /** 142 | * A rule that ensure code lines are always children 143 | * of a code block. 144 | */ 145 | function noOrphanLine(opts: Options, change: Change, context: Object): ?Change { 146 | const { parent } = context; 147 | 148 | const isLine = n => n.type === opts.lineType; 149 | 150 | const linesGroup = getSuccessiveNodes(parent.nodes, isLine); 151 | 152 | linesGroup.forEach(group => { 153 | const container = Block.create({ type: opts.containerType, nodes: [] }); 154 | const firstLineIndex = parent.nodes.indexOf(group.first()); 155 | 156 | change.insertNodeByKey(parent.key, firstLineIndex, container, { 157 | normalize: false 158 | }); 159 | 160 | group.forEach((line, index) => 161 | change.moveNodeByKey(line.key, container.key, index, { 162 | normalize: false 163 | }) 164 | ); 165 | }); 166 | } 167 | 168 | export default schema; 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gitbook/slate-edit-code", 3 | "description": "A Slate plugin to handle code blocks editing.", 4 | "version": "0.15.7", 5 | "license": "Apache-2.0", 6 | "repository": "git://github.com/GitbookIO/slate-edit-code.git", 7 | "main": "./dist/index.js", 8 | "dependencies": { 9 | "detect-indent": "^4.0.0", 10 | "detect-newline": "^2.1.0", 11 | "ends-with": "^0.2.0", 12 | "is-hotkey": "^0.1.1" 13 | }, 14 | "peerDependencies": { 15 | "immutable": "^3.8.2 || ^4.0.0-rc.12", 16 | "@gitbook/slate": "^0.34.7", 17 | "@gitbook/slate-react": "^0.13.4", 18 | "@gitbook/slate-schema-violations": "^0.1.20" 19 | }, 20 | "devDependencies": { 21 | "babel-cli": "^6.26.0", 22 | "babel-core": "^6.26.0", 23 | "babel-eslint": "^8.0.2", 24 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 25 | "babel-preset-es2015": "^6.24.1", 26 | "babel-preset-react": "^6.24.1", 27 | "babel-preset-stage-0": "^6.24.1", 28 | "babelify": "^8.0.0", 29 | "browserify": "^13.3.0", 30 | "eslint": "^4.10.0", 31 | "eslint-config-gitbook": "^2.0.3", 32 | "eslint-plugin-import": "^2.8.0", 33 | "eslint-plugin-prettier": "^2.6.0", 34 | "expect": "^1.20.2", 35 | "flow-bin": "^0.57.3", 36 | "gh-pages": "^0.11.0", 37 | "http-server": "^0.9.0", 38 | "immutable": "^4.0.0-rc.12", 39 | "mocha": "^3.0.1", 40 | "react": "^15.3.0", 41 | "react-dom": "^15.3.0", 42 | "prettier": "^1.13.3", 43 | "@gitbook/slate": "^0.34.11", 44 | "slate-hyperprint": "^2.2.4", 45 | "@gitbook/slate-hyperscript": "^0.5.26", 46 | "@gitbook/slate-react": "^0.13.9", 47 | "@gitbook/slate-schema-violations": "^0.1.24" 48 | }, 49 | "scripts": { 50 | "prepublish": "babel ./lib --out-dir ./dist", 51 | "lint": "eslint ./", 52 | "test": "./node_modules/.bin/mocha ./tests/all.js --compilers js:babel-register --reporter=list", 53 | "postpublish": "npm run deploy-example", 54 | "build-example": "browserify ./example/main.js -o ./example/bundle.js -t [ babelify --presets [ es2015 react stage-0 ] ]", 55 | "serve-example": "http-server ./example/ -p 8081", 56 | "start": "npm run build-example && npm run serve-example", 57 | "deploy-example": "npm run build-example && gh-pages -d ./example" 58 | }, 59 | "keywords": [ 60 | "slate" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /tests/all.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /* eslint-disable import/no-dynamic-require */ 3 | import expect from 'expect'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import Slate from '@gitbook/slate'; 7 | import hyperprint from 'slate-hyperprint'; 8 | import EditCode from '../lib'; 9 | 10 | const PLUGIN = EditCode(); 11 | const SCHEMA = Slate.Schema.create({ 12 | plugins: [PLUGIN] 13 | }); 14 | 15 | function deserializeValue(value) { 16 | return Slate.Value.fromJS( 17 | { 18 | document: value.document, 19 | selection: value.selection, 20 | schema: SCHEMA 21 | }, 22 | { normalize: false } 23 | ); 24 | } 25 | 26 | describe('slate-edit-code', () => { 27 | const tests = fs.readdirSync(__dirname); 28 | 29 | tests.forEach(test => { 30 | if (test[0] === '.' || path.extname(test).length > 0) return; 31 | 32 | it(test, () => { 33 | Slate.resetKeyGenerator(); 34 | const dir = path.resolve(__dirname, test); 35 | const input = require(path.resolve(dir, 'input.js')).default; 36 | const expectedPath = path.resolve(dir, 'expected.js'); 37 | const expected = 38 | fs.existsSync(expectedPath) && require(expectedPath).default; 39 | 40 | const runChange = require(path.resolve(dir, 'change.js')).default; 41 | 42 | const valueInput = deserializeValue(input); 43 | 44 | const newChange = runChange(PLUGIN, valueInput.change()); 45 | 46 | if (expected) { 47 | const newDoc = hyperprint(newChange.value.document, { 48 | strict: true 49 | }); 50 | expect(newDoc).toEqual( 51 | hyperprint(expected.document, { strict: true }) 52 | ); 53 | } 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/backspace-empty-code/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('backspace'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/backspace-empty-code/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /tests/backspace-empty-code/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/backspace-start/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('backspace'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/backspace-start/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/backspace-start/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | {' '} 10 | 11 | Line 1 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /tests/enter-end-offset/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('enter'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/enter-end-offset/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Some code 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /tests/enter-end-offset/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Some code 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /tests/enter-mid-offset/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('enter'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/enter-mid-offset/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Some 9 | code 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /tests/enter-mid-offset/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Some 10 | code 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /tests/enter-start-offset/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('enter'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/enter-start-offset/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Some code 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /tests/enter-start-offset/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Some code 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/enter-with-indent/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('enter'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/enter-with-indent/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | 10 | {' '} 11 | Li 12 | 13 | 14 | {' '} 15 | ne 2 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /tests/enter-with-indent/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | 10 | {' '} 11 | Li 12 | 13 | ne 2 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /tests/hyperscript.js: -------------------------------------------------------------------------------- 1 | import { createHyperscript } from '@gitbook/slate-hyperscript'; 2 | 3 | const h = createHyperscript({ 4 | blocks: { 5 | code_block: 'code_block', 6 | code_line: 'code_line', 7 | paragraph: 'paragraph', 8 | default: 'default' 9 | }, 10 | inlines: { 11 | link: 'link', 12 | 'unknown-inline': 'unknown-inline' 13 | }, 14 | marks: { 15 | italic: 'italic' 16 | } 17 | }); 18 | 19 | export default h; 20 | -------------------------------------------------------------------------------- /tests/isincodeblock-true/change.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | export default function(plugin, change) { 4 | assert.equal(plugin.utils.isInCodeBlock(change.value), true); 5 | 6 | return change; 7 | } 8 | -------------------------------------------------------------------------------- /tests/isincodeblock-true/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello world 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/isincodeblock-true/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello world 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/on-exit-block/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('mod+enter'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/on-exit-block/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Li 9 | 10 | 11 | 12 | ne 1 13 | 14 | {' '} 15 | Line 2 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /tests/on-exit-block/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Li 10 | 11 | ne 1 12 | 13 | 14 | {' '} 15 | Line 2 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /tests/paste-middle/change.js: -------------------------------------------------------------------------------- 1 | export default function(plugin, change) { 2 | return plugin.onPaste( 3 | { 4 | preventDefault() {}, 5 | stopPropagation() {}, 6 | clipboardData: { 7 | // Simulate a text data from IE 8 | // https://github.com/ianstormtaylor/slate/blob/master/packages/slate-react/src/utils/get-event-transfer.js#L161 9 | getData: () => 'Yes\nNo\nQuestion?' 10 | } 11 | }, 12 | change, 13 | {} 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /tests/paste-middle/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | SomeYes 9 | No 10 | Question? code 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /tests/paste-middle/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Some 10 | code 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /tests/schema-multiline-text/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-multiline-text/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | This node must be converted.multiple 9 | lines 10 | of 11 | code and marks 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /tests/schema-multiline-text/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | This node must be converted. 9 | {'multiple\nlines\nof\ncode'} 10 | and marks 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /tests/schema-no-inlines/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-no-inlines/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Some link in code 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/schema-no-inlines/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Some link in code 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | // BITE 17 | -------------------------------------------------------------------------------- /tests/schema-no-marks/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-no-marks/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Some formatted text 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/schema-no-marks/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Some formatted text 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /tests/schema-no-multiple-orphan-lines/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-no-multiple-orphan-lines/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | Line 2 10 | 11 | cut 12 | 13 | Line 3 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /tests/schema-no-multiple-orphan-lines/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | Line 1 8 | Line 2 9 | cut 10 | Line 3 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /tests/schema-no-orphan-lines-wrapped/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-no-orphan-lines-wrapped/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Line 1 10 | Line 2 11 | Line 3 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /tests/schema-no-orphan-lines-wrapped/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | Line 2 10 | Line 3 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /tests/schema-no-single-orphan-line/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-no-single-orphan-line/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Some orphan code 9 | 10 | 11 | Some other code 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /tests/schema-no-single-orphan-line/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | Some orphan code 8 | 9 | Some other code 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /tests/schema-only-lines-empty/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-only-lines-empty/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/schema-only-lines-empty/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/schema-only-lines/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-only-lines/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Hello 10 | invalid 11 | World 12 | invalid 13 | again 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /tests/schema-only-lines/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | Hello 10 | invalid 11 | World 12 | {'invalid\n again'} 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /tests/schema-only-text-join/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-only-text-join/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | One textTwo text 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/schema-only-text-join/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | One text 10 | Two text 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /tests/schema-only-text-with-blocks/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-only-text-with-blocks/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | One textTwo text 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/schema-only-text-with-blocks/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import { Value } from '@gitbook/slate'; 3 | import hyperscript from '../hyperscript'; 4 | 5 | export default Value.fromJS( 6 | { 7 | document: ( 8 | 9 | 10 | 11 | One text 12 | Unwanted text 13 | Two text 14 | 15 | 16 | 17 | ) 18 | }, 19 | { normalize: false } 20 | ); 21 | -------------------------------------------------------------------------------- /tests/schema-only-text/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-only-text/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Some 9 | {' code'} 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /tests/schema-only-text/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | {'Some\n code'} 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /tests/schema-single-text/change.js: -------------------------------------------------------------------------------- 1 | import Slate from '@gitbook/slate'; 2 | 3 | export default function(plugin, change) { 4 | const schema = new Slate.Schema(plugin.schema); 5 | return change.normalize(schema); 6 | } 7 | -------------------------------------------------------------------------------- /tests/schema-single-text/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | One textTwo textThree text 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/schema-single-text/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | One textTwo textThree text 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/select-all/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('mod+a'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/select-all/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | Line 2 10 | Line 3 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /tests/select-all/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | Line 2 10 | Line 3 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /tests/shift-tab-middle-offset/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('shift+tab'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/shift-tab-middle-offset/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | {' '} 10 | Line 1 11 | 12 | Line 2 13 | 14 | {' '} 15 | Line 3 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /tests/shift-tab-middle-offset/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | {' '} 10 | Line 1 11 | 12 | 13 | {' '}L 14 | ine 2 15 | 16 | 17 | {' '} 18 | Line 3 19 | 20 | 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /tests/shift-tab-multilines/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('shift+tab'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/shift-tab-multilines/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | Line 2 10 | 11 | {' '} 12 | Line 3 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /tests/shift-tab-multilines/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | 10 | {' '} 11 | Line 1 12 | 13 | 14 | {' '} 15 | Li 16 | 17 | ne 2 18 | 19 | 20 | {' '} 21 | Line 3 22 | 23 | 24 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /tests/simulate-key.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { parseHotkey } from 'is-hotkey'; 3 | 4 | // Returns a fake Event object for the given hotkey 5 | export default function simulateKey(hotkey: string): Event { 6 | return { 7 | preventDefault() {}, 8 | stopPropagation() {}, 9 | ...parseHotkey(hotkey, { byKey: true }) 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /tests/tab-middle-offset/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('tab'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/tab-middle-offset/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | 10 | {' '} 11 | Line 2 12 | 13 | 14 | {' '} 15 | Line 3 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /tests/tab-middle-offset/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Line 1 9 | 10 | {' '} 11 | 12 | Line 2 13 | 14 | 15 | {' '} 16 | Line 3 17 | 18 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /tests/tab-multilines/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | return plugin.onKeyDown(simulateKey('tab'), change, {}); 5 | } 6 | -------------------------------------------------------------------------------- /tests/tab-multilines/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | {' '} 10 | Line 1 11 | 12 | 13 | {' '} 14 | Line 2 15 | 16 | Line 3 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /tests/tab-multilines/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | 10 | Line 1 11 | 12 | 13 | Line 2 14 | 15 | Line 3 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /tests/tab-start-offset/change.js: -------------------------------------------------------------------------------- 1 | import simulateKey from '../simulate-key'; 2 | 3 | export default function(plugin, change) { 4 | const { value } = change; 5 | const block = value.document.findDescendant( 6 | node => node.type == 'code_block' 7 | ); 8 | 9 | change.collapseToStartOf(block).moveOffsetsTo(0); 10 | 11 | return plugin.onKeyDown(simulateKey('tab'), change, {}); 12 | } 13 | -------------------------------------------------------------------------------- /tests/tab-start-offset/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | 9 | {' '} 10 | Some code 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /tests/tab-start-offset/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Some code 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/togglecodeblock-code/change.js: -------------------------------------------------------------------------------- 1 | export default function(plugin, change) { 2 | return plugin.changes.toggleCodeBlock(change, 'paragraph'); 3 | } 4 | -------------------------------------------------------------------------------- /tests/togglecodeblock-code/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello world 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/togglecodeblock-code/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | Hello world 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /tests/togglecodeblock-normal/change.js: -------------------------------------------------------------------------------- 1 | export default function(plugin, change) { 2 | return plugin.changes.toggleCodeBlock(change, 'paragraph'); 3 | } 4 | -------------------------------------------------------------------------------- /tests/togglecodeblock-normal/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | Hello world 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /tests/togglecodeblock-normal/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello world 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/unwrapcodeblock-multi-lines/change.js: -------------------------------------------------------------------------------- 1 | export default function(plugin, change) { 2 | return plugin.changes.unwrapCodeBlock(change, 'paragraph'); 3 | } 4 | -------------------------------------------------------------------------------- /tests/unwrapcodeblock-multi-lines/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | Hello 8 | world 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /tests/unwrapcodeblock-multi-lines/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello 9 | world 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /tests/unwrapcodeblock-normal/change.js: -------------------------------------------------------------------------------- 1 | export default function(plugin, change) { 2 | return plugin.changes.unwrapCodeBlock(change, 'paragraph'); 3 | } 4 | -------------------------------------------------------------------------------- /tests/unwrapcodeblock-normal/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | Hello world 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /tests/unwrapcodeblock-normal/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello world 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/unwrapcodeblock-selection/change.js: -------------------------------------------------------------------------------- 1 | export default function(plugin, change) { 2 | const newValue = plugin.changes.unwrapCodeBlock(change, 'paragraph'); 3 | 4 | return newValue; 5 | } 6 | -------------------------------------------------------------------------------- /tests/unwrapcodeblock-selection/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | Hello world 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /tests/unwrapcodeblock-selection/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello world 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/wrapcodeblock-normal/change.js: -------------------------------------------------------------------------------- 1 | export default function(plugin, change) { 2 | return plugin.changes.wrapCodeBlock(change); 3 | } 4 | -------------------------------------------------------------------------------- /tests/wrapcodeblock-normal/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello world 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/wrapcodeblock-normal/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | Hello world 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /tests/wrapcodeblock-selection/change.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | export default function(plugin, change) { 4 | plugin.changes.wrapCodeBlock(change); 5 | 6 | assert.equal(change.value.startOffset, 5); 7 | 8 | return change; 9 | } 10 | -------------------------------------------------------------------------------- /tests/wrapcodeblock-selection/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello world 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/wrapcodeblock-selection/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello 9 | world 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /tests/wrapcodeblock-with-inline/change.js: -------------------------------------------------------------------------------- 1 | export default function(plugin, change) { 2 | return plugin.changes.wrapCodeBlock(change); 3 | } 4 | -------------------------------------------------------------------------------- /tests/wrapcodeblock-with-inline/expected.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | import hyperscript from '../hyperscript'; 3 | 4 | export default ( 5 | 6 | 7 | 8 | Hello world 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /tests/wrapcodeblock-with-inline/input.js: -------------------------------------------------------------------------------- 1 | /** @jsx hyperscript */ 2 | /* eslint-disable react/void-dom-elements-no-children */ 3 | import hyperscript from '../hyperscript'; 4 | 5 | export default ( 6 | 7 | 8 | 9 | Hello world 10 | 11 | 12 | 13 | ); 14 | --------------------------------------------------------------------------------