@astrojs/markdown-remark 7.0.0-beta.6 → 7.0.0-beta.7

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.
@@ -11,6 +11,7 @@ export declare const defaultExcludeLanguages: string[];
11
11
  * @param highlighter
12
12
  * A function which receives the code and language, and returns the HTML of a syntax
13
13
  * highlighted `<pre>` element.
14
+ * @returns The number of code blocks that were highlighted.
14
15
  */
15
- export declare function highlightCodeBlocks(tree: Root, highlighter: Highlighter, excludeLanguages?: string[]): Promise<void>;
16
+ export declare function highlightCodeBlocks(tree: Root, highlighter: Highlighter, excludeLanguages?: string[]): Promise<number>;
16
17
  export {};
package/dist/highlight.js CHANGED
@@ -54,6 +54,7 @@ async function highlightCodeBlocks(tree, highlighter, excludeLanguages = []) {
54
54
  const index = grandParent.children.indexOf(parent);
55
55
  grandParent.children[index] = replacement;
56
56
  }
57
+ return nodes.length;
57
58
  }
58
59
  export {
59
60
  defaultExcludeLanguages,
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export { rehypePrism } from './rehype-prism.js';
5
5
  export { rehypeShiki } from './rehype-shiki.js';
6
6
  export { remarkCollectImages } from './remark-collect-images.js';
7
7
  export { type CreateShikiHighlighterOptions, createShikiHighlighter, type ShikiHighlighter, type ShikiHighlighterHighlightOptions, } from './shiki.js';
8
+ export { globalShikiStyleCollector } from './shiki-style-collector.js';
9
+ export { transformerStyleToClass, type ShikiTransformerStyleToClass, } from './transformers/style-to-class.js';
8
10
  export * from './types.js';
9
11
  export declare const syntaxHighlightDefaults: Required<SyntaxHighlightConfig>;
10
12
  export declare const markdownConfigDefaults: Required<AstroMarkdownOptions>;
package/dist/index.js CHANGED
@@ -25,6 +25,10 @@ import { remarkCollectImages as remarkCollectImages2 } from "./remark-collect-im
25
25
  import {
26
26
  createShikiHighlighter
27
27
  } from "./shiki.js";
28
+ import { globalShikiStyleCollector } from "./shiki-style-collector.js";
29
+ import {
30
+ transformerStyleToClass
31
+ } from "./transformers/style-to-class.js";
28
32
  export * from "./types.js";
29
33
  const syntaxHighlightDefaults = {
30
34
  type: "shiki",
@@ -118,7 +122,8 @@ async function createMarkdownProcessor(opts) {
118
122
  headings: result.data.astro?.headings ?? [],
119
123
  localImagePaths: result.data.astro?.localImagePaths ?? [],
120
124
  remoteImagePaths: result.data.astro?.remoteImagePaths ?? [],
121
- frontmatter: result.data.astro?.frontmatter ?? {}
125
+ frontmatter: result.data.astro?.frontmatter ?? {},
126
+ hasCodeBlocks: result.data.astro?.hasCodeBlocks ?? false
122
127
  }
123
128
  };
124
129
  }
