prosemirror-highlight 0.3.2 → 0.4.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 CHANGED
@@ -8,13 +8,16 @@ Highlight your code blocks in [ProseMirror], with any syntax highlighter you lik
8
8
 
9
9
  ### With [Shiki]
10
10
 
11
+ <details>
12
+ <summary>Static loading of a fixed set of languages</summary>
13
+
11
14
  ```ts
12
15
  import { getHighlighter, setCDN } from 'shiki'
13
16
 
14
17
  import { createHighlightPlugin } from 'prosemirror-highlight'
15
18
  import { createParser } from 'prosemirror-highlight/shiki'
16
19
 
17
- setCDN('https://unpkg.com/shiki@0.14.6/')
20
+ setCDN('https://unpkg.com/shiki@0.14.7/')
18
21
 
19
22
  const highlighter = await getHighlighter({
20
23
  theme: 'github-light',
@@ -24,8 +27,70 @@ const parser = createParser(highlighter)
24
27
  export const shikiPlugin = createHighlightPlugin({ parser })
25
28
  ```
26
29
 
30
+ </details>
31
+
32
+ <details>
33
+ <summary>Dynamic loading of arbitrary languages</summary>
34
+
35
+ ```ts
36
+ import { getHighlighter, setCDN, type Highlighter, type Lang } from 'shiki'
37
+
38
+ import { createHighlightPlugin } from 'prosemirror-highlight'
39
+ import { createParser, type Parser } from 'prosemirror-highlight/shiki'
40
+
41
+ setCDN('https://unpkg.com/shiki@0.14.7/')
42
+
43
+ let highlighterPromise: Promise<void> | undefined
44
+ let highlighter: Highlighter | undefined
45
+ let parser: Parser | undefined
46
+ const loadedLanguages = new Set<string>()
47
+
48
+ /**
49
+ * Lazy load highlighter and highlighter languages.
50
+ *
51
+ * When the highlighter or the required language is not loaded, it returns a
52
+ * promise that resolves when the highlighter or the language is loaded.
53
+ * Otherwise, it returns an array of decorations.
54
+ */
55
+ const lazyParser: Parser = (options) => {
56
+ if (!highlighterPromise) {
57
+ highlighterPromise = getHighlighter({
58
+ themes: ['github-light'],
59
+ langs: [],
60
+ }).then((h) => {
61
+ highlighter = h
62
+ })
63
+ return highlighterPromise
64
+ }
65
+
66
+ if (!highlighter) {
67
+ return highlighterPromise
68
+ }
69
+
70
+ const language = options.language
71
+ if (language && !loadedLanguages.has(language)) {
72
+ return highlighter.loadLanguage(language as Lang).then(() => {
73
+ loadedLanguages.add(language)
74
+ })
75
+ }
76
+
77
+ if (!parser) {
78
+ parser = createParser(highlighter)
79
+ }
80
+
81
+ return parser(options)
82
+ }
83
+
84
+ export const shikiLazyPlugin = createHighlightPlugin({ parser: lazyParser })
85
+ ```
86
+
87
+ </details>
88
+
27
89
  ### With [Shikiji]
28
90
 
91
+ <details>
92
+ <summary>Static loading of a fixed set of languages</summary>
93
+
29
94
  ```ts
30
95
  import { getHighlighter } from 'shikiji'
31
96
 
@@ -40,8 +105,68 @@ const parser = createParser(highlighter)
40
105
  export const shikijiPlugin = createHighlightPlugin({ parser })
41
106
  ```
42
107
 
108
+ </details>
109
+
110
+ <details>
111
+ <summary>Dynamic loading of arbitrary languages</summary>
112
+
113
+ ```ts
114
+ import { getHighlighter, type Highlighter, type BuiltinLanguage } from 'shikiji'
115
+
116
+ import { createHighlightPlugin, type Parser } from 'prosemirror-highlight'
117
+ import { createParser } from 'prosemirror-highlight/shikiji'
118
+
119
+ let highlighterPromise: Promise<void> | undefined
120
+ let highlighter: Highlighter | undefined
121
+ let parser: Parser | undefined
122
+ const loadedLanguages = new Set<string>()
123
+
124
+ /**
125
+ * Lazy load highlighter and highlighter languages.
126
+ *
127
+ * When the highlighter or the required language is not loaded, it returns a
128
+ * promise that resolves when the highlighter or the language is loaded.
129
+ * Otherwise, it returns an array of decorations.
130
+ */
131
+ const lazyParser: Parser = (options) => {
132
+ if (!highlighterPromise) {
133
+ highlighterPromise = getHighlighter({
134
+ themes: ['vitesse-light'],
135
+ langs: [],
136
+ }).then((h) => {
137
+ highlighter = h
138
+ })
139
+ return highlighterPromise
140
+ }
141
+
142
+ if (!highlighter) {
143
+ return highlighterPromise
144
+ }
145
+
146
+ const language = options.language
147
+ if (language && !loadedLanguages.has(language)) {
148
+ return highlighter.loadLanguage(language as BuiltinLanguage).then(() => {
149
+ loadedLanguages.add(language)
150
+ })
151
+ }
152
+
153
+ if (!parser) {
154
+ parser = createParser(highlighter)
155
+ }
156
+
157
+ return parser(options)
158
+ }
159
+
160
+ export const shikijiLazyPlugin = createHighlightPlugin({ parser: lazyParser })
161
+ ```
162
+
163
+ </details>
164
+
43
165
  ### With [lowlight] (based on [Highlight.js])
