enarratio-astro 0.1.0 → 0.4.2
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 +86 -22
- package/LICENSE +21 -21
- package/README.md +155 -151
- package/dist/chart.d.ts +24 -2
- package/dist/chart.js +16 -12
- package/dist/index.d.ts +1 -1
- package/dist/markdown.js +8 -1
- package/package.json +66 -66
- package/src/Chart.astro +31 -31
- package/src/Enhance.astro +21 -21
- package/src/chart.ts +209 -177
- package/src/data.ts +156 -156
- package/src/env.d.ts +7 -7
- package/src/figure.ts +14 -14
- package/src/index.ts +18 -17
- package/src/markdown.ts +171 -164
- package/src/once.ts +11 -11
- package/src/styles.ts +36 -36
package/src/markdown.ts
CHANGED
|
@@ -1,164 +1,171 @@
|
|
|
1
|
-
import { type ChartSpec, renderChart } from "./chart.js";
|
|
2
|
-
import { chartMarkup } from "./figure.js";
|
|
3
|
-
import { chartStyles, type StyleOptions } from "./styles.js";
|
|
4
|
-
|
|
5
|
-
/** Options for the Markdown plugins: how charts are styled. */
|
|
6
|
-
export type MarkdownChartOptions = StyleOptions;
|
|
7
|
-
|
|
8
|
-
const BOOLEAN_KEYS = new Set(["regression", "interactive"]);
|
|
9
|
-
const KEYS = new Set([
|
|
10
|
-
"type",
|
|
11
|
-
"alt",
|
|
12
|
-
"title",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** The parts of
|
|
95
|
-
interface
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
1
|
+
import { type ChartSpec, renderChart } from "./chart.js";
|
|
2
|
+
import { chartMarkup } from "./figure.js";
|
|
3
|
+
import { chartStyles, type StyleOptions } from "./styles.js";
|
|
4
|
+
|
|
5
|
+
/** Options for the Markdown plugins: how charts are styled. */
|
|
6
|
+
export type MarkdownChartOptions = StyleOptions;
|
|
7
|
+
|
|
8
|
+
const BOOLEAN_KEYS = new Set(["regression", "interactive"]);
|
|
9
|
+
const KEYS = new Set([
|
|
10
|
+
"type",
|
|
11
|
+
"alt",
|
|
12
|
+
"title",
|
|
13
|
+
"subtitle",
|
|
14
|
+
"caption",
|
|
15
|
+
"notes",
|
|
16
|
+
"source",
|
|
17
|
+
"sourceHref",
|
|
18
|
+
"byline",
|
|
19
|
+
"x",
|
|
20
|
+
"y",
|
|
21
|
+
"series",
|
|
22
|
+
"value",
|
|
23
|
+
"xLabel",
|
|
24
|
+
"yLabel",
|
|
25
|
+
"xUnit",
|
|
26
|
+
"yUnit",
|
|
27
|
+
"layout",
|
|
28
|
+
"orientation",
|
|
29
|
+
"scale",
|
|
30
|
+
"regression",
|
|
31
|
+
"dataTable",
|
|
32
|
+
"id",
|
|
33
|
+
"interactive",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads a `chart` code block: `key: value` lines, a line of three dashes, then the data.
|
|
38
|
+
*
|
|
39
|
+
* ```chart
|
|
40
|
+
* type: line
|
|
41
|
+
* x: month
|
|
42
|
+
* y: visitors
|
|
43
|
+
* alt: Monthly visitors rise from 820 to 1,190.
|
|
44
|
+
* ---
|
|
45
|
+
* month,visitors
|
|
46
|
+
* 2026-01-01,820
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function parseChartBlock(source: string): ChartSpec & { readonly interactive: boolean } {
|
|
50
|
+
const lines = source.split(/\r?\n/);
|
|
51
|
+
const rule = lines.findIndex((line) => line.trim() === "---");
|
|
52
|
+
if (rule === -1)
|
|
53
|
+
throw new Error('Put a line of three dashes ("---") between the options and the data.');
|
|
54
|
+
const spec: Record<string, string | boolean> = {};
|
|
55
|
+
for (const line of lines.slice(0, rule)) {
|
|
56
|
+
if (line.trim() === "") continue;
|
|
57
|
+
const colon = line.indexOf(":");
|
|
58
|
+
const key = colon === -1 ? "" : line.slice(0, colon).trim();
|
|
59
|
+
if (!KEYS.has(key)) {
|
|
60
|
+
throw new Error(`"${line.trim()}" is not an option. Options are: ${[...KEYS].join(", ")}.`);
|
|
61
|
+
}
|
|
62
|
+
const raw = line.slice(colon + 1).trim();
|
|
63
|
+
spec[key] = BOOLEAN_KEYS.has(key) ? raw === "true" || raw === "yes" : raw;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...(spec as Omit<ChartSpec, "data">),
|
|
67
|
+
data: lines.slice(rule + 1).join("\n"),
|
|
68
|
+
interactive: spec["interactive"] === true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The HTML for one chart block, with the chart CSS when it is the document's first chart. */
|
|
73
|
+
async function chartHtml(
|
|
74
|
+
source: string,
|
|
75
|
+
first: boolean,
|
|
76
|
+
options: MarkdownChartOptions,
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
let markup: string;
|
|
79
|
+
try {
|
|
80
|
+
const { interactive, ...spec } = parseChartBlock(source);
|
|
81
|
+
const result = await renderChart(spec, { interactive });
|
|
82
|
+
if (!result.ok) console.warn(`[enarratio-astro] Markdown chart not drawn: ${result.error}`);
|
|
83
|
+
markup = chartMarkup(result, interactive);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
markup = chartMarkup(
|
|
86
|
+
{ ok: false, error: error instanceof Error ? error.message : String(error) },
|
|
87
|
+
false,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const css = first ? chartStyles(options) : "";
|
|
91
|
+
return css ? `<style>${css}</style>\n${markup}` : markup;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The parts of a Markdown code node the plugins read. */
|
|
95
|
+
interface CodeNode {
|
|
96
|
+
readonly type: "code";
|
|
97
|
+
readonly lang?: string | null | undefined;
|
|
98
|
+
readonly value: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The parts of the Sätteri visitor context the plugin uses. */
|
|
102
|
+
interface SatteriContext {
|
|
103
|
+
replaceNode(node: Readonly<CodeNode>, content: { type: "html"; value: string }): void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Charts in Markdown for Astro's default Markdown processor, Sätteri (Astro 7 and later): every
|
|
108
|
+
* code block whose language is `chart` becomes a chart, drawn when the site builds or renders.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // astro.config.mjs
|
|
112
|
+
* import { satteri } from "@astrojs/markdown-satteri";
|
|
113
|
+
* import { satteriCharts } from "enarratio-astro/markdown";
|
|
114
|
+
* export default defineConfig({ markdown: { processor: satteri({ mdastPlugins: [satteriCharts()] }) } });
|
|
115
|
+
*/
|
|
116
|
+
export function satteriCharts(options: MarkdownChartOptions = {}) {
|
|
117
|
+
let first = true;
|
|
118
|
+
return {
|
|
119
|
+
name: "enarratio-charts",
|
|
120
|
+
before() {
|
|
121
|
+
first = true;
|
|
122
|
+
},
|
|
123
|
+
async code(node: Readonly<CodeNode>, ctx: SatteriContext) {
|
|
124
|
+
if (node.lang !== "chart") return;
|
|
125
|
+
// Visitors run concurrently, so the first block claims the stylesheet before it awaits.
|
|
126
|
+
const isFirst = first;
|
|
127
|
+
first = false;
|
|
128
|
+
const value = await chartHtml(node.value, isFirst, options);
|
|
129
|
+
ctx.replaceNode(node, { type: "html", value });
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface MdastParent {
|
|
135
|
+
children: Array<{
|
|
136
|
+
type: string;
|
|
137
|
+
lang?: string | null | undefined;
|
|
138
|
+
value?: string | undefined;
|
|
139
|
+
children?: unknown[] | undefined;
|
|
140
|
+
}>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Charts in Markdown for the unified (remark) processor, Astro 6's default and an option in
|
|
145
|
+
* Astro 7: every code block whose language is `chart` becomes a chart.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* // astro.config.mjs, Astro 6
|
|
149
|
+
* import { remarkCharts } from "enarratio-astro/markdown";
|
|
150
|
+
* export default defineConfig({ markdown: { remarkPlugins: [remarkCharts] } });
|
|
151
|
+
*/
|
|
152
|
+
export function remarkCharts(options: MarkdownChartOptions = {}) {
|
|
153
|
+
return async (tree: MdastParent): Promise<void> => {
|
|
154
|
+
const blocks: MdastParent["children"] = [];
|
|
155
|
+
const walk = (parent: MdastParent) => {
|
|
156
|
+
for (const child of parent.children) {
|
|
157
|
+
if (child.type === "code" && child.lang === "chart") blocks.push(child);
|
|
158
|
+
else if (Array.isArray(child.children)) walk(child as MdastParent);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
walk(tree);
|
|
162
|
+
let first = true;
|
|
163
|
+
for (const node of blocks) {
|
|
164
|
+
const value = await chartHtml(node.value ?? "", first, options);
|
|
165
|
+
first = false;
|
|
166
|
+
// Turned into raw HTML in place, as remark plugins do.
|
|
167
|
+
Object.assign(node, { type: "html", value });
|
|
168
|
+
delete node.lang;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
package/src/once.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
const styled = new WeakSet<object>();
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* True the first time it is asked about a page render. Astro gives each render its own Request,
|
|
5
|
-
* so the theme's stylesheet is written once per page however many charts the page has.
|
|
6
|
-
*/
|
|
7
|
-
export function firstOnPage(request: object): boolean {
|
|
8
|
-
if (styled.has(request)) return false;
|
|
9
|
-
styled.add(request);
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
1
|
+
const styled = new WeakSet<object>();
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True the first time it is asked about a page render. Astro gives each render its own Request,
|
|
5
|
+
* so the theme's stylesheet is written once per page however many charts the page has.
|
|
6
|
+
*/
|
|
7
|
+
export function firstOnPage(request: object): boolean {
|
|
8
|
+
if (styled.has(request)) return false;
|
|
9
|
+
styled.add(request);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
package/src/styles.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { baseStylesheet, type StylesheetOptions, stylesheet, type Theme } from "enarratio";
|
|
2
|
-
import { defaultTheme } from "enarratio/themes";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* What a page's charts are styled with:
|
|
6
|
-
* - `"theme"` (default): an Enarratio theme's colors and rules, light and dark.
|
|
7
|
-
* - `"base"`: the rules without colors or fonts, for a site whose own tokens define the
|
|
8
|
-
* `--enarratio-*` properties (Capsomer's tokens do).
|
|
9
|
-
* - `"none"`: nothing; the site includes Enarratio's stylesheet itself.
|
|
10
|
-
*/
|
|
11
|
-
export type Styles = "theme" | "base" | "none";
|
|
12
|
-
|
|
13
|
-
/** How charts are styled: the {@link Styles} choice, and for `"theme"` which theme and how. */
|
|
14
|
-
export interface StyleOptions {
|
|
15
|
-
readonly styles?: Styles | undefined;
|
|
16
|
-
/** The Enarratio theme. Default: Enarratio's default theme. */
|
|
17
|
-
readonly theme?: Theme | undefined;
|
|
18
|
-
/** Passed to Enarratio's `stylesheet()`, e.g. `{ colorScheme: { dark: '[data-theme="dark"]' } }`. */
|
|
19
|
-
readonly stylesheetOptions?: StylesheetOptions | undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** The note that replaces a chart that cannot be drawn, set apart from the text around it. */
|
|
23
|
-
const NOTE =
|
|
24
|
-
".enarratio-chart-error{border-inline-start:3px solid currentColor;padding-inline-start:.75em;font-size:.875em}";
|
|
25
|
-
|
|
26
|
-
/** The CSS written once per page (or Markdown document) with a chart; empty for `"none"`. */
|
|
27
|
-
export function chartStyles(options: StyleOptions = {}): string {
|
|
28
|
-
switch (options.styles ?? "theme") {
|
|
29
|
-
case "none":
|
|
30
|
-
return "";
|
|
31
|
-
case "base":
|
|
32
|
-
return `${baseStylesheet()}\n${NOTE}`;
|
|
33
|
-
default:
|
|
34
|
-
return `${stylesheet(options.theme ?? defaultTheme, options.stylesheetOptions)}\n${NOTE}`;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
import { baseStylesheet, type StylesheetOptions, stylesheet, type Theme } from "enarratio";
|
|
2
|
+
import { defaultTheme } from "enarratio/themes";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a page's charts are styled with:
|
|
6
|
+
* - `"theme"` (default): an Enarratio theme's colors and rules, light and dark.
|
|
7
|
+
* - `"base"`: the rules without colors or fonts, for a site whose own tokens define the
|
|
8
|
+
* `--enarratio-*` properties (Capsomer's tokens do).
|
|
9
|
+
* - `"none"`: nothing; the site includes Enarratio's stylesheet itself.
|
|
10
|
+
*/
|
|
11
|
+
export type Styles = "theme" | "base" | "none";
|
|
12
|
+
|
|
13
|
+
/** How charts are styled: the {@link Styles} choice, and for `"theme"` which theme and how. */
|
|
14
|
+
export interface StyleOptions {
|
|
15
|
+
readonly styles?: Styles | undefined;
|
|
16
|
+
/** The Enarratio theme. Default: Enarratio's default theme. */
|
|
17
|
+
readonly theme?: Theme | undefined;
|
|
18
|
+
/** Passed to Enarratio's `stylesheet()`, e.g. `{ colorScheme: { dark: '[data-theme="dark"]' } }`. */
|
|
19
|
+
readonly stylesheetOptions?: StylesheetOptions | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The note that replaces a chart that cannot be drawn, set apart from the text around it. */
|
|
23
|
+
const NOTE =
|
|
24
|
+
".enarratio-chart-error{border-inline-start:3px solid currentColor;padding-inline-start:.75em;font-size:.875em}";
|
|
25
|
+
|
|
26
|
+
/** The CSS written once per page (or Markdown document) with a chart; empty for `"none"`. */
|
|
27
|
+
export function chartStyles(options: StyleOptions = {}): string {
|
|
28
|
+
switch (options.styles ?? "theme") {
|
|
29
|
+
case "none":
|
|
30
|
+
return "";
|
|
31
|
+
case "base":
|
|
32
|
+
return `${baseStylesheet()}\n${NOTE}`;
|
|
33
|
+
default:
|
|
34
|
+
return `${stylesheet(options.theme ?? defaultTheme, options.stylesheetOptions)}\n${NOTE}`;
|
|
35
|
+
}
|
|
36
|
+
}
|