Merge branch 'main' of github.com:ueberdosis/tiptap-next into main

This commit is contained in:
Hans Pagel
2020-11-23 15:52:00 +01:00
28 changed files with 189 additions and 92 deletions

View File

@@ -63,6 +63,8 @@ module.exports = {
'@typescript-eslint/no-unused-vars': ['error'], '@typescript-eslint/no-unused-vars': ['error'],
'no-use-before-define': 'off', 'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error'], '@typescript-eslint/no-use-before-define': ['error'],
'no-dupe-class-members': 'off',
'@typescript-eslint/no-dupe-class-members': ['error'],
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/explicit-module-boundary-type': 'off', '@typescript-eslint/explicit-module-boundary-type': 'off',

View File

@@ -1,4 +1,5 @@
const path = require('path') const path = require('path')
const visit = require('unist-util-visit')
function addStyleResource(rule) { function addStyleResource(rule) {
rule.use('style-resource') rule.use('style-resource')
@@ -10,6 +11,28 @@ function addStyleResource(rule) {
}) })
} }
function tableWrapper() {
return async tree => {
visit(
tree,
'table',
(node, index, parent) => {
if (node.type === 'table' && parent.type === 'root') {
const original = { ...node }
node.type = 'div'
node.children = [original]
node.data = {
hProperties: {
class: 'table-wrapper',
},
}
}
},
)
}
}
module.exports = { module.exports = {
siteName: 'tiptap', siteName: 'tiptap',
titleTemplate: '%s | tiptap', titleTemplate: '%s | tiptap',
@@ -26,6 +49,7 @@ module.exports = {
'@gridsome/remark-prismjs', '@gridsome/remark-prismjs',
'remark-container', 'remark-container',
'remark-toc', 'remark-toc',
tableWrapper,
], ],
remark: { remark: {
autolinkHeadings: { autolinkHeadings: {

View File

@@ -100,6 +100,10 @@ export default {
}, },
githubUrl() { githubUrl() {
if (process.env.NODE_ENV === 'development') {
return `vscode://file${this.cwd}/src/demos/${this.name}/${this.files[0].name}`
}
return `https://github.com/ueberdosis/tiptap-next/tree/main/docs/src/demos/${this.name}` return `https://github.com/ueberdosis/tiptap-next/tree/main/docs/src/demos/${this.name}`
}, },
}, },

View File

@@ -13,7 +13,7 @@
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }"> <button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
code code
</button> </button>
<button @click="editor.chain().focus().unsetMarks().run()"> <button @click="editor.chain().focus().unsetAllMarks().run()">
clear marks clear marks
</button> </button>
<button @click="editor.chain().focus().clearNodes().run()"> <button @click="editor.chain().focus().clearNodes().run()">

View File

@@ -13,7 +13,7 @@
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }"> <button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
code code
</button> </button>
<button @click="editor.chain().focus().unsetMarks().run()"> <button @click="editor.chain().focus().unsetAllMarks().run()">
clear marks clear marks
</button> </button>
<button @click="editor.chain().focus().clearNodes().run()"> <button @click="editor.chain().focus().clearNodes().run()">

View File

@@ -13,7 +13,7 @@
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }"> <button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
code code
</button> </button>
<button @click="editor.chain().focus().unsetMarks().run()"> <button @click="editor.chain().focus().unsetAllMarks().run()">
clear marks clear marks
</button> </button>
<button @click="editor.chain().focus().clearNodes().run()"> <button @click="editor.chain().focus().clearNodes().run()">

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div v-if="editor"> <div v-if="editor">
<button @click="editor.chain().focus().unsetMarks().run()"> <button @click="editor.chain().focus().unsetAllMarks().run()">
clear formatting clear formatting
</button> </button>
<button @click="editor.chain().focus().undo().run()"> <button @click="editor.chain().focus().undo().run()">

View File