44
166
 
167
+ <details>
168
+ <summary>Static loading of all languages</summary>
169
+
45
170
  ```ts
46
171
  import 'highlight.js/styles/default.css'
47
172
 
@@ -55,8 +180,13 @@ const parser = createParser(lowlight)
55
180
  export const lowlightPlugin = createHighlightPlugin({ parser })
56
181
  ```
57
182
 
183
+ </details>
184
+
58
185
  ### With [refractor] (based on [Prism])
59
186
 
187
+ <details>
188
+ <summary>Static loading of all languages</summary>
189
+
60
190
  ```ts
61
191
  import { refractor } from 'refractor'
62
192
 
@@ -67,6 +197,8 @@ const parser = createParser(refractor)
67
197
  export const refractorPlugin = createHighlightPlugin({ parser })
68
198
  ```
69
199
 
200
+ </details>
201
+
70
202
  ## Online demo
71
203
 
72
204
  [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/ocavue/prosemirror-highlight?file=playground%2Fmain.ts)
package/dist/index.d.ts CHANGED
@@ -1,25 +1,19 @@
1
1
  import { Node } from 'prosemirror-model';
2
2
  import { Transaction, Plugin } from 'prosemirror-state';
3
3
  import { Decoration, DecorationSet } from 'prosemirror-view';
4
- import { P as Parser, L as LanguageExtractor } from './types-wUmJTPF3.js';
4
+ import { P as Parser, L as LanguageExtractor } from './types-hA0ujWQ1.js';
5
5
 
6
6
  /**
7
7
  * Represents a cache of doc positions to the node and decorations at that position
8
8
  */
9
9
  declare class DecorationCache {
10
10
  private cache;
11
- constructor(cache?: Map<number, {
12
- node: Node;
13
- decorations: Decoration[];
14
- }>);
11
+ constructor(cache?: Map<number, [node: Node, decorations: Decoration[]]>);
15
12
  /**
16
13
  * Gets the cache entry at the given doc position, or null if it doesn't exist
17
14
  * @param pos The doc position of the node you want the cache for
18
15
  */
19
- get(pos: number): {
20
- node: Node;
21
- decorations: Decoration[];
22
- } | undefined;
16
+ get(pos: number): [node: Node, decorations: Decoration[]] | undefined;
23
17
  /**
24
18
  * Sets the cache entry at the given position with the give node/decoration
25
19
  * values
@@ -41,7 +35,7 @@ declare class DecorationCache {
41
35
  * Removes the cache entry at the given position
42
36
  * @param pos The doc position to remove from cache
43
37
  */
44
- private remove;
38
+ remove(pos: number): void;
45
39
  /**
46
40
  * Invalidates the cache by removing all decoration entries on nodes that have
47
41
  * changed, updating the positions of the nodes that haven't and removing all
@@ -58,6 +52,7 @@ declare class DecorationCache {
58
52
  interface HighlightPluginState {
59
53
  cache: DecorationCache;
60
54
  decorations: DecorationSet;
55
+ promises: Promise<void>[];
61
56
  }
62
57
  /**
63
58
  * Creates a plugin that highlights the contents of all nodes (via Decorations)
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ var DecorationCache = class _DecorationCache {
24
24
  if (pos < 0) {
25
25
  return;
26
26
  }
27
- this.cache.set(pos, { node, decorations });
27
+ this.cache.set(pos, [node, decorations]);
28
28
  }
29
29
  /**
30
30
  * Removes the value at the oldPos (if it exists) and sets the new position to
@@ -55,7 +55,7 @@ var DecorationCache = class _DecorationCache {
55
55
  invalidate(tr) {
56
56
  const returnCache = new _DecorationCache(this.cache);
57
57
  const mapping = tr.mapping;
58
- this.cache.forEach(({ node, decorations }, pos) => {
58
+ this.cache.forEach(([node, decorations], pos) => {
59
59
  if (pos < 0) {
60
60
  return;
61
61
  }
@@ -89,31 +89,62 @@ function createHighlightPlugin({
89
89
  state: {
90
90
  init(_, instance) {
91
91
  const cache = new DecorationCache();
92
- const decorations = getDecorationSet(
92
+ const [decorations, promises] = calculateDecoration(
93
93
  instance.doc,
94
94
  parser,
95
95
  nodeTypes,
96
96
  languageExtractor,
97
97
  cache
98
98
  );
99
- return { cache, decorations };
99
+ return { cache, decorations, promises };
100
100
  },
101
- apply(tr, data) {
101
+ apply: (tr, data) => {
102
102
  const cache = data.cache.invalidate(tr);
103
- if (!tr.docChanged) {
103
+ const refresh = !!tr.getMeta("prosemirror-highlight-refresh");
104
+ if (!tr.docChanged && !refresh) {
104
105
  const decorations2 = data.decorations.map(tr.mapping, tr.doc);
105
- return { cache, decorations: decorations2 };
106
+ const promises2 = data.promises;
107
+ return { cache, decorations: decorations2, promises: promises2 };
106
108
  }
107
- const decorations = getDecorationSet(
109
+ const [decorations, promises] = calculateDecoration(
108
110
  tr.doc,
109
111
  parser,
110
112
  nodeTypes,
111
113
  languageExtractor,
112
114
  cache
113
115
  );
114
- return { cache, decorations };
116
+ return { cache, decorations, promises };
115
117
  }
116
118
  },
119
+ view: (view) => {
120
+ const promises = /* @__PURE__ */ new Set();
121
+ const refresh = () => {
122
+ if (promises.size > 0) {
123
+ return;
124
+ }
125
+ const tr = view.state.tr.setMeta("prosemirror-highlight-refresh", true);
126
+ view.dispatch(tr);
127
+ };
128
+ const check = () => {
129
+ var _a;
130
+ const state = key.getState(view.state);
131
+ for (const promise of (_a = state == null ? void 0 : state.promises) != null ? _a : []) {
132
+ promises.add(promise);
133
+ promise.then(() => {
134
+ promises.delete(promise);
135
+ refresh();
136
+ }).catch(() => {
137
+ promises.delete(promise);
138
+ });
139
+ }
140
+ };
141
+ check();
142
+ return {
143
+ update: () => {
144
+ check();
145
+ }
146
+ };
147
+ },
117
148
  props: {
118
149
  decorations(state) {
119
150
  var _a;
@@ -122,8 +153,9 @@ function createHighlightPlugin({
122
153
  }
123
154
  });
124
155
  }
125
- function getDecorationSet(doc, parser, nodeTypes, languageExtractor, cache) {
156
+ function calculateDecoration(doc, parser, nodeTypes, languageExtractor, cache) {
126
157
  const result = [];
158
+ const promises = [];
127
159
  doc.descendants((node, pos) => {
128
160
  if (!node.type.isTextblock) {
129
161
  return true;
@@ -132,20 +164,26 @@ function getDecorationSet(doc, parser, nodeTypes, languageExtractor, cache) {
132
164
  const language = languageExtractor(node);
133
165
  const cached = cache.get(pos);
134
166
  if (cached) {
135
- result.push(...cached.decorations);
167
+ const [_, decorations] = cached;
168
+ result.push(...decorations);
136
169
  } else {
137
170
  const decorations = parser({
138
171
  content: node.textContent,
139
172
  language: language || void 0,
140
173
  pos
141
174
  });
142
- cache.set(pos, node, decorations);
143
- result.push(...decorations);
175
+ if (decorations && Array.isArray(decorations)) {
176
+ cache.set(pos, node, decorations);
177
+ result.push(...decorations);
178
+ } else if (decorations instanceof Promise) {
179
+ cache.remove(pos);
180
+ promises.push(decorations);
181
+ }
144
182
  }
145
183
  }
146
184
  return false;
147
185
  });
148
- return DecorationSet.create(doc, result);
186
+ return [DecorationSet.create(doc, result), promises];
149
187
  }
150
188
  export {
151
189
  DecorationCache,
@@ -1,5 +1,5 @@
1
1
  import { Root } from 'hast';
2
- import { P as Parser } from './types-wUmJTPF3.js';
2
+ import { P as Parser } from './types-hA0ujWQ1.js';
3
3
  import 'prosemirror-model';
4
4
  import 'prosemirror-view';
5
5
 
@@ -9,4 +9,4 @@ type Lowlight = {
9
9
  };
10
10
  declare function createParser(lowlight: Lowlight): Parser;
11
11
 
12
- export { type Lowlight, createParser };
12
+ export { type Lowlight, Parser, createParser };
@@ -1,8 +1,8 @@
1
1
  import { Refractor } from 'refractor/lib/core';
2
- import { P as Parser } from './types-wUmJTPF3.js';
2
+ import { P as Parser } from './types-hA0ujWQ1.js';
3
3
  import 'prosemirror-model';
4
4
  import 'prosemirror-view';
5
5
 
6
6
  declare function createParser(refractor: Refractor): Parser;
7
7
 
8
- export { createParser };
8
+ export { Parser, createParser };
package/dist/shiki.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Highlighter } from 'shiki';
2
- import { P as Parser } from './types-wUmJTPF3.js';
2
+ import { P as Parser } from './types-hA0ujWQ1.js';
3
3
  import 'prosemirror-model';
4
4
  import 'prosemirror-view';
5
5
 
6
6
  declare function createParser(highlighter: Highlighter): Parser;
7
7
 
8
- export { createParser };
8
+ export { Parser, createParser };
package/dist/shikiji.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Highlighter } from 'shikiji';
2
- import { P as Parser } from './types-wUmJTPF3.js';
2
+ import { P as Parser } from './types-hA0ujWQ1.js';
3
3
  import 'prosemirror-model';
4
4
  import 'prosemirror-view';
5
5
 
6
6
  declare function createParser(highlighter: Highlighter): Parser;
7
7
 
8
- export { createParser };
8
+ export { Parser, createParser };
package/dist/shikiji.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // src/shikiji.ts
2
2
  import { Decoration } from "prosemirror-view";
3
- import "shikiji";
4
3
  function createParser(highlighter) {
5
4
  return function parser({ content, language, pos }) {
6
5
  const decorations = [];
@@ -1,6 +1,11 @@
1
1
  import { Node } from 'prosemirror-model';
2
2
  import { Decoration } from 'prosemirror-view';
3
3
 
4
+ /**
5
+ * A function that parses the text content of a code block node and returns an
6
+ * array of decorations. If the underlying syntax highlighter is still loading,
7
+ * you can return a promise that will be resolved when the highlighter is ready.
8
+ */
4
9
  type Parser = (options: {
5
10
  /**
6
11
  * The text content of the code block node.
@@ -14,7 +19,10 @@ type Parser = (options: {
14
19
  * The language of the code block node.
15
20
  */
16
21
  language?: string;
17
- }) => Decoration[];
22
+ }) => Decoration[] | Promise<void>;
23
+ /**
24
+ * A function that extracts the language of a code block node.
25
+ */
18
26
  type LanguageExtractor = (node: Node) => string | undefined;
19
27
 
20
28
  export type { LanguageExtractor as L, Parser as P };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "prosemirror-highlight",
3
3
  "type": "module",
4
- "version": "0.3.2",
5
- "packageManager": "pnpm@8.12.0",
4
+ "version": "0.4.0",
5
+ "packageManager": "pnpm@8.13.1",
6
6
  "description": "A ProseMirror plugin to highlight code blocks",
7
7
  "author": "ocavue <ocavue@gmail.com>",
8
8
  "license": "MIT",
@@ -61,7 +61,7 @@
61
61
  "prosemirror-transform": "^1.8.0",
62
62
  "prosemirror-view": "^1.32.4",
63
63
  "refractor": "^4.8.1",
64
- "shiki": "^0.14.6",
64
+ "shiki": "^0.14.0",
65
65
  "shikiji": "^0.8.0 || ^0.9.0"
66
66
  },
67
67
  "peerDependenciesMeta": {
@@ -100,8 +100,8 @@
100
100
  "@antfu/ni": "^0.21.12",
101
101
  "@ocavue/eslint-config": "^1.4.0",
102
102
  "@types/hast": "^3.0.3",
103
- "@types/node": "^20.10.4",
104
- "eslint": "^8.55.0",
103
+ "@types/node": "^20.10.6",
104
+ "eslint": "^8.56.0",
105
105
  "highlight.js": "^11.9.0",
106
106
  "jsdom": "^23.0.1",
107
107
  "lowlight": "^3.1.0",
@@ -111,14 +111,14 @@
111
111
  "prosemirror-schema-basic": "^1.2.2",
112
112
  "prosemirror-state": "^1.4.3",
113
113
  "prosemirror-transform": "^1.8.0",
114
- "prosemirror-view": "^1.32.6",
114
+ "prosemirror-view": "^1.32.7",
115
115
  "refractor": "^4.8.1",
116
- "shiki": "^0.14.6",
117
- "shikiji": "^0.8.0",
116
+ "shiki": "^0.14.7",
117
+ "shikiji": "^0.9.16",
118
118
  "tsup": "^8.0.1",
119
119
  "typescript": "^5.3.3",
120
- "vite": "^5.0.7",
121
- "vitest": "^1.0.4"
120
+ "vite": "^5.0.10",
121
+ "vitest": "^1.1.1"
122
122
  },
123
123
  "renovate": {
124
124
  "dependencyDashboard": true,