diff --git a/examples/Components/App/style.scss b/examples/Components/App/style.scss index cea996d2..dd733485 100644 --- a/examples/Components/App/style.scss +++ b/examples/Components/App/style.scss @@ -37,9 +37,62 @@ background: $color-black; color: $color-white; font-size: 0.8rem; + overflow-x: auto; code { display: block; + + .hljs-comment, + .hljs-quote { + color: #999999; + } + + .hljs-variable, + .hljs-template-variable, + .hljs-attribute, + .hljs-tag, + .hljs-name, + .hljs-regexp, + .hljs-link, + .hljs-name, + .hljs-selector-id, + .hljs-selector-class { + color: #f2777a; + } + + .hljs-number, + .hljs-meta, + .hljs-built_in, + .hljs-builtin-name, + .hljs-literal, + .hljs-type, + .hljs-params { + color: #f99157; + } + + .hljs-string, + .hljs-symbol, + .hljs-bullet { + color: #99cc99; + } + + .hljs-title, + .hljs-section { + color: #ffcc66; + } + + .hljs-keyword, + .hljs-selector-tag { + color: #6699cc; + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: 700; + } } } diff --git a/examples/Components/Routes/CodeHighlighting/examples.js b/examples/Components/Routes/CodeHighlighting/examples.js new file mode 100644 index 00000000..8e4ab8c5 --- /dev/null +++ b/examples/Components/Routes/CodeHighlighting/examples.js @@ -0,0 +1,81 @@ +export const javascript = `function $initHighlight(block, flags) { + try { + if (block.className.search(/\bno\-highlight\b/) != -1) + return processBlock(block, true, 0x0F) + ' class=""'; + } catch (e) { + /* handle exception */ + } + for (var i = 0 / 2; i < classes.length; i++) { // "0 / 2" should not be parsed as regexp + if (checkCondition(classes[i]) === undefined) + return /\d+/g; + } +}` + +export const css = `@font-face { + font-family: Chunkfive; src: url('Chunkfive.otf'); +} + +body, .usertext { + color: #F0F0F0; background: #600; + font-family: Chunkfive, sans; +} + +@import url(print.css); +@media print { + a[href^=http]::after { + content: attr(href) + } +}` + +export const php = `require_once 'Zend/Uri/Http.php'; + +namespace Location\Web; + +interface Factory +{ + static function _factory(); +} + +abstract class URI extends BaseURI implements Factory +{ + abstract function test(); + + public static $st1 = 1; + const ME = "Yo"; + var $list = NULL; + private $var; + + /** + * Returns a URI + * + * @return URI + */ + static public function _factory($stats = array(), $uri = 'http') + { + echo __METHOD__; + $uri = explode(':', $uri, 0b10); + $schemeSpecific = isset($uri[1]) ? $uri[1] : ''; + $desc = 'Multi +line description'; + + // Security check + if (!ctype_alnum($scheme)) { + throw new Zend_Uri_Exception('Illegal scheme'); + } + + $this->var = 0 - self::$st; + $this->list = list(Array("1"=> 2, 2=>self::ME, 3 => \Location\Web\URI::class)); + + return [ + 'uri' => $uri, + 'value' => null, + ]; + } +} + +echo URI::ME . URI::$st1; + +__halt_compiler () ; datahere +datahere +datahere */ +datahere` diff --git a/examples/Components/Routes/CodeHighlighting/index.vue b/examples/Components/Routes/CodeHighlighting/index.vue new file mode 100644 index 00000000..972f314e --- /dev/null +++ b/examples/Components/Routes/CodeHighlighting/index.vue @@ -0,0 +1,72 @@ + + + \ No newline at end of file diff --git a/examples/Components/Subnavigation/index.vue b/examples/Components/Subnavigation/index.vue index 5122532d..fb743d9b 100644 --- a/examples/Components/Subnavigation/index.vue +++ b/examples/Components/Subnavigation/index.vue @@ -21,6 +21,9 @@ Markdown Shortcuts + + Code Highlighting + Read-Only diff --git a/examples/main.js b/examples/main.js index ce2e6ba5..f573dd74 100644 --- a/examples/main.js +++ b/examples/main.js @@ -10,6 +10,7 @@ import RouteImages from 'Components/Routes/Images' import RouteHidingMenuBar from 'Components/Routes/HidingMenuBar' import RouteTodoList from 'Components/Routes/TodoList' import RouteMarkdownShortcuts from 'Components/Routes/MarkdownShortcuts' +import RouteCodeHighlighting from 'Components/Routes/CodeHighlighting' import RouteReadOnly from 'Components/Routes/ReadOnly' import RouteEmbeds from 'Components/Routes/Embeds' import RouteExport from 'Components/Routes/Export' @@ -71,6 +72,13 @@ const routes = [ githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/MarkdownShortcuts', }, }, + { + path: '/code-highlighting', + component: RouteCodeHighlighting, + meta: { + githubUrl: 'https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/CodeHighlighting', + }, + }, { path: '/read-only', component: RouteReadOnly, diff --git a/packages/tiptap-extensions/package.json b/packages/tiptap-extensions/package.json index adceb514..a88c0fc0 100644 --- a/packages/tiptap-extensions/package.json +++ b/packages/tiptap-extensions/package.json @@ -20,6 +20,7 @@ "url": "https://github.com/heyscrumpy/tiptap/issues" }, "dependencies": { + "lowlight": "^1.10.0", "prosemirror-history": "^1.0.2", "tiptap": "^0.8.0", "tiptap-commands": "^0.2.4" diff --git a/packages/tiptap-extensions/src/index.js b/packages/tiptap-extensions/src/index.js index 8e176784..b0e11961 100644 --- a/packages/tiptap-extensions/src/index.js +++ b/packages/tiptap-extensions/src/index.js @@ -1,6 +1,7 @@ export { default as BlockquoteNode } from './nodes/Blockquote' export { default as BulletListNode } from './nodes/BulletList' export { default as CodeBlockNode } from './nodes/CodeBlock' +export { default as CodeBlockHighlightNode } from './nodes/CodeBlockHighlight' export { default as HardBreakNode } from './nodes/HardBreak' export { default as HeadingNode } from './nodes/Heading' export { default as ImageNode } from './nodes/Image' diff --git a/packages/tiptap-extensions/src/nodes/Blockquote.js b/packages/tiptap-extensions/src/nodes/Blockquote.js index e8bb80ad..0823c89c 100644 --- a/packages/tiptap-extensions/src/nodes/Blockquote.js +++ b/packages/tiptap-extensions/src/nodes/Blockquote.js @@ -1,5 +1,5 @@ -import { Node } from 'tiptap' -import { wrappingInputRule, setBlockType, wrapIn } from 'tiptap-commands' +import { Node, Plugin } from 'tiptap' +import { wrappingInputRule, wrapIn } from 'tiptap-commands' export default class BlockquoteNode extends Node { diff --git a/packages/tiptap-extensions/src/nodes/CodeBlockHighlight.js b/packages/tiptap-extensions/src/nodes/CodeBlockHighlight.js new file mode 100644 index 00000000..26e06cbb --- /dev/null +++ b/packages/tiptap-extensions/src/nodes/CodeBlockHighlight.js @@ -0,0 +1,109 @@ +import { Node, Plugin } from 'tiptap' +import { Decoration, DecorationSet } from 'prosemirror-view' +import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-commands' +import { findBlockNodes } from 'prosemirror-utils' +import low from 'lowlight' + +export default class CodeBlockHighlightNode extends Node { + + get name() { + return 'code_block' + } + + get schema() { + return { + content: 'text*', + marks: '', + group: 'block', + code: true, + defining: true, + draggable: false, + parseDOM: [ + { tag: 'pre', preserveWhitespace: 'full' }, + ], + toDOM: () => ['pre', ['code', 0]], + } + } + + command({ type, schema }) { + return toggleBlockType(type, schema.nodes.paragraph) + } + + keys({ type }) { + return { + 'Shift-Ctrl-\\': setBlockType(type), + } + } + + inputRules({ type }) { + return [ + textblockTypeInputRule(/^```$/, type), + ] + } + + get plugins() { + return [ + new Plugin({ + props: { + decorations({ doc }) { + const decorations = [] + + const blocks = findBlockNodes(doc) + .filter(item => item.node.type.name === 'code_block') + + 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 => { + let startPos = block.pos + 1 + const nodes = low.highlightAuto(block.node.textContent).value + + 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) + }, + }, + }), + ] + } + +} diff --git a/yarn.lock b/yarn.lock index 9d764c33..1633a183 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4068,6 +4068,12 @@ fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" +fault@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa" + dependencies: + format "^0.2.2" + fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -4285,6 +4291,10 @@ form-data@~2.3.1, form-data@~2.3.2: combined-stream "1.0.6" mime-types "^2.1.12" +format@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -4865,6 +4875,10 @@ hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" +highlight.js@~9.12.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -6125,6 +6139,13 @@ lowercase-keys@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" +lowlight@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.10.0.tgz#fbbd9158240d9c6c07c44b055ff0c32f1a2c9b99" + dependencies: + fault "^1.0.2" + highlight.js "~9.12.0" + lpad-align@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/lpad-align/-/lpad-align-1.1.2.tgz#21f600ac1c3095c3c6e497ee67271ee08481fe9e"