astro-d2 0.8.1 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # astro-d2
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#49](https://github.com/HiDeoo/astro-d2/pull/49) [`68bfe04`](https://github.com/HiDeoo/astro-d2/commit/68bfe04b6098c748beb4ffc24c595419917d5a4c) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds new `inline` attribute to override the global `inline` configuration for a specific diagram.
8
+
9
+ - [#49](https://github.com/HiDeoo/astro-d2/pull/49) [`68bfe04`](https://github.com/HiDeoo/astro-d2/commit/68bfe04b6098c748beb4ffc24c595419917d5a4c) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds support for customizing the semibold font in diagrams.
10
+
11
+ - [#49](https://github.com/HiDeoo/astro-d2/pull/49) [`68bfe04`](https://github.com/HiDeoo/astro-d2/commit/68bfe04b6098c748beb4ffc24c595419917d5a4c) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds experimental support for using [D2.js](https://www.npmjs.com/package/@terrastruct/d2) to render diagrams.
12
+
13
+ By default, the integration requires the D2 binary to be installed on the system to generate diagrams. Enabling this option allows generating diagrams using D2.js, a JavaScript wrapper around D2 to run it through WebAssembly.
14
+
15
+ To enable this feature, add the experimental flag in your Astro D2 integration configuration:
16
+
17
+ ```js
18
+ astroD2({
19
+ experimental: {
20
+ useD2js: true,
21
+ },
22
+ })
23
+ ```
24
+
3
25
  ## 0.8.1
4
26
 
5
27
  ### Patch Changes
package/config.ts CHANGED
@@ -9,6 +9,25 @@ export const AstroD2ConfigSchema = z
9
9
  * @see https://d2lang.com/tour/interactive/
10
10
  */
11
11
  appendix: z.boolean().default(false),
12
+ /**
13
+ * Available experimental flags.
14
+ *
15
+ * Note that experimental flags are not guaranteed to be stable and may change or be removed in any future release.
16
+ */
17
+ experimental: z
18
+ .object({
19
+ /**
20
+ * Whether to use D2.js to generate the diagrams instead of the D2 binary.
21
+ *
22
+ * By default, the integration requires the D2 binary to be installed on the system to generate diagrams.
23
+ * Enabling this option allows generating diagrams using D2.js, a JavaScript wrapper around D2 to run it through
24
+ * WebAssembly.
25
+ *
26
+ * @default false
27
+ */
28
+ useD2js: z.boolean().default(false),
29
+ })
30
+ .default({}),
12
31
  /**
13
32
  * Defines the fonts to use for the generated diagrams.
14
33
  *
@@ -28,6 +47,10 @@ export const AstroD2ConfigSchema = z
28
47
  * The relative path from the project's root to the .ttf font file to use for the bold font.
29
48
  */
30
49
  bold: z.string().optional(),
50
+ /**
51
+ * The relative path from the project's root to the .ttf font file to use for the semibold font.
52
+ */
53
+ semibold: z.string().optional(),
31
54
  })
32
55
  .optional(),
33
56
  /**
@@ -96,6 +119,10 @@ export const AstroD2ConfigSchema = z
96
119
  })
97
120
  .default({}),
98
121
  })
122
+ .refine((config) => config.layout !== 'tala' || !config.experimental.useD2js, {
123
+ // TODO(HiDeoo) test
124
+ message: 'The `tala` layout engine is not supported when using the `experimental.useD2js` option.',
125
+ })
99
126
  .default({})
100
127
 
101
128
  export type AstroD2UserConfig = z.input<typeof AstroD2ConfigSchema>
package/index.ts CHANGED
@@ -5,7 +5,7 @@ import type { AstroIntegration } from 'astro'
5
5
 
6
6
  import { AstroD2ConfigSchema, type AstroD2UserConfig } from './config'
7
7
  import { clearContentLayerCache } from './libs/astro'
8
- import { isD2Installed } from './libs/d2'
8
+ import { isD2BinaryInstalled } from './libs/d2'
9
9
  import { throwErrorWithHint } from './libs/integration'
10
10
  import { remarkAstroD2 } from './libs/remark'
11
11
 
@@ -33,7 +33,7 @@ export default function astroD2Integration(userConfig?: AstroD2UserConfig): Astr
33
33
  if (config.skipGeneration) {
34
34
  logger.warn("Skipping generation of D2 diagrams as the 'skipGeneration' option is enabled.")
35
35
  } else {
36
- if (!(await isD2Installed())) {
36
+ if (!config.experimental.useD2js && !(await isD2BinaryInstalled())) {
37
37
  throwErrorWithHint(
38
38
  'Could not find D2. Please check the installation instructions at https://github.com/terrastruct/d2/blob/master/docs/INSTALL.md',
39
39
  )
@@ -15,9 +15,9 @@ export const AttributesSchema = z
15
15
  .optional()
16
16
  .transform((value) => value === 'true'),
17
17
  /**
18
- * The dark theme to use for the diagrams when the user's system preference is set to dark mode.
18
+ * The dark theme to use for the diagram when the user's system preference is set to dark mode.
19
19
  *
20
- * To disable the dark theme and have all diagrams look the same, set this attribute to `'false'`.
20
+ * To disable the dark theme, set this attribute to `'false'`.
21
21
  *
22
22
  * @see https://d2lang.com/tour/themes
23
23
  */