@@ -9,7 +9,7 @@ const MenuBar = () => {
return ( return (
<> <>
<button onClick={() => editor.chain().focus().unsetMarks().run()}> <button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
Clear formatting Clear formatting
</button> </button>
<button <button

View File

@@ -110,14 +110,14 @@ Have a look at all of the core commands listed below. They should give you a goo
| .extendMarkRange() | Extends the text selection to the current mark. | | .extendMarkRange() | Extends the text selection to the current mark. |
| .resetNodeAttributes() | Resets all node attributes to the default value. | | .resetNodeAttributes() | Resets all node attributes to the default value. |
| .selectParentNode() | Select the parent node. | | .selectParentNode() | Select the parent node. |
| .setBlockType() | Replace a given range with a node. |
| .setMark() | Add a mark with new attributes. | | .setMark() | Add a mark with new attributes. |
| .setNode() | Replace a given range with a node. |
| .splitBlock() | Forks a new node from an existing node. | | .splitBlock() | Forks a new node from an existing node. |
| .toggleBlockType() | Toggle a node with another node. |
| .toggleMark() | Toggle a mark on and off. | | .toggleMark() | Toggle a mark on and off. |
| .toggleNode() | Toggle a node with another node. |
| .toggleWrap() | Wraps nodes in another node, or removes an existing wrap. | | .toggleWrap() | Wraps nodes in another node, or removes an existing wrap. |
| .unsetAllMarks() | Remove all marks in the current selection. |
| .unsetMark() | Remove a mark in the current selection. | | .unsetMark() | Remove a mark in the current selection. |
| .unsetMarks() | Remove all marks in the current selection. |
| .updateNodeAttributes() | Update attributes of a node. | | .updateNodeAttributes() | Update attributes of a node. |
### Lists ### Lists

View File

@@ -100,6 +100,33 @@ new Editor({
| `false` | Disables autofocus. | | `false` | Disables autofocus. |
| `null` | Disables autofocus. | | `null` | Disables autofocus. |
### Enable input rules
By default, tiptap enables all [input rules](/guide/build-custom-extensions/#input-rules). With `enableInputRules` you can disable that.
```js
import { Editor } from '@tiptap/core'
import { defaultExtensions } from '@tiptap/starter-kit'
new Editor({
content: `<p>Example Text</p>`,
extensions: defaultExtensions(),
enableInputRules: false,
})
```
### Enable paste rules
By default, tiptap enables all [paste rules](/guide/build-custom-extensions/#paste-rules). With `enablePasteRules` you can disable that.
```js
import { Editor } from '@tiptap/core'
import { defaultExtensions } from '@tiptap/starter-kit'
new Editor({
content: `<p>Example Text</p>`,
extensions: defaultExtensions(),
enablePasteRules: false,
})
```
### Inject CSS ### Inject CSS
By default, tiptap injects [a little bit of CSS](https://github.com/ueberdosis/tiptap-next/tree/main/packages/core/src/style.ts). With `injectCSS` you can disable that. By default, tiptap injects [a little bit of CSS](https://github.com/ueberdosis/tiptap-next/tree/main/packages/core/src/style.ts). With `injectCSS` you can disable that.

View File

@@ -275,7 +275,7 @@ const CustomParagraph = Paragraph.extend({
addCommands() { addCommands() {
return { return {
paragraph: () => ({ commands }) => { paragraph: () => ({ commands }) => {
return commands.toggleBlockType('paragraph', 'paragraph') return commands.toggleNode('paragraph', 'paragraph')
}, },
} }
}, },

View File

@@ -82,7 +82,7 @@
> p code, > p code,
> ul code, > ul code,
> ol code, > ol code,
> table code, > .table-wrapper code,
> .remark-container code { > .remark-container code {
color: $colorYellow; color: $colorYellow;
background-color: rgba($colorYellow, 0.1); background-color: rgba($colorYellow, 0.1);
@@ -92,7 +92,7 @@
> p a, > p a,
> ul a, > ul a,
> ol a, > ol a,
> table a, > .table-wrapper a,
> .remark-container a { > .remark-container a {
text-decoration: underline; text-decoration: underline;
@@ -187,6 +187,12 @@
} }
} }
> .table-wrapper {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
> table { > table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -196,6 +202,7 @@
th, th,
td { td {
padding: 0.5rem; padding: 0.5rem;
min-width: 8rem;
&:first-child { &:first-child {
padding-left: 0; padding-left: 0;
@@ -207,6 +214,7 @@
} }
th { th {
white-space: nowrap;
color: $colorWhite; color: $colorWhite;
font-weight: 500; font-weight: 500;
border-bottom: 1px solid rgba($colorWhite, 0.2); border-bottom: 1px solid rgba($colorWhite, 0.2);
@@ -230,6 +238,7 @@
} }
} }
} }
}
> .remark-container { > .remark-container {
padding: 1.25rem; padding: 1.25rem;

View File

@@ -14,7 +14,12 @@ import createStyleTag from './utils/createStyleTag'
import CommandManager from './CommandManager' import CommandManager from './CommandManager'
import ExtensionManager from './ExtensionManager' import ExtensionManager from './ExtensionManager'
import EventEmitter from './EventEmitter' import EventEmitter from './EventEmitter'
import { EditorOptions, EditorContent, CommandSpec } from './types' import {
EditorOptions,
EditorContent,
CommandSpec,
EditorSelection,
} from './types'
import * as extensions from './extensions' import * as extensions from './extensions'
import style from './style' import style from './style'
@@ -39,7 +44,7 @@ export class Editor extends EventEmitter {
public view!: EditorView public view!: EditorView
public selection = { from: 0, to: 0 } public selection: EditorSelection = { from: 0, to: 0 }
public isFocused = false public isFocused = false
@@ -52,6 +57,8 @@ export class Editor extends EventEmitter {
editable: true, editable: true,
editorProps: {}, editorProps: {},
parseOptions: {}, parseOptions: {},
enableInputRules: true,
enablePasteRules: true,
onInit: () => null, onInit: () => null,
onUpdate: () => null, onUpdate: () => null,
onTransaction: () => null, onTransaction: () => null,

View File

@@ -1,4 +1,3 @@
import { Plugin } from 'prosemirror-state'
import { keymap } from 'prosemirror-keymap' import { keymap } from 'prosemirror-keymap'
import { Schema, Node as ProsemirrorNode } from 'prosemirror-model' import { Schema, Node as ProsemirrorNode } from 'prosemirror-model'
import { inputRules } from 'prosemirror-inputrules' import { inputRules } from 'prosemirror-inputrules'
@@ -37,7 +36,7 @@ export default class ExtensionManager {
}) })
} }
get plugins(): Plugin[] { get plugins() {
const plugins = this.extensions const plugins = this.extensions
.map(extension => { .map(extension => {
const context = { const context = {
@@ -58,7 +57,11 @@ export default class ExtensionManager {
] ]
} }
get inputRules(): any { get inputRules() {
if (!this.editor.options.enableInputRules) {
return []
}
return this.extensions return this.extensions
.map(extension => { .map(extension => {
const context = { const context = {
@@ -72,7 +75,11 @@ export default class ExtensionManager {
.flat() .flat()
} }
get pasteRules(): any { get pasteRules() {
if (!this.editor.options.enablePasteRules) {
return []
}
return this.extensions return this.extensions
.map(extension => { .map(extension => {
const context = { const context = {

View File

@@ -1,16 +1,10 @@
import { TextSelection } from 'prosemirror-state' import { EditorState, TextSelection } from 'prosemirror-state'
import { Editor } from '../Editor'
import { Command, FocusPosition } from '../types' import { Command, FocusPosition } from '../types'
import minMax from '../utils/minMax' import minMax from '../utils/minMax'
interface ResolvedSelection { function resolveSelection(state: EditorState, position: FocusPosition = null) {
from: number, if (!position) {
to: number, return null
}
function resolveSelection(editor: Editor, position: FocusPosition = null): ResolvedSelection {
if (position === null) {
return editor.selection
} }
if (position === 'start' || position === true) { if (position === 'start' || position === true) {
@@ -21,17 +15,17 @@ function resolveSelection(editor: Editor, position: FocusPosition = null): Resol
} }
if (position === 'end') { if (position === 'end') {
const { size } = editor.state.doc.content const { size } = state.doc.content
return { return {
from: size, from: size,
to: size - 1, // TODO: -1 only for nodes with content to: size,
} }
} }
return { return {
from: position as number, from: position,
to: position as number, to: position,
} }
} }
@@ -48,7 +42,7 @@ export const focus = (position: FocusPosition = null): Command => ({
return true return true
} }
const { from, to } = resolveSelection(editor, position) const { from, to } = resolveSelection(editor.state, position) || editor.selection
const { doc } = tr const { doc } = tr
const resolvedFrom = minMax(from, 0, doc.content.size) const resolvedFrom = minMax(from, 0, doc.content.size)
const resolvedEnd = minMax(to, 0, doc.content.size) const resolvedEnd = minMax(to, 0, doc.content.size)

View File

@@ -1,13 +0,0 @@
import { NodeType } from 'prosemirror-model'
import { setBlockType as originalSetBlockType } from 'prosemirror-commands'
import { Command } from '../types'
import getNodeType from '../utils/getNodeType'
/**
* Replace a given range with a node.
*/
export const setBlockType = (typeOrName: string | NodeType, attrs = {}): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return originalSetBlockType(type, attrs)(state, dispatch)
}

View File

@@ -0,0 +1,13 @@
import { NodeType } from 'prosemirror-model'
import { setBlockType } from 'prosemirror-commands'
import { Command } from '../types'
import getNodeType from '../utils/getNodeType'
/**
* Replace a given range with a node.
*/
export const setNode = (typeOrName: string | NodeType, attrs = {}): Command => ({ state, dispatch }) => {
const type = getNodeType(typeOrName, state.schema)
return setBlockType(type, attrs)(state, dispatch)
}

View File

@@ -6,14 +6,14 @@ import getNodeType from '../utils/getNodeType'
/** /**
* Toggle a node with another node. * Toggle a node with another node.
*/ */
export const toggleBlockType = (typeOrName: string | NodeType, toggleTypeOrName: string | NodeType, attrs = {}): Command => ({ state, commands }) => { export const toggleNode = (typeOrName: string | NodeType, toggleTypeOrName: string | NodeType, attrs = {}): Command => ({ state, commands }) => {
const type = getNodeType(typeOrName, state.schema) const type = getNodeType(typeOrName, state.schema)
const toggleType = getNodeType(toggleTypeOrName, state.schema) const toggleType = getNodeType(toggleTypeOrName, state.schema)
const isActive = nodeIsActive(state, type, attrs) const isActive = nodeIsActive(state, type, attrs)
if (isActive) { if (isActive) {
return commands.setBlockType(toggleType) return commands.setNode(toggleType)
} }
return commands.setBlockType(type, attrs) return commands.setNode(type, attrs)
} }

View File

@@ -3,7 +3,7 @@ import { Command } from '../types'
/** /**
* Remove all marks in the current selection. * Remove all marks in the current selection.
*/ */
export const unsetMarks = (): Command => ({ tr, state, dispatch }) => { export const unsetAllMarks = (): Command => ({ tr, state, dispatch }) => {
const { selection } = tr const { selection } = tr
const { from, to, empty } = selection const { from, to, empty } = selection

View File

@@ -15,18 +15,18 @@ import * as resetNodeAttributes from '../commands/resetNodeAttributes'
import * as scrollIntoView from '../commands/scrollIntoView' import * as scrollIntoView from '../commands/scrollIntoView'
import * as selectAll from '../commands/selectAll' import * as selectAll from '../commands/selectAll'
import * as selectParentNode from '../commands/selectParentNode' import * as selectParentNode from '../commands/selectParentNode'
import * as setBlockType from '../commands/setBlockType'
import * as setContent from '../commands/setContent' import * as setContent from '../commands/setContent'
import * as setMark from '../commands/setMark' import * as setMark from '../commands/setMark'
import * as setNode from '../commands/setNode'
import * as sinkListItem from '../commands/sinkListItem' import * as sinkListItem from '../commands/sinkListItem'
import * as splitBlock from '../commands/splitBlock' import * as splitBlock from '../commands/splitBlock'
import * as splitListItem from '../commands/splitListItem' import * as splitListItem from '../commands/splitListItem'
import * as toggleBlockType from '../commands/toggleBlockType'
import * as toggleList from '../commands/toggleList' import * as toggleList from '../commands/toggleList'
import * as toggleMark from '../commands/toggleMark' import * as toggleMark from '../commands/toggleMark'
import * as toggleNode from '../commands/toggleNode'
import * as toggleWrap from '../commands/toggleWrap' import * as toggleWrap from '../commands/toggleWrap'
import * as unsetAllMarks from '../commands/unsetAllMarks'
import * as unsetMark from '../commands/unsetMark' import * as unsetMark from '../commands/unsetMark'
import * as unsetMarks from '../commands/unsetMarks'
import * as updateNodeAttributes from '../commands/updateNodeAttributes' import * as updateNodeAttributes from '../commands/updateNodeAttributes'
import * as wrapIn from '../commands/wrapIn' import * as wrapIn from '../commands/wrapIn'
import * as wrapInList from '../commands/wrapInList' import * as wrapInList from '../commands/wrapInList'
@@ -50,18 +50,18 @@ export const Commands = Extension.create({
...scrollIntoView, ...scrollIntoView,
...selectAll, ...selectAll,
...selectParentNode, ...selectParentNode,
...setBlockType,
...setContent, ...setContent,
...setMark, ...setMark,
...setNode,
...sinkListItem, ...sinkListItem,
...splitBlock, ...splitBlock,
...splitListItem, ...splitListItem,
...toggleBlockType,
...toggleList, ...toggleList,
...toggleMark, ...toggleMark,
...toggleNode,
...toggleWrap, ...toggleWrap,
...unsetMark, ...unsetMark,
...unsetMarks, ...unsetAllMarks,
...updateNodeAttributes, ...updateNodeAttributes,
...wrapIn, ...wrapIn,
...wrapInList, ...wrapInList,

View File

@@ -23,6 +23,8 @@ export interface EditorOptions {
editable: boolean, editable: boolean,
editorProps: EditorProps, editorProps: EditorProps,
parseOptions: ParseOptions, parseOptions: ParseOptions,
enableInputRules: boolean,
enablePasteRules: boolean,
onInit: () => void, onInit: () => void,
onUpdate: () => void, onUpdate: () => void,
onTransaction: (props: { transaction: Transaction }) => void, onTransaction: (props: { transaction: Transaction }) => void,
@@ -30,6 +32,11 @@ export interface EditorOptions {
onBlur: (props: { event: FocusEvent }) => void, onBlur: (props: { event: FocusEvent }) => void,
} }
export type EditorSelection = {
from: number,
to: number,
}
export type EditorContent = string | JSON | null export type EditorContent = string | JSON | null
export type Command = (props: { export type Command = (props: {

View File

@@ -78,13 +78,13 @@ const CodeBlock = Node.create({
* Set a code block * Set a code block
*/ */
setCodeBlock: (attributes?: { language: string }): Command => ({ commands }) => { setCodeBlock: (attributes?: { language: string }): Command => ({ commands }) => {
return commands.setBlockType('codeBlock', attributes) return commands.setNode('codeBlock', attributes)
}, },
/** /**
* Toggle a code block * Toggle a code block
*/ */
toggleCodeBlock: (attributes?: { language: string }): Command => ({ commands }) => { toggleCodeBlock: (attributes?: { language: string }): Command => ({ commands }) => {
return commands.toggleBlockType('codeBlock', 'paragraph', attributes) return commands.toggleNode('codeBlock', 'paragraph', attributes)
}, },
} }
}, },

View File

@@ -60,7 +60,7 @@ const Heading = Node.create({
return false return false
} }
return commands.setBlockType('heading', attributes) return commands.setNode('heading', attributes)
}, },
/** /**
* Toggle a heading node * Toggle a heading node
@@ -70,7 +70,7 @@ const Heading = Node.create({
return false return false
} }
return commands.toggleBlockType('heading', 'paragraph', attributes) return commands.toggleNode('heading', 'paragraph', attributes)
}, },
} }
}, },

View File

@@ -33,7 +33,7 @@ const Paragraph = Node.create({
* Toggle a paragraph * Toggle a paragraph
*/ */
setParagraph: (): Command => ({ commands }) => { setParagraph: (): Command => ({ commands }) => {
return commands.toggleBlockType('paragraph', 'paragraph') return commands.toggleNode('paragraph', 'paragraph')
}, },
} }
}, },

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.0.0-alpha.4](https://github.com/ueberdosis/tiptap-next/compare/@tiptap/starter-kit@2.0.0-alpha.3...@tiptap/starter-kit@2.0.0-alpha.4) (2020-11-20)
**Note:** Version bump only for package @tiptap/starter-kit
# [2.0.0-alpha.3](https://github.com/ueberdosis/tiptap-next/compare/@tiptap/starter-kit@2.0.0-alpha.2...@tiptap/starter-kit@2.0.0-alpha.3) (2020-11-19) # [2.0.0-alpha.3](https://github.com/ueberdosis/tiptap-next/compare/@tiptap/starter-kit@2.0.0-alpha.2...@tiptap/starter-kit@2.0.0-alpha.3) (2020-11-19)
**Note:** Version bump only for package @tiptap/starter-kit **Note:** Version bump only for package @tiptap/starter-kit

View File

@@ -1,6 +1,6 @@
{ {
"name": "@tiptap/starter-kit", "name": "@tiptap/starter-kit",
"version": "2.0.0-alpha.3", "version": "2.0.0-alpha.4",
"homepage": "https://tiptap.dev", "homepage": "https://tiptap.dev",
"keywords": [ "keywords": [
"tiptap", "tiptap",

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.0.0-alpha.5](https://github.com/ueberdosis/tiptap-next/compare/@tiptap/vue-starter-kit@2.0.0-alpha.4...@tiptap/vue-starter-kit@2.0.0-alpha.5) (2020-11-20)
**Note:** Version bump only for package @tiptap/vue-starter-kit
# [2.0.0-alpha.4](https://github.com/ueberdosis/tiptap-next/compare/@tiptap/vue-starter-kit@2.0.0-alpha.3...@tiptap/vue-starter-kit@2.0.0-alpha.4) (2020-11-19) # [2.0.0-alpha.4](https://github.com/ueberdosis/tiptap-next/compare/@tiptap/vue-starter-kit@2.0.0-alpha.3...@tiptap/vue-starter-kit@2.0.0-alpha.4) (2020-11-19)
**Note:** Version bump only for package @tiptap/vue-starter-kit **Note:** Version bump only for package @tiptap/vue-starter-kit

View File

@@ -1,6 +1,6 @@
{ {
"name": "@tiptap/vue-starter-kit", "name": "@tiptap/vue-starter-kit",
"version": "2.0.0-alpha.4", "version": "2.0.0-alpha.5",
"homepage": "https://tiptap.dev", "homepage": "https://tiptap.dev",
"keywords": [ "keywords": [
"tiptap", "tiptap",
@@ -21,7 +21,7 @@
"dist" "dist"
], ],
"dependencies": { "dependencies": {
"@tiptap/starter-kit": "^2.0.0-alpha.3", "@tiptap/starter-kit": "^2.0.0-alpha.4",
"@tiptap/vue": "^2.0.0-alpha.3" "@tiptap/vue": "^2.0.0-alpha.3"
} }
} }