@@ -145,6 +150,7 @@ export {
145
150
  createMarkdownProcessor,
146
151
  createShikiHighlighter,
147
152
  extractFrontmatter,
153
+ globalShikiStyleCollector,
148
154
  isFrontmatterValid,
149
155
  markdownConfigDefaults,
150
156
  parseFrontmatter,
@@ -152,5 +158,6 @@ export {
152
158
  rehypePrism2 as rehypePrism,
153
159
  rehypeShiki2 as rehypeShiki,
154
160
  remarkCollectImages2 as remarkCollectImages,
155
- syntaxHighlightDefaults
161
+ syntaxHighlightDefaults,
162
+ transformerStyleToClass
156
163
  };
@@ -2,7 +2,7 @@ import { highlightCodeBlocks } from "./highlight.js";
2
2
  import { createShikiHighlighter } from "./shiki.js";
3
3
  const rehypeShiki = (config, excludeLangs) => {
4
4
  let highlighterAsync;
5
- return async (tree) => {
5
+ return async (tree, vfile) => {
6
6
  highlighterAsync ??= createShikiHighlighter({
7
7
  langs: config?.langs,
8
8
  theme: config?.theme,
@@ -10,7 +10,7 @@ const rehypeShiki = (config, excludeLangs) => {
10
10
  langAlias: config?.langAlias
11
11
  });
12
12
  const highlighter = await highlighterAsync;
13
- await highlightCodeBlocks(
13
+ const codeBlockCount = await highlightCodeBlocks(
14
14
  tree,
15
15
  (code, language, options) => {
16
16
  return highlighter.codeToHast(code, language, {
@@ -22,6 +22,10 @@ const rehypeShiki = (config, excludeLangs) => {
22
22
  },
23
23
  excludeLangs
24
24
  );
25
+ if (codeBlockCount > 0) {
26
+ vfile.data.astro ??= {};
27
+ vfile.data.astro.hasCodeBlocks = true;
28
+ }
25
29
  };
26
30
  };
27
31
  export {
@@ -0,0 +1,32 @@
1
+ import type { ShikiTransformerStyleToClass } from './transformers/style-to-class.js';
2
+ /**
3
+ * Global singleton to collect Shiki styles from multiple transformer instances.
4
+ *
5
+ * Each code block (whether from Code.astro or markdown) creates a new transformer instance.
6
+ * This collector aggregates styles from all instances so they can be bundled into a single
7
+ * CSS file via the virtual module system.
8
+ */
9
+ declare class ShikiStyleCollector {
10
+ private transformers;
11
+ /**
12
+ * Register a transformer instance to collect styles from.
13
+ * Returns the same transformer to allow chaining.
14
+ */
15
+ register(transformer: ShikiTransformerStyleToClass): ShikiTransformerStyleToClass;
16
+ /**
17
+ * Collect CSS from all registered transformers.
18
+ * This is called by the virtual CSS module to generate the final stylesheet.
19
+ */
20
+ collectCSS(): string;
21
+ /**
22
+ * Clear all registered transformers and their style registries.
23
+ * Called during HMR in dev mode to prevent stale styles.
24
+ */
25
+ clear(): void;
26
+ }
27
+ /**
28
+ * Global instance of the style collector.
29
+ * Shared between Code.astro component and markdown processing.
30
+ */
31
+ export declare const globalShikiStyleCollector: ShikiStyleCollector;
32
+ export {};
@@ -0,0 +1,36 @@
1
+ class ShikiStyleCollector {
2
+ transformers = /* @__PURE__ */ new Set();
3
+ /**
4
+ * Register a transformer instance to collect styles from.
5
+ * Returns the same transformer to allow chaining.
6
+ */
7
+ register(transformer) {
8
+ this.transformers.add(transformer);
9
+ return transformer;
10
+ }
11
+ /**
12
+ * Collect CSS from all registered transformers.
13
+ * This is called by the virtual CSS module to generate the final stylesheet.
14
+ */
15
+ collectCSS() {
16
+ let css = "";
17
+ for (const transformer of this.transformers) {
18
+ css += transformer.getCSS();
19
+ }
20
+ return css;
21
+ }
22
+ /**
23
+ * Clear all registered transformers and their style registries.
24
+ * Called during HMR in dev mode to prevent stale styles.
25
+ */
26
+ clear() {
27
+ for (const transformer of this.transformers) {
28
+ transformer.clearRegistry();
29
+ }
30
+ this.transformers.clear();
31
+ }
32
+ }
33
+ const globalShikiStyleCollector = new ShikiStyleCollector();
34
+ export {
35
+ globalShikiStyleCollector
36
+ };
package/dist/shiki.js CHANGED
@@ -3,6 +3,8 @@ import {
3
3
  createHighlighter,
4
4
  isSpecialLang
5
5
  } from "shiki";
6
+ import { globalShikiStyleCollector } from "./shiki-style-collector.js";
7
+ import { transformerStyleToClass } from "./transformers/style-to-class.js";
6
8
  let _cssVariablesTheme;
7
9
  const cssVariablesTheme = () => _cssVariablesTheme ?? (_cssVariablesTheme = createCssVariablesTheme({
8
10
  variablePrefix: "--astro-code-"
@@ -52,6 +54,8 @@ async function createShikiHighlighter({
52
54
  // they're technically not meta, nor parsed from Shiki's `parseMetaString` API.
53
55
  meta: options?.meta ? { __raw: options?.meta } : void 0,
54
56
  transformers: [
57
+ // Extract inline styles to CSS classes for better performance and CSP compliance
58
+ globalShikiStyleCollector.register(transformerStyleToClass()),
55
59
  {
56
60
  pre(node) {
57
61
  if (inline) {
@@ -64,13 +68,15 @@ async function createShikiHighlighter({
64
68
  } = options?.attributes ?? {};
65
69
  Object.assign(node.properties, rest);
66
70
  const classValue = (normalizePropAsString(node.properties.class) ?? "") + (attributesClass ? ` ${attributesClass}` : "");
67
- const styleValue = (normalizePropAsString(node.properties.style) ?? "") + (attributesStyle ? `; ${attributesStyle}` : "");
68
71
  node.properties.class = classValue.replace(/shiki/g, "astro-code");
69
72
  node.properties.dataLanguage = lang;
70
73
  if (options.wrap === false || options.wrap === void 0) {
71
- node.properties.style = styleValue + "; overflow-x: auto;";
74
+ this.addClassToHast(node, "astro-code-overflow");
72
75
  } else if (options.wrap === true) {
73
- node.properties.style = styleValue + "; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;";
76
+ this.addClassToHast(node, "astro-code-overflow astro-code-wrap");
77
+ }
78
+ if (attributesStyle) {
79
+ node.properties.style = attributesStyle;
74
80
  }
75
81
  },
76
82
  line(node) {
@@ -84,7 +90,7 @@ async function createShikiHighlighter({
84
90
  innerSpanNode.children.unshift({
85
91
  type: "element",
86
92
  tagName: "span",
87
- properties: { style: "user-select: none;" },
93
+ properties: { class: "astro-code-no-select" },
88
94
  children: [{ type: "text", value: start }]
89
95
  });
90
96
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Copied and adapted from https://github.com/shikijs/shiki/blob/main/packages/transformers/src/transformers/style-to-class.ts
3
+ */
4
+ import type { ShikiTransformer } from 'shiki';
5
+ export interface ShikiTransformerStyleToClass extends ShikiTransformer {
6
+ getClassRegistry: () => Map<string, Record<string, string> | string>;
7
+ getCSS: () => string;
8
+ clearRegistry: () => void;
9
+ }
10
+ /**
11
+ * Transform Shiki inline styles to CSS classes.
12
+ * Based on @shikijs/transformers style-to-class transformer.
13
+ *
14
+ * This transformer extracts inline styles from Shiki-generated HTML and converts them
15
+ * to unique class names, allowing CSS to be collected and bundled separately for better
16
+ * performance and CSP compliance.
17
+ */
18
+ export declare function transformerStyleToClass(): ShikiTransformerStyleToClass;
@@ -0,0 +1,75 @@
1
+ function transformerStyleToClass() {
2
+ const classToStyle = /* @__PURE__ */ new Map();
3
+ function stringifyStyle(style) {
4
+ return Object.entries(style).map(([key, value]) => `${key}:${value}`).join(";");
5
+ }
6
+ function registerStyle(style) {
7
+ const str = typeof style === "string" ? style : stringifyStyle(style);
8
+ let className = "__a_" + cyrb53(str);
9
+ if (!classToStyle.has(className)) {
10
+ classToStyle.set(className, typeof style === "string" ? style : { ...style });
11
+ }
12
+ return className;
13
+ }
14
+ return {
15
+ name: "@astrojs/markdown-remark:style-to-class",
16
+ pre(node) {
17
+ if (!node.properties.style) return;
18
+ const className = registerStyle(node.properties.style);
19
+ delete node.properties.style;
20
+ this.addClassToHast(node, className);
21
+ },
22
+ tokens(lines) {
23
+ for (const line of lines) {
24
+ for (const token of line) {
25
+ let className;
26
+ if (token.htmlStyle) {
27
+ className = registerStyle(token.htmlStyle);
28
+ token.htmlStyle = {};
29
+ } else {
30
+ className = registerStyle({ color: token.color });
31
+ token.color = "";
32
+ }
33
+ token.htmlAttrs ||= {};
34
+ if (!token.htmlAttrs.class) {
35
+ token.htmlAttrs.class = className;
36
+ } else {
37
+ token.htmlAttrs.class += ` ${className}`;
38
+ }
39
+ }
40
+ }
41
+ },
42
+ getClassRegistry() {
43
+ return classToStyle;
44
+ },
45
+ getCSS() {
46
+ let css = ".astro-code-overflow{overflow-x:auto}";
47
+ css += ".astro-code-wrap{white-space:pre-wrap;word-wrap:break-word}";
48
+ css += ".astro-code-no-select{user-select:none}";
49
+ for (const [className, style] of classToStyle.entries()) {
50
+ css += `.${className}{${typeof style === "string" ? style : stringifyStyle(style)}}`;
51
+ }
52
+ return css;
53
+ },
54
+ clearRegistry() {
55
+ classToStyle.clear();
56
+ }
57
+ };
58
+ }
59
+ function cyrb53(str, seed = 0) {
60
+ let h1 = 3735928559 ^ seed;
61
+ let h2 = 1103547991 ^ seed;
62
+ for (let i = 0, ch; i < str.length; i++) {
63
+ ch = str.charCodeAt(i);
64
+ h1 = Math.imul(h1 ^ ch, 2654435761);
65
+ h2 = Math.imul(h2 ^ ch, 1597334677);
66
+ }
67
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
68
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
69
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
70
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
71
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36).slice(0, 6);
72
+ }
73
+ export {
74
+ transformerStyleToClass
75
+ };
package/dist/types.d.ts CHANGED
@@ -13,6 +13,7 @@ declare module 'vfile' {
13
13
  localImagePaths?: string[];
14
14
  remoteImagePaths?: string[];
15
15
  frontmatter?: Record<string, any>;
16
+ hasCodeBlocks?: boolean;
16
17
  };
17
18
  }
18
19
  }
@@ -66,6 +67,8 @@ export interface MarkdownProcessorRenderResult {
66
67
  localImagePaths: string[];
67
68
  remoteImagePaths: string[];
68
69
  frontmatter: Record<string, any>;
70
+ /** Whether the markdown contained code blocks that were syntax highlighted */
71
+ hasCodeBlocks?: boolean;
69
72
  };
70
73
  }
71
74
  export interface MarkdownHeading {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/markdown-remark",
3
- "version": "7.0.0-beta.6",
3
+ "version": "7.0.0-beta.7",
4
4
  "type": "module",
5
5
  "author": "withastro",
6
6
  "license": "MIT",