Skip to content

Commit

Permalink
feat(Article editor): Add support for code fragments and blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
nokome committed Oct 9, 2021
1 parent 5c0cd10 commit 3501e69
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 26 deletions.
3 changes: 3 additions & 0 deletions web/src/components/article/edit/inputRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export const articleInputRules = inputRules({
(match, node) => node.childCount + node.attrs.order === match[1]
),

// Markdown code block
textblockTypeInputRule(/^```$/, articleSchema.nodes.CodeBlock),

// Markdown quote block
wrappingInputRule(/^\s*>\s$/, articleSchema.nodes.QuoteBlock),
],
Expand Down
29 changes: 18 additions & 11 deletions web/src/components/article/edit/keymap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { articleSchema } from './schema'
* The ProseMirror `KeyMap` (i.e. key bindings) for a Stencila `Article`.
*
* Most of these bindings are based on those in https://github.com/ProseMirror/prosemirror-example-setup.
* `Mod-` is a shorthand for `Cmd-` on Mac and `Ctrl-` on other platforms.
*
* For docs and examples see:
* - https://prosemirror.net/docs/ref/#keymap
Expand All @@ -41,24 +42,30 @@ export const articleKeymap: Keymap = {
'Escape': selectParentNode,

// Toggling marks
// These are consistent with Google Docs (and others?)
'Mod-i': toggleMark(articleSchema.marks.Emphasis),
'Mod-b': toggleMark(articleSchema.marks.Strong),
'Mod-u': toggleMark(articleSchema.marks.Underline),
'Alt-Shift-5': toggleMark(articleSchema.marks.Delete),
'Mod-.': toggleMark(articleSchema.marks.Superscript),
'Mod-,': toggleMark(articleSchema.marks.Subscript),

// Changing the type of blocks
'Shift-Ctrl-0': setBlockType(articleSchema.nodes.Paragraph),
'Shift-Ctrl-1': setBlockType(articleSchema.nodes.Heading, {depth: 1}),
'Shift-Ctrl-2': setBlockType(articleSchema.nodes.Heading, {depth: 2}),
'Shift-Ctrl-3': setBlockType(articleSchema.nodes.Heading, {depth: 3}),
'Shift-Ctrl-4': setBlockType(articleSchema.nodes.Heading, {depth: 4}),
'Shift-Ctrl-5': setBlockType(articleSchema.nodes.Heading, {depth: 5}),
'Shift-Ctrl-6': setBlockType(articleSchema.nodes.Heading, {depth: 6}),

'Shift-Mod-0': setBlockType(articleSchema.nodes.Paragraph),
'Shift-Mod-1': setBlockType(articleSchema.nodes.Heading, {depth: 1}),
'Shift-Mod-2': setBlockType(articleSchema.nodes.Heading, {depth: 2}),
'Shift-Mod-3': setBlockType(articleSchema.nodes.Heading, {depth: 3}),
'Shift-Mod-4': setBlockType(articleSchema.nodes.Heading, {depth: 4}),
'Shift-Mod-5': setBlockType(articleSchema.nodes.Heading, {depth: 5}),
'Shift-Mod-6': setBlockType(articleSchema.nodes.Heading, {depth: 6}),
'Shift-Mod-\\': setBlockType(articleSchema.nodes.CodeBlock),

// Wrapping blocks in another type
'Ctrl->': wrapIn(articleSchema.nodes.QuoteBlock),
'Mod->': wrapIn(articleSchema.nodes.QuoteBlock),

// List creation / manipulation
'Shift-Ctrl-8': wrapInList(articleSchema.nodes.List, {order: 'Unordered'}),
'Shift-Ctrl-9': wrapInList(articleSchema.nodes.List, {order: 'Ascending'}),
'Shift-Mod-8': wrapInList(articleSchema.nodes.List, {order: 'Unordered'}),
'Shift-Mod-9': wrapInList(articleSchema.nodes.List, {order: 'Ascending'}),
'Enter': splitListItem(articleSchema.nodes.ListItem),
'Mod-[': liftListItem(articleSchema.nodes.ListItem),
'Mod-]': sinkListItem(articleSchema.nodes.ListItem)
Expand Down
81 changes: 68 additions & 13 deletions web/src/components/article/edit/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { codeBlock } from '@stencila/schema'
import {
AttributeSpec,
MarkSpec,
Expand Down Expand Up @@ -57,11 +56,12 @@ export const articleSchema = new Schema({
TableHeader: tableHeader(),
ThematicBreak: thematicBreak(),

// Inline content types, starting with `text` (equivalent of Stencila `String`), the default
// Inline content types, starting with `text` (equivalent of Stencila `String`),
// the default inline node type
text: {
group: 'InlineContent',
},
CodeFragment: inline('CodeFragment', 'code', 'text*'),
CodeFragment: codeFragment(),
},

marks: {
Expand Down Expand Up @@ -166,7 +166,7 @@ function heading(): NodeSpec {
],
toDOM(node) {
return [
'h' + (node.attrs.depth + 1),
`h${node.attrs.depth + 1}`,
{ itemtype: 'http://schema.stenci.la/Heading', itemscope: '' },
0,
]
Expand Down Expand Up @@ -211,7 +211,7 @@ function listItem(): NodeSpec {
return {
content: 'Paragraph BlockContent*',
parseDOM: [{ tag: 'li' }],
toDOM(node) {
toDOM(_node) {
return [
'li',
{ itemtype: 'http://schema.org/ListItem', itemscope: '' },
Expand All @@ -224,7 +224,9 @@ function listItem(): NodeSpec {
/**
* Generate a `NodeSpec` to represent a Stencila `CodeBlock`
*
* This is temporary and wil be replaced with a CodeMirror editor.
* This is temporary and wil be replaced with a CodeMirror editor
* (see https://prosemirror.net/examples/codemirror/ and https://gist.github.com/BrianHung/08146f89ea903f893946963570263040).
*
* Based on https://github.com/ProseMirror/prosemirror-schema-basic/blob/b5ae707ab1be98a1d8735dfdc7d1845bcd126f18/src/schema-basic.js#L59
*/
function codeBlock(): NodeSpec {
Expand All @@ -233,19 +235,72 @@ function codeBlock(): NodeSpec {
content: 'text*',
contentProp: 'text',
marks: '',
attrs: {
programmingLanguage: { default: '' },
},
code: true,
defining: true,
parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
parseDOM: [
{
tag: 'pre',
preserveWhitespace: 'full',
getAttrs(dom) {
const elem = dom as HTMLElement
return {
programmingLanguage:
elem
.querySelector('meta[itemprop="programmingLanguage"][content]')
?.getAttribute('content') ??
elem
.querySelector('code[class^="language-"]')
?.getAttribute('class')
?.substring(9),
}
},
},
],
toDOM(node) {
return [
'pre',
{ itemtype: 'http://schema.stenci.la/CodeBlock', itemscope: '' },
{
itemtype: 'http://schema.stenci.la/CodeBlock',
itemscope: '',
// This is just for inspection that language is parsed properly
title: node.attrs.programmingLanguage,
},
['code', 0],
]
},
}
}

/**
* Generate a `NodeSpec` to represent a Stencila `CodeFragment`
*
* This is temporary and wil be replaced with a CodeMirror editor (as with `CodeBlock`)
*/
function codeFragment(): NodeSpec {
return {
inline: true,
group: 'InlineContent',
content: 'text*',
contentProp: 'text',
marks: '',
attrs: {
programmingLanguage: { default: '' },
},
code: true,
parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
toDOM(_node) {
return [
'code',
{ itemtype: 'http://schema.stenci.la/CodeFragment', itemscope: '' },
0,
]
},
}
}

/**
* Generate a `NodeSpec` to represent a Stencila `Table`
*
Expand All @@ -262,7 +317,7 @@ function table(): NodeSpec {
tableRole: 'table',
isolating: true,
parseDOM: [{ tag: 'table' }],
toDOM(node) {
toDOM(_node) {
return [
'table',
{ itemtype: 'http://schema.org/Table', itemscope: '' },
Expand All @@ -281,7 +336,7 @@ function tableRow(): NodeSpec {
contentProp: 'cells',
tableRole: 'row',
parseDOM: [{ tag: 'tr' }],
toDOM(node) {
toDOM(_node) {
return [
'tr',
{ itemtype: 'http://schema.stenci.la/TableRow', itemscope: '' },
Expand All @@ -305,7 +360,7 @@ function tableCellAttrsSpec(): Record<string, AttributeSpec> {
/**
* Get `TableCell` attributes as part of `parseDOM`
*/
function tableCellAttrsGet(dom: HTMLElement) {
function tableCellAttrsGet(dom: HTMLElement): Record<string, any> {
const widthAttr = dom.getAttribute('data-colwidth')
const widths =
widthAttr && /^\d+(,\d+)*$/.test(widthAttr)
Expand Down Expand Up @@ -386,7 +441,7 @@ function thematicBreak(): NodeSpec {
return {
group: 'BlockContent',
parseDOM: [{ tag: 'hr' }],
toDOM(node) {
toDOM(_node) {
return [
'hr',
{ itemtype: 'http://schema.stenci.la/ThematicBreak', itemscope: '' },
Expand Down Expand Up @@ -430,7 +485,7 @@ function inline(
function mark(name: string, tag: string, parseDOM?: ParseRule[]): MarkSpec {
return {
parseDOM: parseDOM ?? [{ tag }],
toDOM(node) {
toDOM(_node) {
return [tag, { itemtype: name, itemscope: '' }, 0]
},
}
Expand Down
4 changes: 2 additions & 2 deletions web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const main = (
// During development it's very useful to see the patch operations being sent
if (process.env.NODE_ENV !== 'production') {
const { actor, target, ops } = patch
console.log('Sending patch:', JSON.stringify({ actor, target }))
console.log('📢 Sending patch:', JSON.stringify({ actor, target }))
for (const op of ops) console.log(' ', JSON.stringify(op))
}

Expand Down Expand Up @@ -176,7 +176,7 @@ export const main = (

// During development it's useful to see which patches are being received
if (process.env.NODE_ENV !== 'production') {
console.log('Received DOM patch:', JSON.stringify({ actor, target }))
console.log('📩 Received DOM patch:', JSON.stringify({ actor, target }))
for (const op of ops) console.log(' ', JSON.stringify(op))
}

Expand Down

0 comments on commit 3501e69

Please sign in to comment.