@@ -25,6 +25,13 @@ export const AttributesSchema = z
25
25
  .string()
26
26
  .optional()
27
27
  .transform((value) => (value === 'false' ? false : value)),
28
+ /**
29
+ * Overrides the global `inline` configuration for the diagram.
30
+ */
31
+ inline: z
32
+ .union([z.literal('true'), z.literal('false')])
33
+ .optional()
34
+ .transform((value) => (value === undefined ? undefined : value === 'true')),
28
35
  /**
29
36
  * Overrides the global `layout` configuration for the diagram.
30
37
  */
@@ -57,7 +64,7 @@ export const AttributesSchema = z
57
64
  */
58
65
  title: z.string().default('Diagram'),
59
66
  /**
60
- * The default theme to use for the diagrams.
67
+ * The default theme to use for the diagram.
61
68
  *
62
69
  * @see https://d2lang.com/tour/themes
63
70
  */
package/libs/d2.ts CHANGED
@@ -2,15 +2,20 @@ import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import url from 'node:url'
4
4
 
5
+ import { D2, type CompileRequest } from '@terrastruct/d2'
6
+
5
7
  import type { DiagramAttributes } from './attributes'
6
8
  import { exec } from './exec'
7
9
  import type { RemarkAstroD2Config } from './remark'
8
10
 
9
11
  const viewBoxRegex = /viewBox="\d+ \d+ (?<width>\d+) (?<height>\d+)"/
10
12
 
