prosemirror-highlight 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +28 -16
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -32,14 +32,40 @@ export const shikiPlugin = createHighlightPlugin({ parser })
|
|
|
32
32
|
```ts
|
|
33
33
|
import { createHighlightPlugin } from 'prosemirror-highlight'
|
|
34
34
|
import { createParser, type Parser } from 'prosemirror-highlight/shiki'
|
|
35
|
+
import type { Decoration } from 'prosemirror-view'
|
|
35
36
|
import {
|
|
36
|
-
|
|
37
|
+
createHighlighter,
|
|
37
38
|
type BuiltinLanguage,
|
|
38
39
|
type Highlighter,
|
|
39
40
|
} from 'shiki'
|
|
40
41
|
|
|
41
42
|
let highlighter: Highlighter | undefined
|
|
43
|
+
let highlighterPromise: Promise<void> | undefined
|
|
42
44
|
let parser: Parser | undefined
|
|
45
|
+
const loadedLanguages = new Set<string>()
|
|
46
|
+
|
|
47
|
+
function loadHighlighter(): Promise<void> {
|
|
48
|
+
if (!highlighterPromise) {
|
|
49
|
+
highlighterPromise = createHighlighter({
|
|
50
|
+
themes: ['github-light', 'github-dark', 'github-dark-dimmed'],
|
|
51
|
+
langs: [],
|
|
52
|
+
}).then((h) => {
|
|
53
|
+
highlighter = h
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
return highlighterPromise
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function loadLanguage(
|
|
60
|
+
highlighter: Highlighter,
|
|
61
|
+
language: string,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
try {
|
|
64
|
+
return await highlighter.loadLanguage(language as BuiltinLanguage)
|
|
65
|
+
} finally {
|
|
66
|
+
loadedLanguages.add(language)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
43
69
|
|
|
44
70
|
/**
|
|
45
71
|
* Lazy load highlighter and highlighter languages.
|
|
@@ -48,23 +74,25 @@ let parser: Parser | undefined
|
|
|
48
74
|
* promise that resolves when the highlighter or the language is loaded.
|
|
49
75
|
* Otherwise, it returns an array of decorations.
|
|
50
76
|
*/
|
|
51
|
-
const lazyParser: Parser = (options) => {
|
|
77
|
+
const lazyParser: Parser = (options): Promise<void> | Decoration[] => {
|
|
52
78
|
if (!highlighter) {
|
|
53
|
-
return
|
|
54
|
-
themes: ['github-light'],
|
|
55
|
-
langs: [],
|
|
56
|
-
}).then((h) => {
|
|
57
|
-
highlighter = h
|
|
58
|
-
})
|
|
79
|
+
return loadHighlighter()
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
const language = options.language
|
|
62
|
-
if (language && !
|
|
63
|
-
return
|
|
82
|
+
const language = options.language
|
|
83
|
+
if (language && !loadedLanguages.has(language)) {
|
|
84
|
+
return loadLanguage(highlighter, language)
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
if (!parser) {
|
|
67
|
-
parser = createParser(highlighter
|
|
88
|
+
parser = createParser(highlighter, {
|
|
89
|
+
themes: {
|
|
90
|
+
light: 'github-light',
|
|
91
|
+
dark: 'github-dark',
|
|
92
|
+
dim: 'github-dark-dimmed',
|
|
93
|
+
},
|
|
94
|
+
defaultColor: 'dim',
|
|
95
|
+
})
|
|
68
96
|
}
|
|
69
97
|
|
|
70
98
|
return parser(options)
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -97,7 +97,7 @@ function createHighlightPlugin({ parser, nodeTypes = ["code_block", "codeBlock"]
|
|
|
97
97
|
const refresh = !!tr.getMeta("prosemirror-highlight-refresh");
|
|
98
98
|
if (!tr.docChanged && !refresh) return {
|
|
99
99
|
cache,
|
|
100
|
-
decorations: data.decorations
|
|
100
|
+
decorations: data.decorations?.map(tr.mapping, tr.doc),
|
|
101
101
|
promises: data.promises
|
|
102
102
|
};
|
|
103
103
|
const [decorations, promises] = calculateDecoration(tr.doc, parser, nodeTypes, languageExtractor, cache);
|
|
@@ -122,7 +122,8 @@ function createHighlightPlugin({ parser, nodeTypes = ["code_block", "codeBlock"]
|
|
|
122
122
|
promise.then(() => {
|
|
123
123
|
promises.delete(promise);
|
|
124
124
|
refresh();
|
|
125
|
-
}).catch(() => {
|
|
125
|
+
}).catch((error) => {
|
|
126
|
+
console.error("[prosemirror-highlight] Error resolving parser:", error);
|
|
126
127
|
promises.delete(promise);
|
|
127
128
|
});
|
|
128
129
|
}
|
|
@@ -138,35 +139,46 @@ function createHighlightPlugin({ parser, nodeTypes = ["code_block", "codeBlock"]
|
|
|
138
139
|
});
|
|
139
140
|
}
|
|
140
141
|
function calculateDecoration(doc, parser, nodeTypes, languageExtractor, cache) {
|
|
141
|
-
const
|
|
142
|
+
const allDecorations = [];
|
|
142
143
|
const promises = [];
|
|
143
|
-
doc
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
const nodes = collectCodeBlocks(doc, nodeTypes);
|
|
145
|
+
try {
|
|
146
|
+
for (const [node, pos] of nodes) {
|
|
146
147
|
const language = languageExtractor(node);
|
|
147
148
|
const cached = cache.get(pos);
|
|
148
149
|
if (cached) {
|
|
149
150
|
const [_, decorations] = cached;
|
|
150
|
-
|
|
151
|
+
if (decorations.length > 0) allDecorations.push(decorations);
|
|
151
152
|
} else {
|
|
152
|
-
const
|
|
153
|
+
const parsed = parser({
|
|
153
154
|
content: node.textContent,
|
|
154
155
|
language: language || void 0,
|
|
155
156
|
pos,
|
|
156
157
|
size: node.nodeSize
|
|
157
158
|
});
|
|
158
|
-
if (
|
|
159
|
-
cache.set(pos, node,
|
|
160
|
-
|
|
161
|
-
} else if (
|
|
159
|
+
if (parsed && Array.isArray(parsed)) {
|
|
160
|
+
cache.set(pos, node, parsed);
|
|
161
|
+
if (parsed.length > 0) allDecorations.push(parsed);
|
|
162
|
+
} else if (parsed instanceof Promise) {
|
|
162
163
|
cache.remove(pos);
|
|
163
|
-
promises.push(
|
|
164
|
-
}
|
|
164
|
+
promises.push(parsed);
|
|
165
|
+
} else console.error(`[prosemirror-highlight] Invalid parser result:`, parsed);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`[prosemirror-highlight] Error parsing code blocks:`, error);
|
|
170
|
+
}
|
|
171
|
+
return [allDecorations.length > 0 ? DecorationSet.create(doc, allDecorations.flat()) : void 0, promises];
|
|
172
|
+
}
|
|
173
|
+
function collectCodeBlocks(doc, nodeTypes) {
|
|
174
|
+
const nodes = [];
|
|
175
|
+
doc.descendants((node, pos) => {
|
|
176
|
+
if (node.type.isTextblock && nodeTypes.includes(node.type.name)) {
|
|
177
|
+
nodes.push([node, pos]);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
168
180
|
});
|
|
169
|
-
return
|
|
181
|
+
return nodes;
|
|
170
182
|
}
|
|
171
183
|
|
|
172
184
|
//#endregion
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/cache.ts","../src/plugin.ts"],"sourcesContent":["import type { Node as ProseMirrorNode } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport type { Decoration } from 'prosemirror-view'\n\n/**\n * Represents a cache of doc positions to the node and decorations at that position\n */\nexport class DecorationCache {\n private cache: Map<number, [node: ProseMirrorNode, decorations: Decoration[]]>\n\n constructor(\n cache?: Map<number, [node: ProseMirrorNode, decorations: Decoration[]]>,\n ) {\n this.cache = new Map(cache)\n }\n\n /**\n * Gets the cache entry at the given doc position, or null if it doesn't exist\n * @param pos The doc position of the node you want the cache for\n */\n get(pos: number) {\n return this.cache.get(pos)\n }\n\n /**\n * Sets the cache entry at the given position with the give node/decoration\n * values\n * @param pos The doc position of the node to set the cache for\n * @param node The node to place in cache\n * @param decorations The decorations to place in cache\n */\n set(pos: number, node: ProseMirrorNode, decorations: Decoration[]): void {\n if (pos < 0) {\n return\n }\n\n this.cache.set(pos, [node, decorations])\n }\n\n /**\n * Removes the value at the oldPos (if it exists) and sets the new position to\n * the given values\n * @param oldPos The old node position to overwrite\n * @param newPos The new node position to set the cache for\n * @param node The new node to place in cache\n * @param decorations The new decorations to place in cache\n */\n private replace(\n oldPos: number,\n newPos: number,\n node: ProseMirrorNode,\n decorations: Decoration[],\n ): void {\n this.remove(oldPos)\n this.set(newPos, node, decorations)\n }\n\n /**\n * Removes the cache entry at the given position\n * @param pos The doc position to remove from cache\n */\n remove(pos: number): void {\n this.cache.delete(pos)\n }\n\n /**\n * Invalidates the cache by removing all decoration entries on nodes that have\n * changed, updating the positions of the nodes that haven't and removing all\n * the entries that have been deleted; NOTE: this does not affect the current\n * cache, but returns an entirely new one\n * @param tr A transaction to map the current cache to\n */\n invalidate(tr: Transaction): DecorationCache {\n const returnCache = new DecorationCache(this.cache)\n const mapping = tr.mapping\n\n this.cache.forEach(([node, decorations], pos) => {\n if (pos < 0) {\n return\n }\n\n const result = mapping.mapResult(pos)\n const mappedNode = tr.doc.nodeAt(result.pos)\n\n if (result.deleted || !mappedNode?.eq(node)) {\n returnCache.remove(pos)\n } else if (pos !== result.pos) {\n // update the decorations' from/to values to match the new node position\n const updatedDecorations = decorations\n .map((d): Decoration | null => {\n // @ts-expect-error: internal api\n // eslint-disable-next-line @typescript-eslint/no-unsafe-call\n return d.map(mapping, 0, 0) as Decoration | null\n })\n .filter((d): d is Decoration => d != null)\n returnCache.replace(pos, result.pos, mappedNode, updatedDecorations)\n }\n })\n\n return returnCache\n }\n}\n","import type { Node as ProseMirrorNode } from 'prosemirror-model'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { type Decoration, DecorationSet } from 'prosemirror-view'\n\nimport { DecorationCache } from './cache'\nimport type { LanguageExtractor, Parser } from './types'\n\n/**\n * Describes the current state of the highlightPlugin\n */\nexport interface HighlightPluginState {\n cache: DecorationCache\n decorations: DecorationSet\n promises: Promise<void>[]\n}\n\n/**\n * Creates a plugin that highlights the contents of all nodes (via Decorations)\n * with a type passed in blockTypes\n */\nexport function createHighlightPlugin({\n parser,\n nodeTypes = ['code_block', 'codeBlock'],\n languageExtractor = (node) => node.attrs.language as string | undefined,\n}: {\n /**\n * A function that returns an array of decorations for the given node text\n * content, language, and position.\n */\n parser: Parser\n\n /**\n * An array containing all the node type name to target for highlighting.\n *\n * @default ['code_block', 'codeBlock']\n */\n nodeTypes?: string[]\n\n /**\n * A function that returns the language string to use when highlighting that\n * node. By default, it returns `node.attrs.language`.\n */\n languageExtractor?: LanguageExtractor\n}): Plugin<HighlightPluginState> {\n const key = new PluginKey<HighlightPluginState>('prosemirror-highlight')\n\n return new Plugin<HighlightPluginState>({\n key,\n state: {\n init(_, instance) {\n const cache = new DecorationCache()\n const [decorations, promises] = calculateDecoration(\n instance.doc,\n parser,\n nodeTypes,\n languageExtractor,\n cache,\n )\n\n return { cache, decorations, promises }\n },\n apply: (tr, data) => {\n const cache = data.cache.invalidate(tr)\n const refresh = !!tr.getMeta('prosemirror-highlight-refresh')\n\n if (!tr.docChanged && !refresh) {\n const decorations = data.decorations.map(tr.mapping, tr.doc)\n const promises = data.promises\n return { cache, decorations, promises }\n }\n\n const [decorations, promises] = calculateDecoration(\n tr.doc,\n parser,\n nodeTypes,\n languageExtractor,\n cache,\n )\n return { cache, decorations, promises }\n },\n },\n view: (view) => {\n const promises = new Set<Promise<void>>()\n\n // Refresh the decorations when all promises resolve\n const refresh = () => {\n if (promises.size > 0) {\n return\n }\n const tr = view.state.tr.setMeta('prosemirror-highlight-refresh', true)\n view.dispatch(tr)\n }\n\n const check = () => {\n const state = key.getState(view.state)\n\n for (const promise of state?.promises ?? []) {\n promises.add(promise)\n promise\n .then(() => {\n promises.delete(promise)\n refresh()\n })\n .catch(() => {\n promises.delete(promise)\n })\n }\n }\n\n check()\n\n return {\n update: () => {\n check()\n },\n }\n },\n props: {\n decorations(this, state) {\n return this.getState(state)?.decorations\n },\n },\n })\n}\n\nfunction calculateDecoration(\n doc: ProseMirrorNode,\n parser: Parser,\n nodeTypes: string[],\n languageExtractor: LanguageExtractor,\n cache: DecorationCache,\n) {\n const result: Decoration[] = []\n const promises: Promise<void>[] = []\n\n doc.descendants((node, pos) => {\n if (!node.type.isTextblock) {\n return true\n }\n\n if (nodeTypes.includes(node.type.name)) {\n const language = languageExtractor(node)\n const cached = cache.get(pos)\n\n if (cached) {\n const [_, decorations] = cached\n result.push(...decorations)\n } else {\n const decorations = parser({\n content: node.textContent,\n language: language || undefined,\n pos,\n size: node.nodeSize,\n })\n\n if (decorations && Array.isArray(decorations)) {\n cache.set(pos, node, decorations)\n result.push(...decorations)\n } else if (decorations instanceof Promise) {\n cache.remove(pos)\n promises.push(decorations)\n }\n }\n }\n return false\n })\n\n return [DecorationSet.create(doc, result), promises] as const\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/cache.ts","../src/plugin.ts"],"sourcesContent":["import type { Node as ProseMirrorNode } from 'prosemirror-model'\nimport type { Transaction } from 'prosemirror-state'\nimport type { Decoration } from 'prosemirror-view'\n\n/**\n * Represents a cache of doc positions to the node and decorations at that position\n */\nexport class DecorationCache {\n private cache: Map<number, [node: ProseMirrorNode, decorations: Decoration[]]>\n\n constructor(\n cache?: Map<number, [node: ProseMirrorNode, decorations: Decoration[]]>,\n ) {\n this.cache = new Map(cache)\n }\n\n /**\n * Gets the cache entry at the given doc position, or null if it doesn't exist\n * @param pos The doc position of the node you want the cache for\n */\n get(pos: number) {\n return this.cache.get(pos)\n }\n\n /**\n * Sets the cache entry at the given position with the give node/decoration\n * values\n * @param pos The doc position of the node to set the cache for\n * @param node The node to place in cache\n * @param decorations The decorations to place in cache\n */\n set(pos: number, node: ProseMirrorNode, decorations: Decoration[]): void {\n if (pos < 0) {\n return\n }\n\n this.cache.set(pos, [node, decorations])\n }\n\n /**\n * Removes the value at the oldPos (if it exists) and sets the new position to\n * the given values\n * @param oldPos The old node position to overwrite\n * @param newPos The new node position to set the cache for\n * @param node The new node to place in cache\n * @param decorations The new decorations to place in cache\n */\n private replace(\n oldPos: number,\n newPos: number,\n node: ProseMirrorNode,\n decorations: Decoration[],\n ): void {\n this.remove(oldPos)\n this.set(newPos, node, decorations)\n }\n\n /**\n * Removes the cache entry at the given position\n * @param pos The doc position to remove from cache\n */\n remove(pos: number): void {\n this.cache.delete(pos)\n }\n\n /**\n * Invalidates the cache by removing all decoration entries on nodes that have\n * changed, updating the positions of the nodes that haven't and removing all\n * the entries that have been deleted; NOTE: this does not affect the current\n * cache, but returns an entirely new one\n * @param tr A transaction to map the current cache to\n */\n invalidate(tr: Transaction): DecorationCache {\n const returnCache = new DecorationCache(this.cache)\n const mapping = tr.mapping\n\n this.cache.forEach(([node, decorations], pos) => {\n if (pos < 0) {\n return\n }\n\n const result = mapping.mapResult(pos)\n const mappedNode = tr.doc.nodeAt(result.pos)\n\n if (result.deleted || !mappedNode?.eq(node)) {\n returnCache.remove(pos)\n } else if (pos !== result.pos) {\n // update the decorations' from/to values to match the new node position\n const updatedDecorations = decorations\n .map((d): Decoration | null => {\n // @ts-expect-error: internal api\n // eslint-disable-next-line @typescript-eslint/no-unsafe-call\n return d.map(mapping, 0, 0) as Decoration | null\n })\n .filter((d): d is Decoration => d != null)\n returnCache.replace(pos, result.pos, mappedNode, updatedDecorations)\n }\n })\n\n return returnCache\n }\n}\n","import type { Node as ProseMirrorNode } from 'prosemirror-model'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { type Decoration, DecorationSet } from 'prosemirror-view'\n\nimport { DecorationCache } from './cache'\nimport type { LanguageExtractor, Parser } from './types'\n\n/**\n * Describes the current state of the highlightPlugin\n */\nexport interface HighlightPluginState {\n cache: DecorationCache\n decorations: DecorationSet | undefined\n promises: Promise<void>[]\n}\n\n/**\n * Creates a plugin that highlights the contents of all nodes (via Decorations)\n * with a type passed in blockTypes\n */\nexport function createHighlightPlugin({\n parser,\n nodeTypes = ['code_block', 'codeBlock'],\n languageExtractor = (node) => node.attrs.language as string | undefined,\n}: {\n /**\n * A function that returns an array of decorations for the given node text\n * content, language, and position.\n */\n parser: Parser\n\n /**\n * An array containing all the node type name to target for highlighting.\n *\n * @default ['code_block', 'codeBlock']\n */\n nodeTypes?: string[]\n\n /**\n * A function that returns the language string to use when highlighting that\n * node. By default, it returns `node.attrs.language`.\n */\n languageExtractor?: LanguageExtractor\n}): Plugin<HighlightPluginState> {\n const key = new PluginKey<HighlightPluginState>('prosemirror-highlight')\n\n return new Plugin<HighlightPluginState>({\n key,\n state: {\n init(_, instance) {\n const cache = new DecorationCache()\n const [decorations, promises] = calculateDecoration(\n instance.doc,\n parser,\n nodeTypes,\n languageExtractor,\n cache,\n )\n\n return { cache, decorations, promises }\n },\n apply: (tr, data) => {\n const cache = data.cache.invalidate(tr)\n const refresh = !!tr.getMeta('prosemirror-highlight-refresh')\n\n if (!tr.docChanged && !refresh) {\n const decorations = data.decorations?.map(tr.mapping, tr.doc)\n const promises = data.promises\n return { cache, decorations, promises }\n }\n\n const [decorations, promises] = calculateDecoration(\n tr.doc,\n parser,\n nodeTypes,\n languageExtractor,\n cache,\n )\n return { cache, decorations, promises }\n },\n },\n view: (view) => {\n const promises = new Set<Promise<void>>()\n\n // Refresh the decorations when all promises resolve\n const refresh = () => {\n if (promises.size > 0) {\n return\n }\n const tr = view.state.tr.setMeta('prosemirror-highlight-refresh', true)\n view.dispatch(tr)\n }\n\n const check = () => {\n const state = key.getState(view.state)\n\n for (const promise of state?.promises ?? []) {\n promises.add(promise)\n promise\n .then(() => {\n promises.delete(promise)\n refresh()\n })\n .catch((error) => {\n console.error(\n '[prosemirror-highlight] Error resolving parser:',\n error,\n )\n promises.delete(promise)\n })\n }\n }\n\n check()\n\n return {\n update: () => {\n check()\n },\n }\n },\n props: {\n decorations(this, state) {\n return this.getState(state)?.decorations\n },\n },\n })\n}\n\nfunction calculateDecoration(\n doc: ProseMirrorNode,\n parser: Parser,\n nodeTypes: string[],\n languageExtractor: LanguageExtractor,\n cache: DecorationCache,\n): [DecorationSet | undefined, Promise<void>[]] {\n const allDecorations: Decoration[][] = []\n const promises: Promise<void>[] = []\n const nodes = collectCodeBlocks(doc, nodeTypes)\n\n try {\n for (const [node, pos] of nodes) {\n const language = languageExtractor(node)\n const cached = cache.get(pos)\n\n if (cached) {\n const [_, decorations] = cached\n if (decorations.length > 0) {\n allDecorations.push(decorations)\n }\n } else {\n const parsed = parser({\n content: node.textContent,\n language: language || undefined,\n pos,\n size: node.nodeSize,\n })\n if (parsed && Array.isArray(parsed)) {\n cache.set(pos, node, parsed)\n if (parsed.length > 0) {\n allDecorations.push(parsed)\n }\n } else if (parsed instanceof Promise) {\n cache.remove(pos)\n promises.push(parsed)\n } else {\n console.error(\n `[prosemirror-highlight] Invalid parser result:`,\n parsed,\n )\n }\n }\n }\n } catch (error) {\n console.error(`[prosemirror-highlight] Error parsing code blocks:`, error)\n }\n\n const decorationSet =\n allDecorations.length > 0\n ? DecorationSet.create(doc, allDecorations.flat())\n : undefined\n return [decorationSet, promises]\n}\n\nfunction collectCodeBlocks(\n doc: ProseMirrorNode,\n nodeTypes: string[],\n): Array<[node: ProseMirrorNode, pos: number]> {\n const nodes: Array<[node: ProseMirrorNode, pos: number]> = []\n doc.descendants((node, pos) => {\n if (node.type.isTextblock && nodeTypes.includes(node.type.name)) {\n nodes.push([node, pos])\n return false\n }\n })\n return nodes\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prosemirror-highlight",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"description": "A ProseMirror plugin to highlight code blocks",
|
|
6
6
|
"author": "ocavue <ocavue@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@ocavue/eslint-config": "^3.11.2",
|
|
99
99
|
"@ocavue/tsconfig": "^0.6.3",
|
|
100
|
-
"@shikijs/types": "^3.
|
|
100
|
+
"@shikijs/types": "^3.22.0",
|
|
101
101
|
"@types/hast": "^3.0.4",
|
|
102
102
|
"@types/node": "^24.0.0",
|
|
103
103
|
"eslint": "^9.39.2",
|
|
@@ -105,20 +105,20 @@
|
|
|
105
105
|
"jsdom": "^28.0.0",
|
|
106
106
|
"lowlight": "^3.3.0",
|
|
107
107
|
"pkg-pr-new": "^0.0.63",
|
|
108
|
-
"prettier": "^3.8.
|
|
108
|
+
"prettier": "^3.8.1",
|
|
109
109
|
"prosemirror-example-setup": "^1.2.3",
|
|
110
|
-
"prosemirror-model": "^1.25.
|
|
110
|
+
"prosemirror-model": "^1.25.4",
|
|
111
111
|
"prosemirror-schema-basic": "^1.2.4",
|
|
112
|
-
"prosemirror-state": "^1.4.
|
|
113
|
-
"prosemirror-transform": "^1.
|
|
114
|
-
"prosemirror-view": "^1.41.
|
|
112
|
+
"prosemirror-state": "^1.4.4",
|
|
113
|
+
"prosemirror-transform": "^1.11.0",
|
|
114
|
+
"prosemirror-view": "^1.41.6",
|
|
115
115
|
"refractor": "^5.0.0",
|
|
116
|
-
"shiki": "^3.
|
|
117
|
-
"sugar-high": "^0.9.
|
|
116
|
+
"shiki": "^3.22.0",
|
|
117
|
+
"sugar-high": "^0.9.5",
|
|
118
118
|
"tsdown": "^0.20.3",
|
|
119
119
|
"typescript": "^5.9.3",
|
|
120
|
-
"vite": "^7.3.
|
|
121
|
-
"vitest": "^4.0.
|
|
120
|
+
"vite": "^7.3.1",
|
|
121
|
+
"vitest": "^4.0.18"
|
|
122
122
|
},
|
|
123
123
|
"scripts": {
|
|
124
124
|
"dev": "vite",
|