snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:11:02 +05:00
commit 083a46e93b
2115 changed files with 245958 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
import { Editor } from '@tiptap/core'
import { CodeBlockLowlight } from './code-block-lowlight.js'
import { Document } from '@tiptap/extension-document'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Text } from '@tiptap/extension-text'
import { all, createLowlight } from 'lowlight'
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
const lowlight = createLowlight(all)
describe('code block highlight', () => {
let Frontmatter
const editorElClass = 'tiptap'
let editor: Editor
const createEditorEl = () => {
const editorEl = document.createElement('div')
editorEl.classList.add(editorElClass)
document.body.appendChild(editorEl)
return editorEl
}
const getEditorEl = () => document.querySelector(`.${editorElClass}`)
beforeEach(() => {
Frontmatter = CodeBlockLowlight.extend({
name: 'frontmatter',
}).configure({ lowlight })
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
CodeBlockLowlight.configure({ lowlight }),
Frontmatter,
],
content: {
type: 'doc',
content: [
{
type: 'codeBlock',
attrs: {
language: 'javascript',
},
content: [
{
type: 'text',
text: 'alert("Hello world");',
},
],
},
{
type: 'frontmatter',
attrs: {
language: 'yaml',
},
content: [
{
type: 'text',
text: '---\ntitle: Page title\n---',
},
],
},
],
},
})
})
afterEach(() => {
editor.destroy()
getEditorEl()?.remove()
})
it('executes lowlight plugin in extensions that inherit from code-block-lowlight', () => {
expect(getEditorEl()?.querySelector('.language-javascript .hljs-string')).not.toBe(null)
expect(getEditorEl()?.querySelector('.language-yaml .hljs-string')).not.toBe(null)
})
})

View File

@@ -0,0 +1,43 @@
import type { CodeBlockOptions } from '@tiptap/extension-code-block'
import { CodeBlock } from '@tiptap/extension-code-block'
import { LowlightPlugin } from './lowlight-plugin.js'
export interface CodeBlockLowlightOptions extends CodeBlockOptions {
/**
* The lowlight instance.
*/
lowlight: any
}
/**
* This extension allows you to highlight code blocks with lowlight.
* @see https://tiptap.dev/api/nodes/code-block-lowlight
*/
export const CodeBlockLowlight = CodeBlock.extend<CodeBlockLowlightOptions>({
addOptions() {
return {
...this.parent?.(),
lowlight: {},
languageClassPrefix: 'language-',
exitOnTripleEnter: true,
exitOnArrowDown: true,
exitOnArrowUp: true,
defaultLanguage: null,
enableTabIndentation: false,
tabSize: 4,
HTMLAttributes: {},
}
},
addProseMirrorPlugins() {
return [
...(this.parent?.() || []),
LowlightPlugin({
name: this.name,
lowlight: this.options.lowlight,
defaultLanguage: this.options.defaultLanguage,
}),
]
},
})

View File

@@ -0,0 +1,5 @@
import { CodeBlockLowlight } from './code-block-lowlight.js'
export * from './code-block-lowlight.js'
export default CodeBlockLowlight

View File

@@ -0,0 +1,160 @@
import { findChildren } from '@tiptap/core'
import type { Node as ProsemirrorNode } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
// @ts-ignore
import highlight from 'highlight.js/lib/core'
function parseNodes(nodes: any[], className: string[] = []): { text: string; classes: string[] }[] {
return nodes.flatMap(node => {
const classes = [...className, ...(node.properties ? node.properties.className : [])]
if (node.children) {
return parseNodes(node.children, classes)
}
return {
text: node.value,
classes,
}
})
}
function getHighlightNodes(result: any) {
// `.value` for lowlight v1, `.children` for lowlight v2
return result.value || result.children || []
}
function registered(aliasOrLanguage: string) {
return Boolean(highlight.getLanguage(aliasOrLanguage))
}
function getDecorations({
doc,
name,
lowlight,
defaultLanguage,
}: {
doc: ProsemirrorNode
name: string
lowlight: any
defaultLanguage: string | null | undefined
}) {
const decorations: Decoration[] = []
findChildren(doc, node => node.type.name === name).forEach(block => {
let from = block.pos + 1
const language = block.node.attrs.language || defaultLanguage
const languages = lowlight.listLanguages()
const nodes =
language &&
(languages.includes(language) || registered(language) || lowlight.registered?.(language))
? getHighlightNodes(lowlight.highlight(language, block.node.textContent))
: getHighlightNodes(lowlight.highlightAuto(block.node.textContent))
parseNodes(nodes).forEach(node => {
const to = from + node.text.length
if (node.classes.length) {
const decoration = Decoration.inline(from, to, {
class: node.classes.join(' '),
})
decorations.push(decoration)
}
from = to
})
})
return DecorationSet.create(doc, decorations)
}
// oxlint-disable-next-lineno-unsafe-function-type
function isFunction(param: any): param is Function {
return typeof param === 'function'
}
export function LowlightPlugin({
name,
lowlight,
defaultLanguage,
}: {
name: string
lowlight: any
defaultLanguage: string | null | undefined
}) {
if (!['highlight', 'highlightAuto', 'listLanguages'].every(api => isFunction(lowlight[api]))) {
throw Error(
'You should provide an instance of lowlight to use the code-block-lowlight extension',
)
}
const lowlightPlugin: Plugin<any> = new Plugin({
key: new PluginKey('lowlight'),
state: {
init: (_, { doc }) =>
getDecorations({
doc,
name,
lowlight,
defaultLanguage,
}),
apply: (transaction, decorationSet, oldState, newState) => {
const oldNodeName = oldState.selection.$head.parent.type.name
const newNodeName = newState.selection.$head.parent.type.name
const oldNodes = findChildren(oldState.doc, node => node.type.name === name)
const newNodes = findChildren(newState.doc, node => node.type.name === name)
if (
transaction.docChanged &&
// Apply decorations if:
// selection includes named node,
([oldNodeName, newNodeName].includes(name) ||
// OR transaction adds/removes named node,
newNodes.length !== oldNodes.length ||
// OR transaction has changes that completely encapsulte a node
// (for example, a transaction that affects the entire document).
// Such transactions can happen during collab syncing via y-prosemirror, for example.
transaction.steps.some(step => {
// @ts-ignore
return (
// @ts-ignore
step.from !== undefined &&
// @ts-ignore
step.to !== undefined &&
oldNodes.some(node => {
// @ts-ignore
return (
// @ts-ignore
node.pos >= step.from &&
// @ts-ignore
node.pos + node.node.nodeSize <= step.to
)
})
)
}))
) {
return getDecorations({
doc: transaction.doc,
name,
lowlight,
defaultLanguage,
})
}
return decorationSet.map(transaction.mapping, transaction.doc)
},
},
props: {
decorations(state) {
return lowlightPlugin.getState(state)
},
},
})
return lowlightPlugin
}