add experiment demos
This commit is contained in:
103
demos/src/Experiments/Linter/Vue/extension/Linter.ts
Normal file
103
demos/src/Experiments/Linter/Vue/extension/Linter.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
|
||||
import { Node as ProsemirrorNode } from 'prosemirror-model'
|
||||
import LinterPlugin, { Result as Issue } from './LinterPlugin'
|
||||
|
||||
interface IconDivElement extends HTMLDivElement {
|
||||
issue?: Issue
|
||||
}
|
||||
|
||||
function renderIcon(issue: Issue) {
|
||||
const icon: IconDivElement = document.createElement('div')
|
||||
|
||||
icon.className = 'lint-icon'
|
||||
icon.title = issue.message
|
||||
icon.issue = issue
|
||||
|
||||
return icon
|
||||
}
|
||||
|
||||
function runAllLinterPlugins(doc: ProsemirrorNode, plugins: Array<typeof LinterPlugin>) {
|
||||
const decorations: [any?] = []
|
||||
|
||||
const results = plugins.map(RegisteredLinterPlugin => {
|
||||
return new RegisteredLinterPlugin(doc).scan().getResults()
|
||||
}).flat()
|
||||
|
||||
results.forEach(issue => {
|
||||
decorations.push(Decoration.inline(issue.from, issue.to, {
|
||||
class: 'problem',
|
||||
}),
|
||||
Decoration.widget(issue.from, renderIcon(issue)))
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
}
|
||||
|
||||
export interface LinterOptions {
|
||||
plugins: Array<typeof LinterPlugin>,
|
||||
}
|
||||
|
||||
export const Linter = Extension.create({
|
||||
name: 'linter',
|
||||
|
||||
defaultOptions: <LinterOptions>{
|
||||
plugins: [],
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { plugins } = this.options
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('linter'),
|
||||
state: {
|
||||
init(_, { doc }) {
|
||||
return runAllLinterPlugins(doc, plugins)
|
||||
},
|
||||
apply(transaction, oldState) {
|
||||
return transaction.docChanged
|
||||
? runAllLinterPlugins(transaction.doc, plugins)
|
||||
: oldState
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state)
|
||||
},
|
||||
handleClick(view, _, event) {
|
||||
const target = (event.target as IconDivElement)
|
||||
if (/lint-icon/.test(target.className) && target.issue) {
|
||||
const { from, to } = target.issue
|
||||
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.setSelection(TextSelection.create(view.state.doc, from, to))
|
||||
.scrollIntoView(),
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
handleDoubleClick(view, _, event) {
|
||||
const target = (event.target as IconDivElement)
|
||||
if (/lint-icon/.test((event.target as HTMLElement).className) && target.issue) {
|
||||
const prob = target.issue
|
||||
|
||||
if (prob.fix) {
|
||||
prob.fix(view, prob)
|
||||
view.focus()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
35
demos/src/Experiments/Linter/Vue/extension/LinterPlugin.ts
Normal file
35
demos/src/Experiments/Linter/Vue/extension/LinterPlugin.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Node as ProsemirrorNode } from 'prosemirror-model'
|
||||
|
||||
export interface Result {
|
||||
message: string,
|
||||
from: number,
|
||||
to: number,
|
||||
fix?: Function
|
||||
}
|
||||
|
||||
export default class LinterPlugin {
|
||||
protected doc
|
||||
|
||||
private results: Array<Result> = []
|
||||
|
||||
constructor(doc: ProsemirrorNode) {
|
||||
this.doc = doc
|
||||
}
|
||||
|
||||
record(message: string, from: number, to: number, fix?: Function) {
|
||||
this.results.push({
|
||||
message,
|
||||
from,
|
||||
to,
|
||||
fix,
|
||||
})
|
||||
}
|
||||
|
||||
scan() {
|
||||
return this
|
||||
}
|
||||
|
||||
getResults() {
|
||||
return this.results
|
||||
}
|
||||
}
|
||||
8
demos/src/Experiments/Linter/Vue/extension/index.ts
Normal file
8
demos/src/Experiments/Linter/Vue/extension/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Linter } from './Linter'
|
||||
|
||||
export * from './Linter'
|
||||
export default Linter
|
||||
|
||||
export { BadWords } from './plugins/BadWords'
|
||||
export { Punctuation } from './plugins/Punctuation'
|
||||
export { HeadingLevel } from './plugins/HeadingLevel'
|
||||
@@ -0,0 +1,25 @@
|
||||
import LinterPlugin from '../LinterPlugin'
|
||||
|
||||
export class BadWords extends LinterPlugin {
|
||||
|
||||
public regex = /\b(obviously|clearly|evidently|simply)\b/ig
|
||||
|
||||
scan() {
|
||||
this.doc.descendants((node: any, position: number) => {
|
||||
if (!node.isText) {
|
||||
return
|
||||
}
|
||||
|
||||
const matches = this.regex.exec(node.text)
|
||||
|
||||
if (matches) {
|
||||
this.record(
|
||||
`Try not to say '${matches[0]}'`,
|
||||
position + matches.index, position + matches.index + matches[0].length,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import LinterPlugin, { Result as Issue } from '../LinterPlugin'
|
||||
|
||||
export class HeadingLevel extends LinterPlugin {
|
||||
fixHeader(level: number) {
|
||||
return function ({ state, dispatch }: EditorView, issue: Issue) {
|
||||
dispatch(state.tr.setNodeMarkup(issue.from - 1, undefined, { level }))
|
||||
}
|
||||
}
|
||||
|
||||
scan() {
|
||||
let lastHeadLevel: number | null = null
|
||||
|
||||
this.doc.descendants((node, position) => {
|
||||
if (node.type.name === 'heading') {
|
||||
// Check whether heading levels fit under the current level
|
||||
const { level } = node.attrs
|
||||
|
||||
if (lastHeadLevel != null && level > lastHeadLevel + 1) {
|
||||
this.record(`Heading too small (${level} under ${lastHeadLevel})`,
|
||||
position + 1, position + 1 + node.content.size,
|
||||
this.fixHeader(lastHeadLevel + 1))
|
||||
}
|
||||
lastHeadLevel = level
|
||||
}
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import LinterPlugin, { Result as Issue } from '../LinterPlugin'
|
||||
|
||||
export class Punctuation extends LinterPlugin {
|
||||
public regex = / ([,.!?:]) ?/g
|
||||
|
||||
fix(replacement: any) {
|
||||
return function ({ state, dispatch }: EditorView, issue: Issue) {
|
||||
dispatch(
|
||||
state.tr.replaceWith(
|
||||
issue.from, issue.to,
|
||||
state.schema.text(replacement),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
scan() {
|
||||
this.doc.descendants((node, position) => {
|
||||
if (!node.isText) return
|
||||
|
||||
if (!node.text) return
|
||||
|
||||
const matches = this.regex.exec(node.text)
|
||||
|
||||
if (matches) {
|
||||
this.record(
|
||||
'Suspicious spacing around punctuation',
|
||||
position + matches.index, position + matches.index + matches[0].length,
|
||||
this.fix(`${matches[1]} `),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
15
demos/src/Experiments/Linter/Vue/index.html
Normal file
15
demos/src/Experiments/Linter/Vue/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module">
|
||||
import setup from '../../../../setup/vue.ts'
|
||||
import source from '@source'
|
||||
setup('Experiments/Linter', source)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
96
demos/src/Experiments/Linter/Vue/index.vue
Normal file
96
demos/src/Experiments/Linter/Vue/index.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div>
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Heading from '@tiptap/extension-heading'
|
||||
import Linter, { BadWords, Punctuation, HeadingLevel } from './extension'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Heading,
|
||||
Text,
|
||||
Linter.configure({
|
||||
plugins: [
|
||||
BadWords,
|
||||
Punctuation,
|
||||
HeadingLevel,
|
||||
],
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<h1>
|
||||
Lint example
|
||||
</h1>
|
||||
<p>
|
||||
This is a sentence ,but the comma clearly isn't in the right place.
|
||||
</p>
|
||||
<h3>
|
||||
Too-minor header
|
||||
</h3>
|
||||
<p>
|
||||
You can hover over the icons on the right to see what the problem is, click them to select the relevant text, and, obviously, double-click them to automatically fix it (if supported).
|
||||
</ul>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.problem {
|
||||
background: #fdd;
|
||||
border-bottom: 1px solid #f22;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.lint-icon {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 100px;
|
||||
background: #f22;
|
||||
color: white;
|
||||
font-family: times, georgia, serif;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
text-align: center;
|
||||
padding-left: .5px;
|
||||
line-height: 1.1em
|
||||
}
|
||||
|
||||
.lint-icon:before {
|
||||
content: "!";
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
padding-right: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user