@brillout/docpress 0.15.11 → 0.15.12

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.
Files changed (30) hide show
  1. package/Layout.tsx +3 -0
  2. package/autoScrollNav.ts +3 -3
  3. package/{components → code-blocks/components}/CodeSnippets.css +1 -5
  4. package/{components → code-blocks/components}/CodeSnippets.tsx +3 -2
  5. package/{components → code-blocks/components}/Pre.tsx +1 -3
  6. package/{components → code-blocks/hooks}/useMDXComponents.tsx +2 -2
  7. package/{components/CodeSnippets → code-blocks/hooks}/useSelectCodeLang.ts +14 -0
  8. package/{rehypeMetaToProps.ts → code-blocks/rehypeMetaToProps.ts} +2 -2
  9. package/{remarkDetype.ts → code-blocks/remarkDetype.ts} +31 -11
  10. package/code-blocks/shikiTransformerAutoLinks.ts +61 -0
  11. package/components/index.ts +0 -1
  12. package/css/index.css +0 -1
  13. package/dist/code-blocks/rehypeMetaToProps.d.ts +35 -0
  14. package/dist/{rehypeMetaToProps.js → code-blocks/rehypeMetaToProps.js} +2 -2
  15. package/dist/{remarkDetype.js → code-blocks/remarkDetype.js} +25 -7
  16. package/dist/code-blocks/shikiTransformerAutoLinks.d.ts +8 -0
  17. package/dist/code-blocks/shikiTransformerAutoLinks.js +51 -0
  18. package/dist/components/index.d.ts +0 -1
  19. package/dist/components/index.js +0 -1
  20. package/dist/vite.config.js +8 -3
  21. package/index.ts +3 -11
  22. package/package.json +3 -2
  23. package/vite.config.ts +8 -3
  24. package/dist/components/CodeSnippets/useSelectCodeLang.d.ts +0 -7
  25. package/dist/components/CodeSnippets/useSelectCodeLang.js +0 -50
  26. package/dist/components/CodeSnippets.d.ts +0 -11
  27. package/dist/components/CodeSnippets.js +0 -35
  28. package/dist/rehypeMetaToProps.d.ts +0 -19
  29. /package/{components → code-blocks/components}/Pre.css +0 -0
  30. /package/dist/{remarkDetype.d.ts → code-blocks/remarkDetype.d.ts} +0 -0
package/Layout.tsx CHANGED
@@ -15,6 +15,7 @@ import { ExternalLinks } from './ExternalLinks'
15
15
  import { coseMenuModalOnMouseLeave, openMenuModal, toggleMenuModal } from './MenuModal/toggleMenuModal'
16
16
  import { MenuModal } from './MenuModal'
17
17
  import { autoScrollNav_SSR } from './autoScrollNav'
18
+ import { initializeJsToggle_SSR } from './code-blocks/hooks/useSelectCodeLang'
18
19
  import { SearchLink } from './docsearch/SearchLink'
19
20
  import { navigate } from 'vike/client/router'
20
21
  import { css } from './utils/css'
@@ -78,6 +79,8 @@ function Layout({ children }: { children: React.ReactNode }) {
78
79
  <NavHead />
79
80
  {content}
80
81
  </div>
82
+ {/* Early toggling, to avoid layout jumps */}
83
+ <script dangerouslySetInnerHTML={{ __html: initializeJsToggle_SSR }}></script>
81
84
  </div>
82
85
  )
83
86
  }
package/autoScrollNav.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { autoScrollNav }
2
- export const autoScrollNav_SSR = `autoScrollNav();${autoScrollNav.toString()}`
2
+ export { autoScrollNav_SSR }
3
3
 
4
- // - We cannot use TypeScript syntax because of autoScrollNav_SSR
4
+ // - WARNING: We cannot use TypeScript here because we serialize the function.
5
5
  // - We have to save & restore `document.documentElement.scrollTop` because scrollIntoView() scrolls the main view. (I don't know why).
6
6
  // - Failed alternatives:
7
7
  // - scrollIntoViewIfNeeded() (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded) would work (it doesn't scroll the main view) but Firefox doesn't support it.
@@ -17,7 +17,7 @@ export const autoScrollNav_SSR = `autoScrollNav();${autoScrollNav.toString()}`
17
17
  // // ...
18
18
  // document.documentElement.style.overflow = overflowOriginal
19
19
  // ```
20
-
20
+ const autoScrollNav_SSR = `autoScrollNav();${autoScrollNav.toString()}`
21
21
  function autoScrollNav() {
22
22
  const nav = document.querySelector('#nav-left .navigation-content')
23
23
  if (!nav) return
@@ -8,11 +8,6 @@
8
8
  }
