From 5452095343fdef896b4cf81e7dbf9d5ff75e684e Mon Sep 17 00:00:00 2001 From: Hans Pagel Date: Tue, 19 Jan 2021 14:49:07 +0100 Subject: [PATCH] experimental linters commit 5b31740d967bb61bfed6a2338d07e8fc6e4957b3 Author: Hans Pagel Date: Tue Jan 19 14:48:00 2021 +0100 refactoring, disable TS checks for now commit 6fcc5082287ba4dd5457b8ea6e6d8300efaaafb6 Author: Hans Pagel Date: Tue Jan 19 14:42:14 2021 +0100 move everything to a new experiments structure commit 2b5f394ad4c916f7ac364fa03d05e2f4311e9b1d Author: Hans Pagel Date: Mon Jan 18 20:22:35 2021 +0100 refactoring commit 91a3747adca114fbce0972a2a2efa751e94d4ea4 Author: Hans Pagel Date: Mon Jan 18 17:48:59 2021 +0100 refactoring commit 4550fa70059060b6702425970ba33bcf6a0f3e66 Author: Hans Pagel Date: Mon Jan 18 17:37:43 2021 +0100 load plugins in the example commit a7087af14044673c587c233c44a5e767ff23b160 Author: Hans Pagel Date: Mon Jan 18 17:31:47 2021 +0100 init new linter plugin --- .../Experiments/Linter/extension/Linter.ts | 98 +++++++++++++++++++ .../Linter/extension/LinterPlugin.ts | 23 +++++ .../Experiments/Linter/extension/index.ts | 8 ++ .../Linter/extension/plugins/BadWords.ts | 26 +++++ .../Linter/extension/plugins/HeadingLevel.ts | 30 ++++++ .../Linter/extension/plugins/Punctuation.ts | 37 +++++++ docs/src/demos/Experiments/Linter/index.vue | 96 ++++++++++++++++++ docs/src/docPages/experiments.md | 4 + docs/src/docPages/experiments/linter.md | 5 + 9 files changed, 327 insertions(+) create mode 100644 docs/src/demos/Experiments/Linter/extension/Linter.ts create mode 100644 docs/src/demos/Experiments/Linter/extension/LinterPlugin.ts create mode 100644 docs/src/demos/Experiments/Linter/extension/index.ts create mode 100644 docs/src/demos/Experiments/Linter/extension/plugins/BadWords.ts create mode 100644 docs/src/demos/Experiments/Linter/extension/plugins/HeadingLevel.ts create mode 100644 docs/src/demos/Experiments/Linter/extension/plugins/Punctuation.ts create mode 100644 docs/src/demos/Experiments/Linter/index.vue create mode 100644 docs/src/docPages/experiments.md create mode 100644 docs/src/docPages/experiments/linter.md diff --git a/docs/src/demos/Experiments/Linter/extension/Linter.ts b/docs/src/demos/Experiments/Linter/extension/Linter.ts new file mode 100644 index 00000000..42fe777f --- /dev/null +++ b/docs/src/demos/Experiments/Linter/extension/Linter.ts @@ -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: { + 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, + } +} diff --git a/docs/src/demos/Experiments/Linter/extension/LinterPlugin.ts b/docs/src/demos/Experiments/Linter/extension/LinterPlugin.ts new file mode 100644 index 00000000..46fc867a --- /dev/null +++ b/docs/src/demos/Experiments/Linter/extension/LinterPlugin.ts @@ -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 + } +} diff --git a/docs/src/demos/Experiments/Linter/extension/index.ts b/docs/src/demos/Experiments/Linter/extension/index.ts new file mode 100644 index 00000000..29d42a68 --- /dev/null +++ b/docs/src/demos/Experiments/Linter/extension/index.ts @@ -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' diff --git a/docs/src/demos/Experiments/Linter/extension/plugins/BadWords.ts b/docs/src/demos/Experiments/Linter/extension/plugins/BadWords.ts new file mode 100644 index 00000000..59194c21 --- /dev/null +++ b/docs/src/demos/Experiments/Linter/extension/plugins/BadWords.ts @@ -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 + } +} diff --git a/docs/src/demos/Experiments/Linter/extension/plugins/HeadingLevel.ts b/docs/src/demos/Experiments/Linter/extension/plugins/HeadingLevel.ts new file mode 100644 index 00000000..ebb2cd8f --- /dev/null +++ b/docs/src/demos/Experiments/Linter/extension/plugins/HeadingLevel.ts @@ -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 + } +} diff --git a/docs/src/demos/Experiments/Linter/extension/plugins/Punctuation.ts b/docs/src/demos/Experiments/Linter/extension/plugins/Punctuation.ts new file mode 100644 index 00000000..d3a228d7 --- /dev/null +++ b/docs/src/demos/Experiments/Linter/extension/plugins/Punctuation.ts @@ -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 + } +} diff --git a/docs/src/demos/Experiments/Linter/index.vue b/docs/src/demos/Experiments/Linter/index.vue new file mode 100644 index 00000000..b4c09f9c --- /dev/null +++ b/docs/src/demos/Experiments/Linter/index.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/docs/src/docPages/experiments.md b/docs/src/docPages/experiments.md new file mode 100644 index 00000000..70f7d340 --- /dev/null +++ b/docs/src/docPages/experiments.md @@ -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) diff --git a/docs/src/docPages/experiments/linter.md b/docs/src/docPages/experiments/linter.md new file mode 100644 index 00000000..37d083f8 --- /dev/null +++ b/docs/src/docPages/experiments/linter.md @@ -0,0 +1,5 @@ +# Linter + +⚠️ Experiment + +