experimental linters
commit 5b31740d967bb61bfed6a2338d07e8fc6e4957b3 Author: Hans Pagel <hans.pagel@ueber.io> Date: Tue Jan 19 14:48:00 2021 +0100 refactoring, disable TS checks for now commit 6fcc5082287ba4dd5457b8ea6e6d8300efaaafb6 Author: Hans Pagel <hans.pagel@ueber.io> Date: Tue Jan 19 14:42:14 2021 +0100 move everything to a new experiments structure commit 2b5f394ad4c916f7ac364fa03d05e2f4311e9b1d Author: Hans Pagel <hans.pagel@ueber.io> Date: Mon Jan 18 20:22:35 2021 +0100 refactoring commit 91a3747adca114fbce0972a2a2efa751e94d4ea4 Author: Hans Pagel <hans.pagel@ueber.io> Date: Mon Jan 18 17:48:59 2021 +0100 refactoring commit 4550fa70059060b6702425970ba33bcf6a0f3e66 Author: Hans Pagel <hans.pagel@ueber.io> Date: Mon Jan 18 17:37:43 2021 +0100 load plugins in the example commit a7087af14044673c587c233c44a5e767ff23b160 Author: Hans Pagel <hans.pagel@ueber.io> Date: Mon Jan 18 17:31:47 2021 +0100 init new linter plugin
This commit is contained in:
98
docs/src/demos/Experiments/Linter/extension/Linter.ts
Normal file
98
docs/src/demos/Experiments/Linter/extension/Linter.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Extension } from '@tiptap/core'
|
||||||
|
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||||
|
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
|
||||||
|
|
||||||
|
function renderIcon(issue) {
|
||||||
|
const icon = document.createElement('div')
|
||||||
|
|
||||||
|
icon.className = 'lint-icon'
|
||||||
|
icon.title = issue.message
|
||||||
|
icon.issue = issue
|
||||||
|
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
|
||||||
|
function runAllLinterPlugins(doc, plugins) {
|
||||||
|
const decorations: [any?] = []
|
||||||
|
|
||||||
|
const results = plugins.map(LinterPlugin => {
|
||||||
|
return new LinterPlugin(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: [any],
|
||||||
|
}
|
||||||
|
|
||||||
|
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, prevState) {
|
||||||
|
return transaction.docChanged
|
||||||
|
? runAllLinterPlugins(transaction.doc, plugins)
|
||||||
|
: prevState
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
return this.getState(state)
|
||||||
|
},
|
||||||
|
handleClick(view, _, event) {
|
||||||
|
if (/lint-icon/.test(event.target.className)) {
|
||||||
|
const { from, to } = event.target.issue
|
||||||
|
|
||||||
|
view.dispatch(
|
||||||
|
view.state.tr
|
||||||
|
.setSelection(TextSelection.create(view.state.doc, from, to))
|
||||||
|
.scrollIntoView(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleDoubleClick(view, _, event) {
|
||||||
|
if (/lint-icon/.test(event.target.className)) {
|
||||||
|
const prob = event.target.issue
|
||||||
|
|
||||||
|
if (prob.fix) {
|
||||||
|
prob.fix(view)
|
||||||
|
view.focus()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface AllExtensions {
|
||||||
|
Linter: typeof Linter,
|
||||||
|
}
|
||||||
|
}
|
||||||
23
docs/src/demos/Experiments/Linter/extension/LinterPlugin.ts
Normal file
23
docs/src/demos/Experiments/Linter/extension/LinterPlugin.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export default class LinterPlugin {
|
||||||
|
protected doc
|
||||||
|
|
||||||
|
private results = []
|
||||||
|
|
||||||
|
constructor(doc: any) {
|
||||||
|
this.doc = doc
|
||||||
|
}
|
||||||
|
|
||||||
|
record(message: string, from: number, to: number, fix?: null) {
|
||||||
|
this.results.push({
|
||||||
|
message,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
fix,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getResults() {
|
||||||
|
return this.results
|
||||||
|
}
|
||||||
|
}
|
||||||
8
docs/src/demos/Experiments/Linter/extension/index.ts
Normal file
8
docs/src/demos/Experiments/Linter/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,26 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
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: any) => {
|
||||||
|
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 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import LinterPlugin from '../LinterPlugin'
|
||||||
|
|
||||||
|
export class HeadingLevel extends LinterPlugin {
|
||||||
|
fixHeader(level) {
|
||||||
|
return function ({ state, dispatch }) {
|
||||||
|
dispatch(state.tr.setNodeMarkup(this.from - 1, null, { level }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scan() {
|
||||||
|
let lastHeadLevel = 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 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import LinterPlugin from '../LinterPlugin'
|
||||||
|
|
||||||
|
export class Punctuation extends LinterPlugin {
|
||||||
|
public regex = / ([,.!?:]) ?/g
|
||||||
|
|
||||||
|
fix(replacement: any) {
|
||||||
|
return function ({ state, dispatch }) {
|
||||||
|
dispatch(
|
||||||
|
state.tr.replaceWith(
|
||||||
|
this.from, this.to,
|
||||||
|
state.schema.text(replacement),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scan() {
|
||||||
|
this.doc.descendants((node, position) => {
|
||||||
|
if (!node.isText) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
96
docs/src/demos/Experiments/Linter/index.vue
Normal file
96
docs/src/demos/Experiments/Linter/index.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<editor-content :editor="editor" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Editor, EditorContent } from '@tiptap/vue-starter-kit'
|
||||||
|
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>
|
||||||
4
docs/src/docPages/experiments.md
Normal file
4
docs/src/docPages/experiments.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Experiments
|
||||||
|
Congratulations! You’ve found our secret playground with a list of experiments. Be aware, that nothing here is ready to use. Feel free to play around, but please, don’t open an issue for a bug you’ve found here or send pull requests. :-)
|
||||||
|
|
||||||
|
* [Linter](/experiments/linter)
|
||||||
5
docs/src/docPages/experiments/linter.md
Normal file
5
docs/src/docPages/experiments/linter.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Linter
|
||||||
|
|
||||||
|
⚠️ Experiment
|
||||||
|
|
||||||
|
<demo name="Experiments/Linter" highlight="" />
|
||||||
Reference in New Issue
Block a user