11
- export async function isD2Installed() {
13
+ // When using D2.js, we cache the loaded fonts to avoid reading them for each diagram.
14
+ const jsFonts: Partial<Record<D2Font, Uint8Array>> = {}
15
+
16
+ export async function isD2BinaryInstalled() {
12
17
  try {
13
- await getD2Version()
18
+ await getD2BinaryVersion()
14
19
 
15
20
  return true
16
21
  } catch {
@@ -24,6 +29,17 @@ export async function generateD2Diagram(
24
29
  input: string,
25
30
  outputPath: string,
26
31
  cwd: string,
32
+ ) {
33
+ const generateFn = config.experimental.useD2js ? generateD2jsDiagram : generateD2BinaryDiagram
34
+ return generateFn(config, attributes, input, outputPath, cwd)
35
+ }
36
+
37
+ async function generateD2BinaryDiagram(
38
+ config: RemarkAstroD2Config,
39
+ attributes: DiagramAttributes,
40
+ input: string,
41
+ outputPath: string,
42
+ cwd: string,
27
43
  ) {
28
44
  const extraArgs = []
29
45
 
@@ -58,6 +74,12 @@ export async function generateD2Diagram(
58
74
  extraArgs.push(`--font-bold=${path.relative(cwd, path.join(url.fileURLToPath(config.root), config.fonts.bold))}`)
59
75
  }
60
76
 
77
+ if (config.fonts?.semibold) {
78
+ extraArgs.push(
79
+ `--font-semibold=${path.relative(cwd, path.join(url.fileURLToPath(config.root), config.fonts.semibold))}`,
80
+ )
81
+ }
82
+
61
83
  if ((config.appendix && attributes.appendix !== false) || attributes.appendix === true) {
62
84
  extraArgs.push(`--force-appendix`)
63
85
  }
@@ -85,26 +107,90 @@ export async function generateD2Diagram(
85
107
  return await getD2Diagram(outputPath)
86
108
  }
87
109
 
88
- export async function getD2Diagram(diagramPath: string): Promise<D2Diagram | undefined> {
110
+ async function generateD2jsDiagram(
111
+ config: RemarkAstroD2Config,
112
+ attributes: DiagramAttributes,
113
+ input: string,
114
+ outputPath: string,
115
+ ) {
89
116
  try {
90
- const content = await fs.readFile(diagramPath, 'utf8')
91
- const match = viewBoxRegex.exec(content)
92
- const { height, width } = match?.groups ?? {}
117
+ const request: CompileRequest = {
118
+ fs: { [outputPath]: input },
119
+ inputPath: outputPath,
120
+ options: {
121
+ // @ts-expect-error - We enforce that the layout cannot be 'tala' when using D2.js when validating the config.
122
+ layout: attributes.layout ?? config.layout,
123
+ pad: attributes.pad ?? config.pad,
124
+ sketch: (attributes.sketch === 'true' ? true : attributes.sketch) ?? config.sketch,
125
+ themeID: Number.parseInt(attributes.theme ?? config.theme.default, 10),
126
+ },
127
+ }
128
+
129
+ const darkTheme = attributes.darkTheme ?? config.theme.dark
130
+ if (darkTheme !== false) request.options.darkThemeID = Number.parseInt(darkTheme, 10)
131
+
132
+ if (attributes.animateInterval) request.options.animateInterval = Number.parseInt(attributes.animateInterval, 10)
93
133
 
94
- if (!height || !width) {
95
- return
134
+ if (attributes.target !== undefined) request.options.target = attributes.target
135
+ else if (attributes.animateInterval) request.options.target = '*'
136
+
137
+ if (config.fonts?.regular) {
138
+ request.options.fontRegular = await getD2jsFont('regular', config.root, config.fonts.regular)
96
139
  }
97
140
 
98
- const computedHeight = Number.parseInt(height, 10)
99
- const computedWidth = Number.parseInt(width, 10)
141
+ if (config.fonts?.italic) {
142
+ request.options.fontItalic = await getD2jsFont('italic', config.root, config.fonts.italic)
143
+ }
100
144
 
101
- return { content, size: { height: computedHeight, width: computedWidth } }
145
+ if (config.fonts?.bold) {
146
+ request.options.fontBold = await getD2jsFont('bold', config.root, config.fonts.bold)
147
+ }
148
+
149
+ if (config.fonts?.semibold) {
150
+ request.options.fontSemibold = await getD2jsFont('semibold', config.root, config.fonts.semibold)
151
+ }
152
+
153
+ if ((config.appendix && attributes.appendix !== false) || attributes.appendix === true) {
154
+ request.options.forceAppendix = true
155
+ }
156
+
157
+ const d2 = new D2()
158
+ const response = await d2.compile(request)
159
+ const content = await d2.render(response.diagram, response.renderOptions)
160
+
161
+ await fs.mkdir(path.dirname(outputPath), { recursive: true })
162
+ await fs.writeFile(outputPath, content)
163
+
164
+ return getD2DiagramFromContent(content)
165
+ } catch (error) {
166
+ throw new Error('Failed to generate D2 diagram using D2.js.', { cause: error })
167
+ }
168
+ }
169
+
170
+ export async function getD2Diagram(diagramPath: string): Promise<D2Diagram | undefined> {
171
+ try {
172
+ const content = await fs.readFile(diagramPath, 'utf8')
173
+ return getD2DiagramFromContent(content)
102
174
  } catch (error) {
103
175
  throw new Error(`Failed to get D2 diagram size at '${diagramPath}'.`, { cause: error })
104
176
  }
105
177
  }
106
178
 
107
- async function getD2Version() {
179
+ function getD2DiagramFromContent(content: string): D2Diagram | undefined {
180
+ const match = viewBoxRegex.exec(content)
181
+ const { height, width } = match?.groups ?? {}
182
+
183
+ if (!height || !width) {
184
+ return
185
+ }
186
+
187
+ const computedHeight = Number.parseInt(height, 10)
188
+ const computedWidth = Number.parseInt(width, 10)
189
+
190
+ return { content, size: { height: computedHeight, width: computedWidth } }
191
+ }
192
+
193
+ async function getD2BinaryVersion() {
108
194
  try {
109
195
  const [version] = await exec('d2', ['--version'])
110
196
 
@@ -118,6 +204,17 @@ async function getD2Version() {
118
204
  }
119
205
  }
120
206
 
207
+ async function getD2jsFont(font: D2Font, root: URL, fontPath: string): Promise<Uint8Array> {
208
+ if (jsFonts[font]) return jsFonts[font]
209
+
210
+ const buffer = await fs.readFile(path.join(url.fileURLToPath(root), fontPath))
211
+
212
+ // Necessary when crossing the JS/WASM boundary.
213
+ jsFonts[font] = [...buffer] as unknown as Uint8Array
214
+
215
+ return jsFonts[font]
216
+ }
217
+
121
218
  export interface D2Diagram {
122
219
  content: string
123
220
  size: D2Size
@@ -129,3 +226,5 @@ export type D2Size =
129
226
  width: number
130
227
  }
131
228
  | undefined
229
+
230
+ type D2Font = 'regular' | 'italic' | 'bold' | 'semibold'
package/libs/remark.ts CHANGED
@@ -59,7 +59,7 @@ export function remarkAstroD2(config: RemarkAstroD2Config) {
59
59
  parent.children.splice(
60
60
  index,
61
61
  1,
62
- config.inline
62
+ (attributes.inline ?? config.inline)
63
63
  ? makeHtmlSvgNode(attributes, diagram)
64
64
  : makeHtmlImgNode(attributes, outputPath.imgPath, diagram?.size),
65
65
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-d2",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "description": "Astro integration and remark plugin to transform D2 Markdown code blocks into diagrams.",
6
6
  "author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",
@@ -10,6 +10,7 @@
10
10
  "./package.json": "./package.json"
11
11
  },
12
12
  "dependencies": {
13
+ "@terrastruct/d2": "^0.1.33",
13
14
  "hast-util-from-html": "^2.0.3",
14
15
  "hast-util-to-html": "^9.0.4",
15
16
  "unist-util-visit": "^5.0.0"