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:
Hans Pagel 2021-01-19 14:49:07 +01:00
parent 1e478cd26f
commit 5452095343
9 changed files with 327 additions and 0 deletions

View 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,
}
}

View 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
}
}

View 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'

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View 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>

View File

@ -0,0 +1,4 @@
# Experiments
Congratulations! Youve 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, dont open an issue for a bug youve found here or send pull requests. :-)
* [Linter](/experiments/linter)

View File

@ -0,0 +1,5 @@
# Linter
⚠️ Experiment
<demo name="Experiments/Linter" highlight="" />