A library for making functional languages editable using visual blocks inside of codemirror
CodeMirror-Blocks ("CMB") is not a block editor. It's a toolkit for building block editors. In other words, it is NOT intended to be used in your IDE. ;-)
CMB should instead be included in language-specific modules, so your Python editor would include a CMB-enabled Python module. CMB provides the blockification and a11y features, and the language module provides the parser and (optionally) the vocalization and appearance for each construct in the language. The language module is what you use in your IDE.
The following language modules are available now:
If you happen to use one of those languages, you're good to go! Export the language module using the normal npm run build
mechanism, then include it in your favorite CodeMirror-enabled project. You can now use the CodeMirrorBlocks
constructor to replace an existing CodeMirror instance with blocks. In other words, you'd replace code that looks like this:
// make a new CM instance inside the container elt, passing in CM ops
this.editor = CodeMirror(container, {/* CodeMirror options */}});
With code that looks like this:
// make a new CMB instance inside the container elt, passing in CMB ops
this.editor = CodeMirrorBlocks(container, {/* CodeMirrorBlocks options */});
But if you're here, our guess is that you have a language in mind (Python, Rust, YourFavLang, etc.) and you want to allow people to hack in that language even if they need blocks, or rely on screenreaders. So how does one make a language module?
NOTE: your IDE will need to load CodeMirror as an external dependency. We assume it already does (otherwise, why would you be here?), so you'll need to provide it yourself.
Make a repo (e.g. - YourFavLang-blocks
), and include CMB as a dependency:
npm install --save codemirror-blocks
Then add folders for your parser (src/languages/YourFavLang
) and test cases (spec/languages/YourFavLang
).
Write a parser for your language that produces an Abstract Syntax Tree composed of CMB's node types.
CMB's AST nodes all have constructors that take arguments specifying (1) the from
and to
position of the node (in CodeMirror's {line, ch}
format), (2) the fields that define the node, and (3) an options
object.
The options
object is used for a number of CMB-internal purposes, but there are two (language-dependant) fields that your parser will need to set. First, you'll want to set options[aria-label]
to a short, descriptive string for how that node should be vocalized by a screenreader (e.g. - "v: a value definition"). Your parser will also be responsible for associating block- and line-comments with the node they describe. For example:
if (node instanceof structures.defVar) {
var name = parseNode(node.name);
var expr = parseNode(node.expr);
var cmnt = new Comment(cmntFrom, cmtTo, cmtText);
return new VariableDefinition(
from,
to,
name,
expr,
{'aria-label': node.name.val+': a value definition', 'comment' : comment}
);
}
You can also provide your own AST nodes, by extending the built-in AST.ASTNode
class. Here is one example of a language defining custom AST nodes.
Your subclassed Node must contain:
constructor
- Consumes thefrom
andto
locations, all required child fields, and anoptions
object initialized to{}
.spec
- A static field, which defines specifications for all fields of the node. These specifications are documented here. Note: failing to properly list all the fields of the node can leave the editor in an unstable state, and result in unspecified behavior.longDescription()
- a method that dynamically computes a detailed description of the node (optionally referring to its children, for example), and produces a string that will be read aloud to the user.pretty()
- A method that describes how the node should be pretty-printed. Pretty-printing options are documented here.render()
- A method that produces the node (usually in JSX) to be rendered. Note: all DropTargets in a node'srender()
method must declare afield
property, corresponding to one of the fields of the node that are defined inspec
. This tells CMB what part of the node is modified when the DropTarget is edited.
Create an index.js see this example file that hooks up your language to the CMB library.
CMB provides default CSS styling for all node types, but you can always add your own! Add a style.less
file that overrides the built-in styles, providing your own "look and feel" using standard CSS.
Obviously, if you've added new AST node types, you'll have to provide the styling for those yourself!
In spec/languages/YourFavLang
, add some unit tests for your parser! Make sure you test all the fields, but especially the aria-label
and longDescription()
return values.
You may find it useful to check out this example.
But maybe you're not here to make an accessible block editor for a new language. Maybe you want to hack on the CMB library itself, and help close some of our open issues!
To get your dev environment up and running, follow these steps.
-
Checkout the repository in your favorite manner
-
install dependencies with
npm
npm install
-
build the library and generate type declarations
npm run build npm run build-types
-
run tests
npm test
(Note: you may need to specify the
CHROME_BIN
environment variable to point to your installation of chrome or chromium. i.e.CHROME_BIN=/usr/bin/chromium npm test
)
CMB is written in a combination of typescript and javascript, with some language extensions (like jsx syntax) that are handled by babel. As such, before any code can actually be executed, it must be transpiled to javascript. The easiest way to do this during development is to run each of the following commands in their own terminal:
-
Continuously transpile code from the
src/
directory to thelib/
directory with babel:npm run build-watch
-
Continuously run typechecks and generate type declaration files in
lib/
with typescript:npm run build-types-watch
-
With these two processes running, you can then start the development server in yet another terminal with:
npm start
then open http://localhost:8080 in your browser to see CMB running on a web page. This web page just executes the example javascript from
example/ediotor-example.js
, but will automatically update whenever any code is changed. -
Finally, you can also run tests continuously whenever code is changed with:
npm run test-watch
To run tests, you will need either Chrome or Chromium and may need to point the CHROME_BIN
environment variable to the binary like this:
export CHROME_BIN=/usr/bin/chromium
If you don't want to type this in every terminal, you can add it to your .{bash,zsh}rc
file.
Coverage reports can be generated by setting the COVERAGE
environment variable to true
:
COVERAGE=true npm test
The reports will be written to the .coverage/
directory.