Skip to content

Instantly share code, notes, and snippets.

@philippkuehn
Created May 22, 2019 17:56
Show Gist options
  • Save philippkuehn/32fa6e0a08b47ac7225d7d1c65e8cc7e to your computer and use it in GitHub Desktop.
Save philippkuehn/32fa6e0a08b47ac7225d7d1c65e8cc7e to your computer and use it in GitHub Desktop.
codeblock extension for tiptap using on scrumpy.io
<template>
<div class="c-code-block">
<div class="c-code-block__meta" contenteditable="false" v-if="editable">
<icon-btn
class="c-code-block__action-icon"
name="more"
:dropdown-props="{ closeOnClick: false }"
ref="icon"
>
<template v-slot:dropdown>
<actions-list>
<actions-group>
<input
class="c-code-block__search"
type="text"
autofocus
v-model="query"
placeholder="Search …"
ref="search"
/>
</actions-group>
<actions-group class="c-code-block__list">
<actions-item
v-for="language in filteredLanguages"
:key="language.value"
:label="language.label"
@click.native="setLanguage(language.value)"
/>
</actions-group>
</actions-list>
</template>
</icon-btn>
</div>
<pre
class="c-code-block__pre"
:data-lang="node.attrs.language"
spellcheck="false"
><code class="c-code-block__code" ref="content"></code></pre>
</div>
</template>
<script>
import Fuse from 'fuse.js'
import IconBtn from 'Components/IconBtn'
import { ActionsList, ActionsGroup, ActionsItem } from 'Components/Actions'
export default {
components: {
IconBtn,
ActionsList,
ActionsGroup,
ActionsItem,
},
props: ['node', 'updateAttrs', 'editable', 'options'],
data() {
return {
query: null,
}
},
computed: {
filteredLanguages() {
if (!this.query) {
return this.options.languages
}
const fuse = new Fuse(this.options.languages, {
threshold: 0.2,
keys: [
'value',
],
})
return fuse.search(this.query)
},
},
methods: {
setLanguage(language) {
this.updateAttrs({
language,
})
this.$refs.icon.$refs.wrapper.$emit('close')
this.query = null
if (this.$refs.search) {
this.$refs.search.blur()
}
},
},
}
</script>
<style lang="scss" src="./style.scss"></style>
import { Plugin, PluginKey } from 'tiptap'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { findBlockNodes } from 'prosemirror-utils'
import low from 'lowlight/lib/core'
function getDecorations({ doc, name }) {
const decorations = []
const blocks = findBlockNodes(doc).filter(item => item.node.type.name === name)
const flatten = list => list.reduce(
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [],
)
function parseNodes(nodes, className = []) {
return nodes.map(node => {
const classes = [
...className,
...node.properties ? node.properties.className : [],
]
if (node.children) {
return parseNodes(node.children, classes)
}
return {
text: node.value,
classes,
}
})
}
blocks.forEach(block => {
const { language } = block.node.attrs
let startPos = block.pos + 1
let nodes
try {
nodes = language
? low.highlight(language, block.node.textContent).value
: low.highlightAuto(block.node.textContent).value
} catch (error) {
if (error && /Unknown language/.test(error.message)) {
return
}
throw error
}
flatten(parseNodes(nodes))
.map(node => {
const from = startPos
const to = from + node.text.length
startPos = to
return {
...node,
from,
to,
}
})
.forEach(node => {
const decoration = Decoration.inline(node.from, node.to, {
class: node.classes.join(' '),
})
decorations.push(decoration)
})
})
return DecorationSet.create(doc, decorations)
}
export default function HighlightPlugin({ name }) {
return new Plugin({
name: new PluginKey('highlight'),
state: {
init: (_, { doc }) => getDecorations({ doc, name }),
apply: (transaction, decorationSet, oldState, state) => {
// TODO: find way to cache decorations
// see: https://discuss.prosemirror.net/t/how-to-update-multiple-inline-decorations-on-node-change/1493
const nodeName = state.selection.$head.parent.type.name
const previousNodeName = oldState.selection.$head.parent.type.name
if (transaction.docChanged && [nodeName, previousNodeName].includes(name)) {
return getDecorations({ doc: transaction.doc, name })
}
return decorationSet.map(transaction.mapping, transaction.doc)
},
},
props: {
decorations(state) {
return this.getState(state)
},
},
})
}
import low from 'lowlight/lib/core'
import { Node } from 'tiptap'
import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-commands'
import languages from './languages'
import Component from './Component'
import HighlightPlugin from './HighlightPlugin'
languages.forEach(language => {
if (language.value && language.config) {
low.registerLanguage(language.value, language.config)
}
})
export default class CodeBlockHighlight extends Node {
get name() {
return 'code_block'
}
get view() {
return Component
}
get defaultOptions() {
return {
languages,
}
}
get schema() {
return {
attrs: {
language: {
default: null,
},
},
content: 'text*',
marks: '',
group: 'block',
code: true,
defining: true,
draggable: false,
parseDOM: [
{ tag: 'pre', preserveWhitespace: 'full' },
],
toDOM: () => ['pre', ['code', 0]],
}
}
commands({ type, schema }) {
return () => toggleBlockType(type, schema.nodes.paragraph)
}
keys({ type }) {
return {
'Shift-Ctrl-\\': setBlockType(type),
}
}
inputRules({ type }) {
return [
textblockTypeInputRule(/^```$/, type),
]
}
get plugins() {
return [
HighlightPlugin({ name: this.name }),
]
}
}
import apache from 'highlight.js/lib/languages/apache'
import bash from 'highlight.js/lib/languages/bash'
import cpp from 'highlight.js/lib/languages/cpp'
import cs from 'highlight.js/lib/languages/cs'
import css from 'highlight.js/lib/languages/css'
import diff from 'highlight.js/lib/languages/diff'
import dockerfile from 'highlight.js/lib/languages/dockerfile'
import http from 'highlight.js/lib/languages/http'
import ini from 'highlight.js/lib/languages/ini'
import go from 'highlight.js/lib/languages/go'
import less from 'highlight.js/lib/languages/less'
import java from 'highlight.js/lib/languages/java'
import javascript from 'highlight.js/lib/languages/javascript'
import json from 'highlight.js/lib/languages/json'
import makefile from 'highlight.js/lib/languages/makefile'
import markdown from 'highlight.js/lib/languages/markdown'
import nginx from 'highlight.js/lib/languages/nginx'
import objectivec from 'highlight.js/lib/languages/objectivec'
import perl from 'highlight.js/lib/languages/perl'
import php from 'highlight.js/lib/languages/php'
import plaintext from 'highlight.js/lib/languages/plaintext'
import properties from 'highlight.js/lib/languages/properties'
import python from 'highlight.js/lib/languages/python'
import ruby from 'highlight.js/lib/languages/ruby'
import scss from 'highlight.js/lib/languages/scss'
import shell from 'highlight.js/lib/languages/shell'
import sql from 'highlight.js/lib/languages/sql'
import typescript from 'highlight.js/lib/languages/typescript'
import xml from 'highlight.js/lib/languages/xml'
import yaml from 'highlight.js/lib/languages/yaml'
export default [
{
label: 'Auto Detection',
value: null,
config: null,
},
{
label: 'Plain Text',
value: 'plaintext',
config: plaintext,
},
{
label: 'Apache',
value: 'apache',
config: apache,
},
{
label: 'Bash',
value: 'bash',
config: bash,
},
{
label: 'C++',
value: 'cpp',
config: cpp,
},
{
label: 'C#+',
value: 'cs',
config: cs,
},
{
label: 'CSS',
value: 'css',
config: css,
},
{
label: 'Diff',
value: 'diff',
config: diff,
},
{
label: 'Dockerfile',
value: 'dockerfile',
config: dockerfile,
},
{
label: 'HTTP',
value: 'http',
config: http,
},
{
label: 'Ini, TOML',
value: 'ini',
config: ini,
},
{
label: 'Go',
value: 'go',
config: go,
},
{
label: 'Less',
value: 'less',
config: less,
},
{
label: 'Java',
value: 'java',
config: java,
},
{
label: 'JavaScript',
value: 'javascript',
config: javascript,
},
{
label: 'JSON',
value: 'json',
config: json,
},
{
label: 'Makefile',
value: 'makefile',
config: makefile,
},
{
label: 'Markdown',
value: 'markdown',
config: markdown,
},
{
label: 'Nginx',
value: 'nginx',
config: nginx,
},
{
label: 'Objective-C',
value: 'objectivec',
config: objectivec,
},
{
label: 'Perl',
value: 'perl',
config: perl,
},
{
label: 'PHP',
value: 'php',
config: php,
},
{
label: 'Properties',
value: 'properties',
config: properties,
},
{
label: 'Python',
value: 'python',
config: python,
},
{
label: 'Ruby',
value: 'ruby',
config: ruby,
},
{
label: 'SCSS',
value: 'scss',
config: scss,
},
{
label: 'Shell',
value: 'shell',
config: shell,
},
{
label: 'SQL',
value: 'sql',
config: sql,
},
{
label: 'TypeScript',
value: 'typescript',
config: typescript,
},
{
label: 'HTML, XML',
value: 'xml',
config: xml,
},
{
label: 'YAML',
value: 'yaml',
config: yaml,
},
]
@import '~settings';
@import '~utilityFunctions';
.c-code-block {
position: relative;
white-space: normal;
&__meta {
position: absolute;
top: 0.25rem;
right: 0;
}
&__pre {
}
&__code {
display: block;
white-space: pre;
}
.is-desktop &__action-icon {
opacity: 0;
}
.is-desktop &:hover &__action-icon,
.is-desktop .tippy-active &__action-icon {
opacity: 1;
}
&__search {
background-color: color(grey, transparent);
border-radius: 6px;
border: none;
padding: 0.25rem 0.5rem;
}
&__list {
max-height: 15rem;
overflow: auto;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment