@nuxtjs/mdc 0.1.6 → 0.2.1

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.
@@ -1,29 +1,13 @@
1
- import { getHighlighter, BUNDLED_LANGUAGES, BUNDLED_THEMES, FontStyle } from "shiki-es";
2
- import { consola } from "consola";
3
- import mdcTMLanguage from "./mdc.tmLanguage.mjs";
4
- const logger = consola.withTag("@nuxtjs/mdc");
5
- const resolveLang = (lang) => BUNDLED_LANGUAGES.find((l) => l.id === lang || l.aliases?.includes(lang));
6
- const resolveTheme = (theme) => {
7
- if (!theme) {
8
- return;
9
- }
10
- if (typeof theme === "string") {
11
- theme = {
12
- default: theme
13
- };
14
- }
15
- return Object.entries(theme).reduce((acc, [key, value]) => {
16
- acc[key] = BUNDLED_THEMES.find((t) => t === value);
17
- return acc;
18
- }, {});
19
- };
1
+ import { getHighlighter } from "shikiji";
20
2
  export const useShikiHighlighter = createSingleton((opts) => {
21
- const { theme, preload } = opts || {};
3
+ const { theme, preload, wrapperStyle } = opts || {};
22
4
  let promise;
23
5
  const getShikiHighlighter = () => {
24
6
  if (!promise) {
25
7
  promise = getHighlighter({
26
- theme: theme?.default || theme || "dark-plus",
8
+ themes: [
9
+ theme?.default || theme || "dark-plus"
10
+ ],
27
11
  langs: [
28
12
  ...preload || [],
29
13
  "diff",
@@ -36,13 +20,7 @@ export const useShikiHighlighter = createSingleton((opts) => {
36
20
  "md",
37
21
  "yaml",
38
22
  "vue",
39
- {
40
- id: "md",
41
- scopeName: "text.markdown.mdc",
42
- path: "mdc.tmLanguage.json",
43
- aliases: ["markdown", "md", "mdc"],
44
- grammar: mdcTMLanguage
45
- }
23
+ "mdc"
46
24
  ]
47
25
  }).then((highlighter) => {
48
26
  const themes = Object.values(typeof theme === "string" ? { default: theme } : theme || {});
@@ -54,239 +32,55 @@ export const useShikiHighlighter = createSingleton((opts) => {
54
32
  }
55
33
  return promise;
56
34
  };
57
- const splitCodeToLines = (code) => {
58
- const lines = code.split(/\r\n|\r|\n/);
59
- return [...lines.map((line) => [{ content: line }])];
60
- };
61
- const getHighlightedTokens = async (code, lang, theme2) => {
35
+ const getHighlightedAST = async (code, lang, theme2, opts2) => {
62
36
  const highlighter = await getShikiHighlighter();
63
- code = code.replace(/\n+$/, "");
64
- lang = resolveLang(lang || "")?.id || lang;
65
- theme2 = resolveTheme(theme2 || "") || { default: highlighter.getTheme() };
66
- const newThemes = Object.values(theme2).filter((t) => !highlighter.getLoadedThemes().includes(t));
67
- if (newThemes.length) {
68
- await Promise.all(newThemes.map(highlighter.loadTheme));
37
+ const { highlights = [] } = opts2 || {};
38
+ const themesObject = typeof theme2 === "string" ? { default: theme2 } : theme2 || {};
39
+ const themeNames = Object.values(themesObject);
40
+ if (themeNames.length) {
41
+ await Promise.all(themeNames.map((theme3) => highlighter.loadTheme(theme3)));
69
42
  }
70
- const result = {
71
- code,
72
- lang,
73
- theme: Object.fromEntries(Object.entries(theme2).map(([n, t]) => [n, highlighter.getTheme(t)])),
74
- tokens: splitCodeToLines(code)
75
- };
76
- if (!lang) {
77
- return result;
78
- }
79
- if (!highlighter.getLoadedLanguages().includes(lang)) {
80
- const languageRegistration = resolveLang(lang);
81
- if (languageRegistration) {
82
- await highlighter.loadLanguage(languageRegistration);
83
- } else {
84
- logger.warn(`Language '${lang}' is not supported by shiki. Skipping highlight.`);
85
- return result;
86
- }
87
- }
88
- const coloredTokens = Object.entries(theme2).map(([key, theme3]) => {
89
- const tokens = highlighter.codeToThemedTokens(code, lang, theme3, { includeExplanation: false }).map((line) => line.map((token) => ({
90
- content: token.content,
91
- style: {
92
- [key]: { color: token.color, fontStyle: token.fontStyle }
93
- }
94
- })));
95
- return {
96
- key,
97
- theme: theme3,
98
- tokens
99
- };
100
- });
101
- const highlightedCode = [];
102
- for (const line in coloredTokens[0].tokens) {
103
- highlightedCode[line] = coloredTokens.reduce((acc, color) => {
104
- return mergeLines({
105
- key: coloredTokens[0].key,
106
- tokens: acc
107
- }, {
108
- key: color.key,
109
- tokens: color.tokens[line]
110
- });
111
- }, coloredTokens[0].tokens[line]);
43
+ if (lang && !highlighter.getLoadedLanguages().includes(lang)) {
44
+ await highlighter.loadLanguage(lang);
112
45
  }
113
- result.tokens = highlightedCode;
114
- return result;
115
- };
116
- const getHighlightedAST = async (code, lang, theme2, opts2) => {
117
- const { tokens, theme: themeMap } = await getHighlightedTokens(code, lang, theme2);
118
- const { highlights = [], styleMap = {} } = opts2 || {};
119
- const className = Object.values(themeMap).map((th) => th.name).join("_").replace(".", "");
120
- styleMap[className] = {
121
- style: Object.entries(themeMap).reduce((acc, [key, th]) => {
122
- acc[key] = {
123
- background: th.bg,
124
- color: th.fg
125
- };
126
- return acc;
127
- }, {}),
128
- className
129
- };
130
- const tree = tokens.map((line, lineIndex) => {
131
- if (lineIndex !== tokens.length - 1) {
132
- if (line.length === 0) {
133
- line.push({ content: "" });
46
+ const root = highlighter.codeToHast(code.trimEnd(), {
47
+ lang,
48
+ themes: themesObject,
49
+ defaultColor: "default",
50
+ transforms: {
51
+ line(node, line) {
52
+ node.properties ||= {};
53
+ if (highlights.includes(line)) {
54
+ node.properties.class = (node.properties.class || "") + " highlight";
55
+ }
56
+ node.properties.line = line;
134
57
  }
135
- line[line.length - 1].content += "\n";
136
58
  }
137
- return {
138
- type: "element",
139
- tagName: "span",
140
- properties: {
141
- class: ["line", highlights.includes(lineIndex + 1) ? "highlight" : ""].join(" ").trim(),
142
- line: lineIndex + 1
143
- },
144
- children: line.map(tokenSpan)
145
- };
146
59
  });
60
+ const preEl = root.children[0];
61
+ const codeEl = preEl.children[0];
62
+ preEl.properties.style = wrapperStyle ? typeof wrapperStyle === "string" ? wrapperStyle : preEl.properties.style : "";
63
+ const style = Object.keys(themesObject).filter((color) => color !== "default").map((color) => [
64
+ wrapperStyle ? `html.${color} .shiki,` : "",
65
+ `html.${color} .shiki span {`,
66
+ `color: var(--shiki-${color}) !important;`,
67
+ `background: var(--shiki-${color}-bg) !important;`,
68
+ `font-style: var(--shiki-${color}-font-style) !important;`,
69
+ `font-weight: var(--shiki-${color}-font-weight) !important;`,
70
+ `text-decoration: var(--shiki-${color}-text-decoration) !important;`,
71
+ "}"
72
+ ].join("").trim()).join("\n");
147
73
  return {
148
- tree,
149
- className
150
- };
151
- function getSpanProps(token) {
152
- if (!token.style) {
153
- return {};
154
- }
155
- const key = Object.values(token.style).map((themeStyle) => Object.values(themeStyle).join("")).join("");
156
- if (!styleMap[key]) {
157
- styleMap[key] = {
158
- style: token.style,
159
- // Using the hash value of the style as the className,
160
- // ensure that the className remains stable over multiple compilations,
161
- // which facilitates content caching.
162
- className: "ct-" + hash(key)
163
- };
164
- }
165
- return { class: styleMap[key].className };
166
- }
167
- function tokenSpan(token) {
168
- return {
169
- type: "element",
170
- tagName: "span",
171
- properties: getSpanProps(token),
172
- children: [{ type: "text", value: token.content }]
173
- };
174
- }
175
- };
176
- const getHighlightedCode = async (code, lang, theme2, opts2) => {
177
- const styleMap = opts2?.styleMap || {};
178
- const highlights = opts2?.highlights || [];
179
- const { tree, className } = await getHighlightedAST(code, lang, theme2, { styleMap, highlights });
180
- function renderNode(node) {
181
- if (node.type === "text") {
182
- return node.value.replace(/</g, "&lt;").replace(/>/g, "&gt;");
183
- }
184
- const children = node.children.map(renderNode).join("");
185
- return `<${node.tag} class="${node.props.class}">${children}</${node.tag}>`;
186
- }
187
- return {
188
- code: tree.map(renderNode).join(""),
189
- className,
190
- styles: generateStyles(styleMap)
74
+ tree: codeEl.children,
75
+ className: preEl.properties.class,
76
+ inlineStyle: preEl.properties.style,
77
+ style
191
78
  };
192
79
  };
193
- const generateStyles = (styleMap) => {
194
- const styles = [];
195
- for (const styleToken of Object.values(styleMap)) {
196
- const defaultStyle = styleToken.style.default;
197
- const hasColor = !!defaultStyle?.color;
198
- const hasBold = isBold(defaultStyle);
199
- const hasItalic = isItalic(defaultStyle);
200
- const hasUnderline = isUnderline(defaultStyle);
201
- const themeStyles = Object.entries(styleToken.style).map(([variant, style]) => {
202
- const styleText = [
203
- // If the default theme has a style, but the current theme does not have one,
204
- // we need to override to reset style
205
- ["color", style.color || (hasColor ? "unset" : "")],
206
- ["font-weight", isBold(style) ? "bold" : hasBold ? "unset" : ""],
207
- ["font-style", isItalic(style) ? "italic" : hasItalic ? "unset" : ""],
208
- ["text-decoration", isUnderline(style) ? "bold" : hasUnderline ? "unset" : ""],
209
- ["background", style.background || ""]
210
- ].filter((kv) => kv[1]).map((kv) => kv.join(":") + ";").join("");
211
- return { variant, styleText };
212
- });
213
- const defaultThemeStyle = themeStyles.find((themeStyle) => themeStyle.variant === "default");
214
- themeStyles.forEach((themeStyle) => {
215
- if (themeStyle.variant === "default") {
216
- styles.push(`.${styleToken.className}{${themeStyle.styleText}}`);
217
- } else if (themeStyle.styleText !== defaultThemeStyle?.styleText) {
218
- styles.push(`.${themeStyle.variant} .${styleToken.className}{${themeStyle.styleText}}`);
219
- }
220
- });
221
- }
222
- return styles.join("\n");
223
- };
224
80
  return {
225
- getHighlightedTokens,
226
- getHighlightedAST,
227
- getHighlightedCode,
228
- generateStyles
81
+ getHighlightedAST
229
82
  };
230
83
  });
231
- function mergeLines(line1, line2) {
232
- const mergedTokens = [];
233
- const right = {
234
- key: line1.key,
235
- tokens: line1.tokens.slice()
236
- };
237
- const left = {
238
- key: line2.key,
239
- tokens: line2.tokens.slice()
240
- };
241
- let index = 0;
242
- while (index < right.tokens.length) {
243
- const rightToken = right.tokens[index];
244
- const leftToken = left.tokens[index];
245
- if (rightToken.content === leftToken.content) {
246
- mergedTokens.push({
247
- content: rightToken.content,
248
- style: {
249
- ...right.tokens[index].style,
250
- ...left.tokens[index].style
251
- }
252
- });
253
- index += 1;
254
- continue;
255
- }
256
- if (rightToken.content.startsWith(leftToken.content)) {
257
- const nextRightToken = {
258
- ...rightToken,
259
- content: rightToken.content.slice(leftToken.content.length)
260
- };
261
- rightToken.content = leftToken.content;
262
- right.tokens.splice(index + 1, 0, nextRightToken);
263
- continue;
264
- }
265
- if (leftToken.content.startsWith(rightToken.content)) {
266
- const nextLeftToken = {
267
- ...leftToken,
268
- content: leftToken.content.slice(rightToken.content.length)
269
- };
270
- leftToken.content = rightToken.content;
271
- left.tokens.splice(index + 1, 0, nextLeftToken);
272
- continue;
273
- }
274
- throw new Error("Unexpected token");
275
- }
276
- return mergedTokens;
277
- }
278
- function isBold(style) {
279
- return style && style.fontStyle === FontStyle.Bold;
280
- }
281
- function isItalic(style) {
282
- return style && style.fontStyle === FontStyle.Italic;
283
- }
284
- function isUnderline(style) {
285
- return style && style.fontStyle === FontStyle.Underline;
286
- }
287
- function hash(str) {
288
- return Array.from(str).reduce((s, c) => Math.imul(31, s) + c.charCodeAt(0) | 0, 0).toString().slice(-6);
289
- }
290
84
  function createSingleton(fn) {
291
85
  let instance;
292
86
  return (...args) => {
@@ -1,5 +1,5 @@
1
1
  import type { Root } from '../types/hast';
2
- import type { Highlighter, Theme } from '../shiki/types';
2
+ import type { Highlighter, Theme } from './types';
3
3
  interface RehypeShikiOption {
4
4
  theme?: Theme;
5
5
  highlighter?: Highlighter;
@@ -3,15 +3,16 @@ import { toString } from "hast-util-to-string";
3
3
  import { defu } from "defu";
4
4
  const defaults = {
5
5
  theme: {
6
- default: "github-dark",
7
- dark: "github-light"
6
+ default: "github-light",
7
+ dark: "github-dark"
8
8
  },
9
- highlighter: (code, lang, theme) => {
9
+ highlighter: (code, lang, theme, highlights) => {
10
10
  return $fetch("/api/_mdc/highlight", {
11
11
  params: {
12
12
  code,
13
13
  lang,
14
- theme: JSON.stringify(theme)
14
+ theme: JSON.stringify(theme),
15
+ highlights: JSON.stringify(highlights)
15
16
  }
16
17
  });
17
18
  }
@@ -23,17 +24,24 @@ export function rehypeShiki(opts = {}) {
23
24
  const styles = [];
24
25
  visit(
25
26
  tree,
26
- (node) => node.tagName === "pre" && !!node.properties?.language,
27
+ (node) => ["pre", "code"].includes(node.tagName) && !!node.properties?.language,
27
28
  (node) => {
28
29
  const _node = node;
29
- const task = options.highlighter(toString(node), _node.properties.language, options.theme).then(({ tree: tree2, className, style }) => {
30
+ const task = options.highlighter(
31
+ toString(node),
32
+ _node.properties.language,
33
+ options.theme,
34
+ _node.properties.highlights ?? []
35
+ ).then(({ tree: tree2, className, style, inlineStyle }) => {
30
36
  _node.properties.className = ((_node.properties.className || "") + " " + className).trim();
37
+ _node.properties.style = ((_node.properties.style || "") + " " + inlineStyle).trim();
31
38
  if (_node.children[0]?.tagName === "code") {
32
39
  _node.children[0].children = tree2;
33
40
  } else {
34
- _node.children = tree2;
41
+ _node.children = tree2[0].children;
35
42
  }
36
- styles.push(style);
43
+ if (style)
44
+ styles.push(style);
37
45
  });
38
46
  tasks.push(task);
39
47
  }
@@ -1,32 +1,13 @@
1
1
  import type { Element } from '../types/hast';
2
- import type { Theme as ShikiTheme, IThemedToken } from 'shiki-es';
3
- export type Theme = ShikiTheme | Record<string, ShikiTheme>;
4
- export type HighlightThemedTokenStyle = Pick<IThemedToken, 'color' | 'fontStyle'> & {
5
- background?: string;
6
- };
7
- export type TokenStyleMap = Record<string, {
8
- style: Record<string, HighlightThemedTokenStyle>;
9
- className: string;
10
- }>;
11
- export interface HighlightParams {
12
- code: string;
13
- lang: string;
14
- theme: Theme;
15
- }
2
+ import type { BuiltinTheme } from 'shikiji';
3
+ export type Theme = BuiltinTheme | Record<string, BuiltinTheme>;
16
4
  export interface HighlighterOptions {
17
- styleMap: TokenStyleMap;
18
- highlights: Array<number>;
19
- }
20
- export interface HighlightThemedToken {
21
- content: string;
22
- style?: Record<string, HighlightThemedTokenStyle>;
5
+ highlights: number[];
23
6
  }
24
- export interface HighlightThemedTokenLine {
25
- key: string;
26
- tokens: HighlightThemedToken[];
27
- }
28
- export type Highlighter = (code: string, lang: string, theme: Theme) => Promise<{
7
+ export interface HighlightResult {
29
8
  tree: Element[];
30
9
  className: string;
31
10
  style: string;
32
- }>;
11
+ inlineStyle: string;
12
+ }
13
+ export type Highlighter = (code: string, lang: string, theme: Theme, highlights: number[]) => Promise<HighlightResult>;
@@ -1,4 +1,4 @@
1
- import { type VNode } from 'vue';
1
+ import type { VNode } from 'vue';
2
2
  import type { MDCElement, MDCNode } from '../types';
3
3
  /**
4
4
  * List of text nodes
@@ -34,4 +34,4 @@ export declare function nodeTextContent(node: VNode | MDCNode): string;
34
34
  * @returns
35
35
  */
36
36
  export declare function unwrap(vnode: VNode, tags?: string[]): VNode | VNode[];
37
- export declare function flatUnwrap(vnodes: VNode | VNode[], tags?: string[]): Array<VNode | string>;
37
+ export declare function flatUnwrap(vnodes: VNode | VNode[], tags?: string | string[]): Array<VNode | string> | VNode;
@@ -1,4 +1,3 @@
1
- import { Text } from "vue";
2
1
  export const TEXT_TAGS = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "li"];
3
2
  export function isTag(vnode, tag) {
4
3
  if (vnode.type === tag) {
@@ -13,7 +12,7 @@ export function isTag(vnode, tag) {
13
12
  return false;
14
13
  }
15
14
  export function isText(vnode) {
16
- return isTag(vnode, "text") || isTag(vnode, Text);
15
+ return isTag(vnode, "text") || isTag(vnode, Symbol.for("v-txt"));
17
16
  }
18
17
  export function nodeChildren(node) {
19
18
  if (Array.isArray(node.children) || typeof node.children === "string") {
@@ -40,7 +39,7 @@ export function nodeTextContent(node) {
40
39
  }
41
40
  return "";
42
41
  }
43
- export function unwrap(vnode, tags = ["p"]) {
42
+ export function unwrap(vnode, tags = []) {
44
43
  if (Array.isArray(vnode)) {
45
44
  return vnode.flatMap((node) => unwrap(node, tags));
46
45
  }
@@ -53,14 +52,20 @@ export function unwrap(vnode, tags = ["p"]) {
53
52
  }
54
53
  return result;
55
54
  }
56
- function _flatUnwrap(vnodes, tags = ["p"]) {
55
+ function _flatUnwrap(vnodes, tags = []) {
57
56
  vnodes = Array.isArray(vnodes) ? vnodes : [vnodes];
58
57
  if (!tags.length) {
59
58
  return vnodes;
60
59
  }
61
60
  return vnodes.flatMap((vnode) => _flatUnwrap(unwrap(vnode, [tags[0]]), tags.slice(1))).filter((vnode) => !(isText(vnode) && nodeTextContent(vnode).trim() === ""));
62
61
  }
63
- export function flatUnwrap(vnodes, tags = ["p"]) {
62
+ export function flatUnwrap(vnodes, tags = []) {
63
+ if (typeof tags === "string") {
64
+ tags = tags.split(",").map((tag) => tag.trim()).filter(Boolean);
65
+ }
66
+ if (!tags.length) {
67
+ return vnodes;
68
+ }
64
69
  return _flatUnwrap(vnodes, tags).reduce((acc, item) => {
65
70
  if (isText(item)) {
66
71
  if (typeof acc[acc.length - 1] === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtjs/mdc",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "Nuxt MDC module",
5
5
  "repository": "nuxt-modules/mdc",
6
6
  "license": "MIT",
@@ -35,8 +35,8 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@nuxt/kit": "latest",
38
- "@types/hast": "^3.0.0",
39
- "@types/mdast": "^4.0.0",
38
+ "@types/hast": "^3.0.1",
39
+ "@types/mdast": "^4.0.1",
40
40
  "@vue/compiler-core": "^3.3.4",
41
41
  "consola": "^3.2.3",
42
42
  "defu": "^6.1.2",
@@ -47,10 +47,10 @@
47
47
  "mdast-util-to-hast": "^13.0.2",
48
48
  "micromark-util-sanitize-uri": "^2.0.0",
49
49
  "ohash": "^1.1.3",
50
- "property-information": "^6.2.0",
51
- "rehype-external-links": "^2.1.0",
50
+ "property-information": "^6.3.0",
51
+ "rehype-external-links": "^3.0.0",
52
52
  "rehype-raw": "^6.1.1",
53
- "rehype-slug": "^5.1.0",
53
+ "rehype-slug": "^6.0.0",
54
54
  "rehype-sort-attribute-values": "^5.0.0",
55
55
  "rehype-sort-attributes": "^5.0.0",
56
56
  "remark-emoji": "^4.0.0",
@@ -59,29 +59,29 @@
59
59
  "remark-parse": "^10.0.2",
60
60
  "remark-rehype": "^10.1.0",
61
61
  "scule": "^1.0.0",
62
- "shiki-es": "^0.14.0",
63
- "ufo": "^1.3.0",
64
- "unified": "^11.0.2",
62
+ "shikiji": "^0.6.8",
63
+ "ufo": "^1.3.1",
64
+ "unified": "^11.0.3",
65
65
  "unist-builder": "^4.0.0",
66
66
  "unist-util-visit": "^5.0.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@nuxt/devtools": "latest",
70
70
  "@nuxt/eslint-config": "^0.2.0",
71
- "@nuxt/module-builder": "^0.5.0",
72
- "@nuxt/schema": "^3.7.0",
73
- "@nuxt/test-utils": "^3.7.0",
71
+ "@nuxt/module-builder": "^0.5.2",
72
+ "@nuxt/schema": "^3.7.4",
73
+ "@nuxt/test-utils": "^3.7.4",
74
74
  "@nuxthq/ui": "^2.7.0",
75
- "@types/mdurl": "^1.0.2",
76
- "@types/node": "^20.5.7",
75
+ "@types/mdurl": "^1.0.3",
76
+ "@types/node": "^20.7.1",
77
77
  "changelogen": "^0.5.5",
78
- "eslint": "^8.48.0",
79
- "nuxt": "^3.7.0",
80
- "rehype": "^12.0.1",
81
- "release-it": "^16.1.5",
82
- "vitest": "^0.34.3"
78
+ "eslint": "^8.50.0",
79
+ "nuxt": "^3.7.4",
80
+ "rehype": "^13.0.1",
81
+ "release-it": "^16.2.1",
82
+ "vitest": "^0.34.5"
83
83
  },
84
- "packageManager": "pnpm@8.7.0",
84
+ "packageManager": "pnpm@8.8.0",
85
85
  "release-it": {
86
86
  "git": {
87
87
  "commitMessage": "chore(release): release v${version}"