9
9
  }
10
10
 
11
- /* Hide language toggle for YAML */
12
- &:has(code[data-language='yaml']) .code-lang-toggle {
13
- display: none !important;
14
- }
15
-
16
11
  /* Language toggle styles */
17
12
  .code-lang-toggle {
18
13
  position: absolute !important;
@@ -28,6 +23,7 @@
28
23
  margin: 0;
29
24
  padding: 0 4px;
30
25
  height: 25px;
26
+ width: 59px;
31
27
  display: flex;
32
28
  background-color: #f7f7f7;
33
29
  opacity: 0;
@@ -5,7 +5,7 @@ export { TypescriptOnly }
5
5
  export { CodeSnippets }
6
6
 
7
7
  import React, { useEffect, useRef } from 'react'
8
- import { useSelectCodeLang } from './CodeSnippets/useSelectCodeLang'
8
+ import { useSelectCodeLang } from '../hooks/useSelectCodeLang'
9
9
  import './CodeSnippets.css'
10
10
 
11
11
  /** Only show if TypeScript is selected */
@@ -14,7 +14,7 @@ function TypescriptOnly({ children }: { children: React.ReactNode }) {
14
14
  return <div style={{ display: codeLangSelected === 'ts' ? 'block' : 'none' }}>{children}</div>
15
15
  }
16
16
 
17
- function CodeSnippets({ children }: { children: React.ReactNode }) {
17
+ function CodeSnippets({ children, hideToggle = false }: { children: React.ReactNode; hideToggle: boolean }) {
18
18
  const [codeLangSelected, selectCodeLang] = useSelectCodeLang()
19
19
  const prevPositionRef = useRef<null | { top: number; el: Element }>(null)
20
20
 
@@ -35,6 +35,7 @@ function CodeSnippets({ children }: { children: React.ReactNode }) {
35
35
  type="checkbox"
36
36
  name="code-lang-toggle"
37
37
  className="code-lang-toggle raised"
38
+ style={{ display: hideToggle ? 'none' : undefined }}
38
39
  checked={codeLangSelected === 'ts'}
39
40
  onChange={onChange}
40
41
  title="Toggle language"
@@ -1,9 +1,7 @@
1
1
  export { Pre }
2
2
 
3
3
  import React, { ComponentPropsWithoutRef, useState } from 'react'
4
- /* Importing it here chokes the tests. I don't know why.
5
4
  import './Pre.css'
6
- //*/
7
5
 
8
6
  function Pre({ children, ...props }: ComponentPropsWithoutRef<'pre'> & { 'hide-menu'?: string }) {
9
7
  return (
@@ -26,7 +24,7 @@ function CopyButton() {
26
24
  const icon =
27
25
  isSuccess === null ? (
28
26
  // Copy icon
29
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
27
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="black" strokeWidth="2">
30
28
  <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
31
29
  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
32
30
  </svg>
@@ -2,8 +2,8 @@ export { useMDXComponents }
2
2
 
3
3
  import React from 'react'
4
4
  import type { UseMdxComponents } from '@mdx-js/mdx'
5
- import { Pre } from './Pre.js'
6
- import { CodeSnippets } from './CodeSnippets.js'
5
+ import { Pre } from '../components/Pre.js'
6
+ import { CodeSnippets } from '../components/CodeSnippets.js'
7
7
 
8
8
  const useMDXComponents: UseMdxComponents = () => {
9
9
  return {
@@ -1,4 +1,5 @@
1
1
  export { useSelectCodeLang }
2
+ export { initializeJsToggle_SSR }
2
3
 
3
4
  import { useState, useEffect, useCallback } from 'react'
4
5
  import { assertWarning } from '../../utils/assert'
@@ -53,6 +54,19 @@ function useSelectCodeLang() {
53
54
  return [codeLangSelected, selectCodeLang] as const
54
55
  }
55
56
 
57
+ // WARNING: We cannot use the variables storageKey nor codeLangDefaultClient here: closures
58
+ // don't work because we serialize the function.
59
+ // WARNING: We cannot use TypeScript here, for the same reason.
60
+ const initializeJsToggle_SSR = `initializeJsToggle();${initializeJsToggle.toString()};`
61
+ function initializeJsToggle() {
62
+ const codeLangSelected = localStorage.getItem('docpress:code-lang') ?? 'js'
63
+ if (codeLangSelected === 'js') {
64
+ const inputs = document.querySelectorAll('.code-lang-toggle')
65
+ // @ts-ignore
66
+ for (const input of inputs) input.checked = false
67
+ }
68
+ }
69
+
56
70
  declare global {
57
71
  interface WindowEventMap {
58
72
  'code-lang-storage': CustomEvent
@@ -1,4 +1,4 @@
1
- export { rehypeMetaToProps }
1
+ export { rehypeMetaToProps, parseMetaString }
2
2
 
3
3
  import { visit } from 'unist-util-visit'
4
4
  import type { ElementData, Root } from 'hast'
@@ -51,7 +51,7 @@ function parseMetaString(metaString: ElementData['meta']): Record<string, string
51
51
 
52
52
  const props: Record<string, string> = {}
53
53
 
54
- const keyValuePairRE = /([a-zA-Z_-]+)(?:="([^"]*)")?(?=\s|$)/g
54
+ const keyValuePairRE = /([a-zA-Z_-]+)(?:=([^"'\s]+))?(?=\s|$)/g
55
55
  for (const match of metaString.matchAll(keyValuePairRE)) {
56
56
  let [_, key, value] = match
57
57
  props[kebabCase(key)] = value || 'true'
@@ -1,9 +1,11 @@
1
1
  export { remarkDetype }
2
2
 
3
3
  import type { Root, Parent, Code } from 'mdast'
4
+ import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
4
5
  import type { VFile } from '@mdx-js/mdx/internal-create-format-aware-processors'
5
6
  import { visit } from 'unist-util-visit'
6
- import { assertUsage } from './utils/assert.js'
7
+ import { assertUsage } from '../utils/assert.js'
8
+ import { parseMetaString } from './rehypeMetaToProps.js'
7
9
  import pc from '@brillout/picocolors'
8
10
  import module from 'node:module'
9
11
  // Cannot use `import { transform } from 'detype'` as it results in errors,
@@ -13,7 +15,6 @@ const { transform: detype } = module.createRequire(import.meta.url)('detype') as
13
15
  const prettierOptions: NonNullable<Parameters<typeof detype>[2]>['prettierOptions'] = {
14
16
  semi: false,
15
17
  singleQuote: true,
16
- printWidth: 100,
17
18
  trailingComma: 'none',
18
19
  }
19
20
 
@@ -65,22 +66,29 @@ function transformYaml(node: CodeNode) {
65
66
  }
66
67
 
67
68
  // Wrap both the original YAML and `yamlJsCode` with <CodeSnippets>
68
- const yamlContainer = {
69
- type: 'mdxJsxFlowElement' as const,
69
+ const yamlContainer: MdxJsxFlowElement = {
70
+ type: 'mdxJsxFlowElement',
70
71
  name: 'CodeSnippets',
71
72
  children: [yamlJsCode, codeBlock],
72
- attributes: [],
73
+ attributes: [
74
+ {
75
+ name: 'hideToggle',
76
+ type: 'mdxJsxAttribute',
77
+ },
78
+ ],
73
79
  }
74
80
  parent.children.splice(index, 1, yamlContainer)
75
81
  }
76
82
 
77
83
  async function transformTsToJs(node: CodeNode, file: VFile) {
78
84
  const { codeBlock, index, parent } = node
79
- let codeBlockContentJs = replaceFileNameSuffixes(codeBlock.value)
85
+ const maxWidth = Number(parseMetaString(codeBlock.meta)['max-width'])
86
+ let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value)
87
+ let codeBlockContentJs = ''
80
88
 
81
89
  // Remove TypeScript from the TS/TSX/Vue code node
82
90
  try {
83
- codeBlockContentJs = await detype(codeBlockContentJs, `some-dummy-filename.${codeBlock.lang}`, {
91
+ codeBlockContentJs = await detype(codeBlockReplacedJs, `some-dummy-filename.${codeBlock.lang}`, {
84
92
  customizeBabelConfig(config) {
85
93
  // Add `onlyRemoveTypeImports: true` to the internal `@babel/preset-typescript` config
86
94
  // https://github.com/cyco130/detype/blob/46ec867e9efd31d31a312a215ca169bd6bff4726/src/transform.ts#L206
@@ -88,7 +96,10 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
88
96
  config.presets = [[config.presets[0], { onlyRemoveTypeImports: true }]]
89
97
  },
90
98
  removeTsComments: true,
91
- prettierOptions,
99
+ prettierOptions: {
100
+ ...prettierOptions,
101
+ printWidth: maxWidth ? maxWidth : 99,
102
+ },
92
103
  })
93
104
  } catch (error) {
94
105
  // Log errors and return original content instead of throwing
@@ -113,6 +124,7 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
113
124
  if (codeBlockContentJs === codeBlock.value) return
114
125
 
115
126
  const { position, lang, ...rest } = codeBlock
127
+ const attributes: MdxJsxFlowElement['attributes'] = []
116
128
 
117
129
  const jsCode: Code = {
118
130
  ...rest,
@@ -121,12 +133,20 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
121
133
  value: codeBlockContentJs,
122
134
  }
123
135
 
136
+ // Add `hideToggle` attribute (prop) to `CodeSnippets` if the only change was replacing `.ts` with `.js`
137
+ if (codeBlockReplacedJs === codeBlockContentJs) {
138
+ attributes.push({
139
+ name: 'hideToggle',
140
+ type: 'mdxJsxAttribute',
141
+ })
142
+ }
143
+
124
144
  // Wrap both the original `codeBlock` and `jsCode` with <CodeSnippets>
125
- const container = {
126
- type: 'mdxJsxFlowElement' as const,
145
+ const container: MdxJsxFlowElement = {
146
+ type: 'mdxJsxFlowElement',
127
147
  name: 'CodeSnippets',
128
148
  children: [jsCode, codeBlock],
129
- attributes: [],
149
+ attributes,
130
150
  }
131
151
  parent.children.splice(index, 1, container)
132
152
  }
@@ -0,0 +1,61 @@
1
+ export { shikiTransformerAutoLinks }
2
+
3
+ import type { ShikiTransformer } from 'shiki'
4
+
5
+ const linkRE = /https:\/\/[^\s]*[^.,\s"'`]/g
6
+
7
+ /**
8
+ * A Shiki transformer that converts plain HTTPS URLs in code blocks into clickable `<a>` links.
9
+ *
10
+ * Inspired by `@jcayzac/shiki-transformer-autolinks`, but tailored for a narrower use case.
11
+ */
12
+ function shikiTransformerAutoLinks(): ShikiTransformer {
13
+ return {
14
+ name: 'docpress-shiki-autolinks',
15
+ span(span) {
16
+ if (span.children.length !== 1) return
17
+ let child = span.children[0]
18
+ if (child.type !== 'text') return
19
+
20
+ const links: { href: string; index: number }[] = []
21
+ const matches = Array.from(child.value.matchAll(linkRE))
22
+
23
+ // Filter out URLs that contain `${...}`. e.g. `https://star-wars.brillout.com/api/films/${id}.json`.
24
+ const filtered = matches.filter(([href]) => !href.includes('${'))
25
+ if (filtered.length === 0) return
26
+
27
+ for (const match of filtered) {
28
+ const [href] = match
29
+ links.unshift({ href, index: match.index })
30
+ }
31
+
32
+ const newChildren: typeof span.children = []
33
+ for (const { href, index } of links) {
34
+ const postIndex = index + href.length
35
+ const postValue = child.value.slice(postIndex)
36
+
37
+ if (postValue.length > 0) {
38
+ newChildren.unshift({ type: 'text', value: postValue })
39
+ }
40
+
41
+ newChildren.unshift({
42
+ type: 'element',
43
+ tagName: 'a',
44
+ properties: { href },
45
+ children: [{ type: 'text', value: href }],
46
+ })
47
+
48
+ child = {
49
+ type: 'text',
50
+ value: child.value.slice(0, index),
51
+ }
52
+ }
53
+
54
+ if (child.value.length > 0) {
55
+ newChildren.unshift(child)
56
+ }
57
+
58
+ span.children = newChildren
59
+ },
60
+ }
61
+ }
@@ -8,4 +8,3 @@ export * from './HorizontalLine'
8
8
  export * from './CodeBlockTransformer'
9
9
  export * from './Comment'
10
10
  export * from './FileRemoved'
11
- export * from './CodeSnippets'
package/css/index.css CHANGED
@@ -7,5 +7,4 @@
7
7
  @import './code.css';
8
8
  @import './table.css';
9
9
  @import './tooltip.css';
10
- @import '../components/Pre.css';
11
10
  @import '@docsearch/css';
@@ -0,0 +1,35 @@
1
+ export { rehypeMetaToProps, parseMetaString };
2
+ import type { ElementData, Root } from 'hast';
3
+ /**
4
+ * Rehype plugin to extract metadata from `<code>` blocks in markdown
5
+ * and attach them as props to the parent `<pre>` element.
6
+ *
7
+ * This allows using those props inside a custom `<Pre>` component.
8
+ *
9
+ * Example:
10
+ * ~~~mdx
11
+ * ```js foo="bar" hide_copy='true'
12
+ * export function add(a, b) {
13
+ * return a + b
14
+ * }
15
+ * ```
16
+ * ~~~
17
+ * These props are then added to the `<pre>` element
18
+ */
19
+ declare function rehypeMetaToProps(): (tree: Root) => void;
20
+ /**
21
+ * Minimal parser for a metadata string into key-value pairs.
22
+ *
23
+ * Supports simple patterns: key or key="value".
24
+ *
25
+ * Keys must contain only letters, dashes, or underscores (no digits).
26
+ * Keys are converted to kebab-case. Values default to "true" if missing.
27
+ *
28
+ * Example:
29
+ * parseMetaString('foo fooBar="value"')
30
+ * => { foo: 'true', foo_bar: 'value' }
31
+ *
32
+ * @param metaString - The input metadata string.
33
+ * @returns A plain object of parsed key-value pairs.
34
+ */
35
+ declare function parseMetaString(metaString: ElementData['meta']): Record<string, string>;
@@ -1,4 +1,4 @@
1
- export { rehypeMetaToProps };
1
+ export { rehypeMetaToProps, parseMetaString };
2
2
  import { visit } from 'unist-util-visit';
3
3
  /**
4
4
  * Rehype plugin to extract metadata from `<code>` blocks in markdown
@@ -46,7 +46,7 @@ function parseMetaString(metaString) {
46
46
  if (!metaString)
47
47
  return {};
48
48
  const props = {};
49
- const keyValuePairRE = /([a-zA-Z_-]+)(?:="([^"]*)")?(?=\s|$)/g;
49
+ const keyValuePairRE = /([a-zA-Z_-]+)(?:=([^"'\s]+))?(?=\s|$)/g;
50
50
  for (const match of metaString.matchAll(keyValuePairRE)) {
51
51
  let [_, key, value] = match;
52
52
  props[kebabCase(key)] = value || 'true';
@@ -1,6 +1,7 @@
1
1
  export { remarkDetype };
2
2
  import { visit } from 'unist-util-visit';
3
- import { assertUsage } from './utils/assert.js';
3
+ import { assertUsage } from '../utils/assert.js';
4
+ import { parseMetaString } from './rehypeMetaToProps.js';
4
5
  import pc from '@brillout/picocolors';
5
6
  import module from 'node:module';
6
7
  // Cannot use `import { transform } from 'detype'` as it results in errors,
@@ -9,7 +10,6 @@ const { transform: detype } = module.createRequire(import.meta.url)('detype');
9
10
  const prettierOptions = {
10
11
  semi: false,
11
12
  singleQuote: true,
12
- printWidth: 100,
13
13
  trailingComma: 'none',
14
14
  };
15
15
  function remarkDetype() {
@@ -54,16 +54,23 @@ function transformYaml(node) {
54
54
  type: 'mdxJsxFlowElement',
55
55
  name: 'CodeSnippets',
56
56
  children: [yamlJsCode, codeBlock],
57
- attributes: [],
57
+ attributes: [
58
+ {
59
+ name: 'hideToggle',
60
+ type: 'mdxJsxAttribute',
61
+ },
62
+ ],
58
63
  };
59
64
  parent.children.splice(index, 1, yamlContainer);
60
65
  }
61
66
  async function transformTsToJs(node, file) {
62
67
  const { codeBlock, index, parent } = node;
63
- let codeBlockContentJs = replaceFileNameSuffixes(codeBlock.value);
68
+ const maxWidth = Number(parseMetaString(codeBlock.meta)['max-width']);
69
+ let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value);
70
+ let codeBlockContentJs = '';
64
71
  // Remove TypeScript from the TS/TSX/Vue code node
65
72
  try {
66
- codeBlockContentJs = await detype(codeBlockContentJs, `some-dummy-filename.${codeBlock.lang}`, {
73
+ codeBlockContentJs = await detype(codeBlockReplacedJs, `some-dummy-filename.${codeBlock.lang}`, {
67
74
  customizeBabelConfig(config) {
68
75
  // Add `onlyRemoveTypeImports: true` to the internal `@babel/preset-typescript` config
69
76
  // https://github.com/cyco130/detype/blob/46ec867e9efd31d31a312a215ca169bd6bff4726/src/transform.ts#L206
@@ -71,7 +78,10 @@ async function transformTsToJs(node, file) {
71
78
  config.presets = [[config.presets[0], { onlyRemoveTypeImports: true }]];
72
79
  },
73
80
  removeTsComments: true,
74
- prettierOptions,
81
+ prettierOptions: {
82
+ ...prettierOptions,
83
+ printWidth: maxWidth ? maxWidth : 99,
84
+ },
75
85
  });
76
86
  }
77
87
  catch (error) {
@@ -93,18 +103,26 @@ async function transformTsToJs(node, file) {
93
103
  if (codeBlockContentJs === codeBlock.value)
94
104
  return;
95
105
  const { position, lang, ...rest } = codeBlock;
106
+ const attributes = [];
96
107
  const jsCode = {
97
108
  ...rest,
98
109
  // The jsCode lang should be js|jsx|vue
99
110
  lang: lang.replace('t', 'j'),
100
111
  value: codeBlockContentJs,
101
112
  };
113
+ // Add `hideToggle` attribute (prop) to `CodeSnippets` if the only change was replacing `.ts` with `.js`
114
+ if (codeBlockReplacedJs === codeBlockContentJs) {
115
+ attributes.push({
116
+ name: 'hideToggle',
117
+ type: 'mdxJsxAttribute',
118
+ });
119
+ }
102
120
  // Wrap both the original `codeBlock` and `jsCode` with <CodeSnippets>
103
121
  const container = {
104
122
  type: 'mdxJsxFlowElement',
105
123
  name: 'CodeSnippets',
106
124
  children: [jsCode, codeBlock],
107
- attributes: [],
125
+ attributes,
108
126
  };
109
127
  parent.children.splice(index, 1, container);
110
128
  }
@@ -0,0 +1,8 @@
1
+ export { shikiTransformerAutoLinks };
2
+ import type { ShikiTransformer } from 'shiki';
3
+ /**
4
+ * A Shiki transformer that converts plain HTTPS URLs in code blocks into clickable `<a>` links.
5
+ *
6
+ * Inspired by `@jcayzac/shiki-transformer-autolinks`, but tailored for a narrower use case.
7
+ */
8
+ declare function shikiTransformerAutoLinks(): ShikiTransformer;
@@ -0,0 +1,51 @@
1
+ export { shikiTransformerAutoLinks };
2
+ const linkRE = /https:\/\/[^\s]*[^.,\s"'`]/g;
3
+ /**
4
+ * A Shiki transformer that converts plain HTTPS URLs in code blocks into clickable `<a>` links.
5
+ *
6
+ * Inspired by `@jcayzac/shiki-transformer-autolinks`, but tailored for a narrower use case.
7
+ */
8
+ function shikiTransformerAutoLinks() {
9
+ return {
10
+ name: 'docpress-shiki-autolinks',
11
+ span(span) {
12
+ if (span.children.length !== 1)
13
+ return;
14
+ let child = span.children[0];
15
+ if (child.type !== 'text')
16
+ return;
17
+ const links = [];
18
+ const matches = Array.from(child.value.matchAll(linkRE));
19
+ // Filter out URLs that contain `${...}`. e.g. `https://star-wars.brillout.com/api/films/${id}.json`.
20
+ const filtered = matches.filter(([href]) => !href.includes('${'));
21
+ if (filtered.length === 0)
22
+ return;
23
+ for (const match of filtered) {
24
+ const [href] = match;
25
+ links.unshift({ href, index: match.index });
26
+ }
27
+ const newChildren = [];
28
+ for (const { href, index } of links) {
29
+ const postIndex = index + href.length;
30
+ const postValue = child.value.slice(postIndex);
31
+ if (postValue.length > 0) {
32
+ newChildren.unshift({ type: 'text', value: postValue });
33
+ }
34
+ newChildren.unshift({
35
+ type: 'element',
36
+ tagName: 'a',
37
+ properties: { href },
38
+ children: [{ type: 'text', value: href }],
39
+ });
40
+ child = {
41
+ type: 'text',
42
+ value: child.value.slice(0, index),
43
+ };
44
+ }
45
+ if (child.value.length > 0) {
46
+ newChildren.unshift(child);
47
+ }
48
+ span.children = newChildren;
49
+ },
50
+ };
51
+ }
@@ -8,4 +8,3 @@ export * from './HorizontalLine';
8
8
  export * from './CodeBlockTransformer';
9
9
  export * from './Comment';
10
10
  export * from './FileRemoved';
11
- export * from './CodeSnippets';
@@ -8,4 +8,3 @@ export * from './HorizontalLine';
8
8
  export * from './CodeBlockTransformer';
9
9
  export * from './Comment';
10
10
  export * from './FileRemoved';
11
- export * from './CodeSnippets';
@@ -5,12 +5,17 @@ import { parsePageSections } from './parsePageSections.js';
5
5
  import rehypePrettyCode from 'rehype-pretty-code';
6
6
  import remarkGfm from 'remark-gfm';
7
7
  import { transformerNotationDiff } from '@shikijs/transformers';
8
- import { remarkDetype } from './remarkDetype.js';
9
- import { rehypeMetaToProps } from './rehypeMetaToProps.js';
8
+ import { rehypeMetaToProps } from './code-blocks/rehypeMetaToProps.js';
9
+ import { remarkDetype } from './code-blocks/remarkDetype.js';
10
+ import { shikiTransformerAutoLinks } from './code-blocks/shikiTransformerAutoLinks.js';
10
11
  const root = process.cwd();
11
12
  const prettyCode = [
12
13
  rehypePrettyCode,
13
- { theme: 'github-light', keepBackground: false, transformers: [transformerNotationDiff()] },
14
+ {
15
+ theme: 'github-light',
16
+ keepBackground: false,
17
+ transformers: [transformerNotationDiff(), shikiTransformerAutoLinks()],
18
+ },
14
19
  ];
15
20
  const rehypePlugins = [prettyCode, [rehypeMetaToProps]];
16
21
  const remarkPlugins = [remarkGfm, remarkDetype];
package/index.ts CHANGED
@@ -1,16 +1,8 @@
1
1
  /**********/
2
2
  /* PUBLIC */
3
3
  /**********/
4
- export {
5
- CodeBlockTransformer,
6
- Link,
7
- RepoLink,
8
- FileAdded,
9
- FileRemoved,
10
- ImportMeta,
11
- Emoji,
12
- TypescriptOnly,
13
- } from './components'
4
+ export { CodeBlockTransformer, Link, RepoLink, FileAdded, FileRemoved, ImportMeta, Emoji } from './components'
5
+ export { TypescriptOnly } from './code-blocks/components/CodeSnippets'
14
6
  export { MenuToggle } from './Layout'
15
7
  export * from './components/Note'
16
8
  export * from './icons/index'
@@ -28,4 +20,4 @@ export { usePageContext } from './renderer/usePageContext'
28
20
  /************/
29
21
  // We provide our own `useMDXComponents()` to enable MDX component injection by setting `providerImportSource` to '@brillout/docpress'.
30
22
  // https://mdxjs.com/guides/injecting-components/
31
- export { useMDXComponents } from './components/useMDXComponents'
23
+ export { useMDXComponents } from './code-blocks/hooks/useMDXComponents'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brillout/docpress",
3
- "version": "0.15.11",
3
+ "version": "0.15.12",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@brillout/picocolors": "^1.0.10",
@@ -64,7 +64,8 @@
64
64
  "@types/mdast": "^4.0.4",
65
65
  "@types/node": "^22.5.5",
66
66
  "@types/react": "^18.3.8",
67
- "@types/react-dom": "^18.3.0"
67
+ "@types/react-dom": "^18.3.0",
68
+ "mdast-util-mdx-jsx": "^3.2.0"
68
69
  },
69
70
  "repository": "https://github.com/brillout/docpress",
70
71
  "license": "MIT",
package/vite.config.ts CHANGED
@@ -7,13 +7,18 @@ import { parsePageSections } from './parsePageSections.js'
7
7
  import rehypePrettyCode from 'rehype-pretty-code'
8
8
  import remarkGfm from 'remark-gfm'
9
9
  import { transformerNotationDiff } from '@shikijs/transformers'
10
- import { remarkDetype } from './remarkDetype.js'
11
- import { rehypeMetaToProps } from './rehypeMetaToProps.js'
10
+ import { rehypeMetaToProps } from './code-blocks/rehypeMetaToProps.js'
11
+ import { remarkDetype } from './code-blocks/remarkDetype.js'
12
+ import { shikiTransformerAutoLinks } from './code-blocks/shikiTransformerAutoLinks.js'
12
13
 
13
14
  const root = process.cwd()
14
15
  const prettyCode = [
15
16
  rehypePrettyCode,
16
- { theme: 'github-light', keepBackground: false, transformers: [transformerNotationDiff()] },
17
+ {
18
+ theme: 'github-light',
19
+ keepBackground: false,
20
+ transformers: [transformerNotationDiff(), shikiTransformerAutoLinks()],
21
+ },
17
22
  ]
18
23
  const rehypePlugins: any = [prettyCode, [rehypeMetaToProps]]
19
24
  const remarkPlugins = [remarkGfm, remarkDetype]
@@ -1,7 +0,0 @@
1
- export { useSelectCodeLang };
2
- declare function useSelectCodeLang(): readonly [string, (value: string) => void];
3
- declare global {
4
- interface WindowEventMap {
5
- 'code-lang-storage': CustomEvent;
6
- }
7
- }
@@ -1,50 +0,0 @@
1
- export { useSelectCodeLang };
2
- import { useState, useEffect, useCallback } from 'react';
3
- import { assertWarning } from '../../utils/assert';
4
- const storageKey = 'docpress:code-lang';
5
- const codeLangDefaultSsr = 'ts';
6
- const codeLangDefaultClient = 'js';
7
- function useSelectCodeLang() {
8
- const [codeLangSelected, setCodeLangSelected] = useState(codeLangDefaultSsr);
9
- const updateState = () => {
10
- setCodeLangSelected(getCodeLangStorage());
11
- };
12
- const updateStateOnStorageEvent = (event) => {
13
- if (event.key === storageKey)
14
- updateState();
15
- };
16
- const getCodeLangStorage = () => {
17
- try {
18
- return window.localStorage.getItem(storageKey) ?? codeLangDefaultClient;
19
- }
20
- catch (error) {
21
- console.error(error);
22
- assertWarning(false, 'Error reading from localStorage');
23
- return codeLangDefaultClient;
24
- }
25
- };
26
- const selectCodeLang = useCallback((value) => {
27
- try {
28
- window.localStorage.setItem(storageKey, value);
29
- setCodeLangSelected(value);
30
- window.dispatchEvent(new CustomEvent('code-lang-storage'));
31
- }
32
- catch (error) {
33
- console.error(error);
34
- assertWarning(false, 'Error setting localStorage');
35
- }
36
- }, []);
37
- useEffect(() => {
38
- // Initial load from localStorage
39
- updateState();
40
- // Update code lang in current tab
41
- window.addEventListener('code-lang-storage', updateState);
42
- // Update code lang if changed in another tab
43
- window.addEventListener('storage', updateStateOnStorageEvent);
44
- return () => {
45
- window.removeEventListener('code-lang-storage', updateState);
46
- window.removeEventListener('storage', updateStateOnStorageEvent);
47
- };
48
- }, []);
49
- return [codeLangSelected, selectCodeLang];
50
- }
@@ -1,11 +0,0 @@
1
- export { TypescriptOnly };
2
- export { CodeSnippets };
3
- import React from 'react';
4
- import './CodeSnippets.css';
5
- /** Only show if TypeScript is selected */
6
- declare function TypescriptOnly({ children }: {
7
- children: React.ReactNode;
8
- }): React.JSX.Element;
9
- declare function CodeSnippets({ children }: {
10
- children: React.ReactNode;
11
- }): React.JSX.Element;
@@ -1,35 +0,0 @@
1
- // Public
2
- export { TypescriptOnly };
3
- // Internal
4
- export { CodeSnippets };
5
- import React, { useEffect, useRef } from 'react';
6
- import { useSelectCodeLang } from './CodeSnippets/useSelectCodeLang';
7
- import './CodeSnippets.css';
8
- /** Only show if TypeScript is selected */
9
- function TypescriptOnly({ children }) {
10
- const [codeLangSelected] = useSelectCodeLang();
11
- return React.createElement("div", { style: { display: codeLangSelected === 'ts' ? 'block' : 'none' } }, children);
12
- }
13
- function CodeSnippets({ children }) {
14
- const [codeLangSelected, selectCodeLang] = useSelectCodeLang();
15
- const prevPositionRef = useRef(null);
16
- // Restores the scroll position of the toggle element after toggling languages.
17
- useEffect(() => {
18
- if (!prevPositionRef.current)
19
- return;
20
- const { top, el } = prevPositionRef.current;
21
- const delta = el.getBoundingClientRect().top - top;
22
- if (delta !== 0) {
23
- window.scrollBy(0, delta);
24
- }
25
- prevPositionRef.current = null;
26
- }, [codeLangSelected]);
27
- return (React.createElement("div", { className: "code-snippets" },
28
- React.createElement("input", { type: "checkbox", name: "code-lang-toggle", className: "code-lang-toggle raised", checked: codeLangSelected === 'ts', onChange: onChange, title: "Toggle language" }),
29
- children));
30
- function onChange(e) {
31
- const element = e.target;
32
- prevPositionRef.current = { top: element.getBoundingClientRect().top, el: element };
33
- selectCodeLang(element.checked ? 'ts' : 'js');
34
- }
35
- }
@@ -1,19 +0,0 @@
1
- export { rehypeMetaToProps };
2
- import type { Root } from 'hast';
3
- /**
4
- * Rehype plugin to extract metadata from `<code>` blocks in markdown
5
- * and attach them as props to the parent `<pre>` element.
6
- *
7
- * This allows using those props inside a custom `<Pre>` component.
8
- *
9
- * Example:
10
- * ~~~mdx
11
- * ```js foo="bar" hide_copy='true'
12
- * export function add(a, b) {
13
- * return a + b
14
- * }
15
- * ```
16
- * ~~~
17
- * These props are then added to the `<pre>` element
18
- */
19
- declare function rehypeMetaToProps(): (tree: Root) => void;
File without changes