@imontene/mdrender 2.10.0 → 2.11.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,17 @@
1
1
  # Cambios
2
2
 
3
+ ## 2.11.0 — 2026-11-06
4
+
5
+ - **Acabado de los diagramas Mermaid**, en todos los temas:
6
+ - esquinas ligeramente redondeadas en todas las figuras: rectángulos, subgraphs, actores y
7
+ notas, y también rombos, hexágonos, paralelogramos, trapecios y banderas, redibujados con
8
+ las esquinas suavizadas;
9
+ - títulos de los subgraphs en negrita, medidos así por Mermaid para que no se desborden;
10
+ - rellenos de figuras y subgraphs con su color, pero al 72 % de opacidad, para que se vean el
11
+ fondo y las marcas de agua. Se quitó la sombra del estilo de Mermaid 12, que agrisaba los
12
+ rellenos translúcidos; los PDF con diagramas pesan menos, porque Chrome la guardaba como
13
+ imagen.
14
+
3
15
  ## 2.10.0 — 2026-11-04
4
16
 
5
17
  - **Tema de revista de tecnología** `--theme tech_magazine` (o `tech_magazine-dark`, con la
package/README.md CHANGED
@@ -392,6 +392,12 @@ resumen se separan con una línea en blanco.
392
392
  - Si una fórmula o diagrama tiene un error, el documento se genera igual con una caja roja en
393
393
  su lugar, se informa `error: archivo.md:LÍNEA: mensaje` y el comando termina con código `2`.
394
394
 
395
+ **Acabado de los diagramas**: en todos los temas, las figuras de Mermaid tienen las esquinas
396
+ ligeramente redondeadas (rectángulos, rombos, hexágonos, paralelogramos, trapecios, banderas,
397
+ subgraphs, actores y notas), los títulos de los subgraphs van en negrita, y el relleno de figuras
398
+ y subgraphs conserva su color pero es algo transparente, de modo que se ven el fondo de la página
399
+ y las marcas de agua.
400
+
395
401
  **Autoajuste de diagramas de flujo**: en el PDF, un diagrama más ancho que la columna o más alto
396
402
  que la página se reduce entero, y su texto puede quedar diminuto. Por eso mdrender dibuja cada
397
403
  `flowchart`/`graph` con varios anchos de ajuste de las etiquetas y se queda con el que imprime el
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The finish mdrender gives Mermaid diagrams, in every theme:
3
+ *
4
+ * - shapes with slightly rounded corners: rectangles (nodes, subgraphs, sequence actors and
5
+ * notes) through CSS `rx`/`ry`, and polygon nodes (decision, hexagon, parallelogram,
6
+ * trapezoid) redrawn as paths with eased corners, since an SVG polygon has no corner radius;
7
+ * - subgraph titles in bold;
8
+ * - shape and subgraph fills kept in their colours but partly transparent, so the page and its
9
+ * watermarks show through.
10
+ *
11
+ * The CSS goes to Mermaid as `themeCSS`, so it applies while Mermaid measures labels: a bold
12
+ * subgraph title gets the room it needs. Polygons are rounded after rendering, which leaves the
13
+ * diagram's size unchanged.
14
+ */
15
+ /** Corner radius of rectangles and polygon nodes, in CSS px. */
16
+ export declare const CORNER_RADIUS = 5;
17
+ /** Opacity of shape and subgraph fills (strokes and text stay opaque). */
18
+ export declare const FILL_OPACITY = 0.72;
19
+ export declare const MERMAID_THEME_CSS: string;
20
+ type Point = [number, number];
21
+ /**
22
+ * Path data for a closed polygon whose corners are eased with quadratic curves of `radius`
23
+ * (smaller where an edge is too short for it: at most a third of each adjacent edge).
24
+ */
25
+ export declare function roundedPolygonPath(points: Point[], radius?: number): string;
26
+ /**
27
+ * The corners of a closed shape drawn only with straight lines written as cubic segments, as
28
+ * Mermaid draws its flag and other hand-drawn polygons: the fill as one subpath, the outline as
29
+ * each side drawn twice ("M a C … b M a C … b M b C … c …"). Undefined for any path with a curve,
30
+ * another command, a gap or an open end.
31
+ */
32
+ export declare function straightPathCorners(d: string): Point[] | undefined;
33
+ /**
34
+ * Round the corners of node polygons: `<polygon class="label-container">` (decision, hexagon,
35
+ * parallelogram, trapezoid) and closed shapes drawn only with straight segments (the flag).
36
+ * Other polygons and paths (arrowheads, edges, curved shapes) are left alone.
37
+ */
38
+ export declare function roundNodePolygons(svg: string, radius?: number): string;
39
+ export {};
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The finish mdrender gives Mermaid diagrams, in every theme:
3
+ *
4
+ * - shapes with slightly rounded corners: rectangles (nodes, subgraphs, sequence actors and
5
+ * notes) through CSS `rx`/`ry`, and polygon nodes (decision, hexagon, parallelogram,
6
+ * trapezoid) redrawn as paths with eased corners, since an SVG polygon has no corner radius;
7
+ * - subgraph titles in bold;
8
+ * - shape and subgraph fills kept in their colours but partly transparent, so the page and its
9
+ * watermarks show through.
10
+ *
11
+ * The CSS goes to Mermaid as `themeCSS`, so it applies while Mermaid measures labels: a bold
12
+ * subgraph title gets the room it needs. Polygons are rounded after rendering, which leaves the
13
+ * diagram's size unchanged.
14
+ */
15
+ /** Corner radius of rectangles and polygon nodes, in CSS px. */
16
+ export const CORNER_RADIUS = 5;
17
+ /** Opacity of shape and subgraph fills (strokes and text stay opaque). */
18
+ export const FILL_OPACITY = 0.72;
19
+ const SHAPES = [
20
+ ".node rect",
21
+ ".node polygon",
22
+ ".node circle",
23
+ ".node ellipse",
24
+ ".node path.label-container",
25
+ // Hand-drawn shapes (flag, cylinder…) carry the class on their group: fill-opacity inherits.
26
+ ".node .outer-path",
27
+ ".cluster rect",
28
+ "rect.actor",
29
+ "rect.note",
30
+ ];
31
+ export const MERMAID_THEME_CSS = [
32
+ `.node rect, .cluster rect, rect.actor, rect.note { rx: ${CORNER_RADIUS}px; ry: ${CORNER_RADIUS}px; }`,
33
+ `${SHAPES.join(", ")} { fill-opacity: ${FILL_OPACITY}; }`,
34
+ ".cluster-label, .cluster-label * { font-weight: 700; }",
35
+ // Mermaid's "neo" look drops a shadow under shapes; under a see-through fill it would show
36
+ // through the whole shape and grey it, hiding the page behind instead of revealing it.
37
+ `${SHAPES.join(", ")}, .node .outer-path path { filter: none !important; }`,
38
+ ].join("\n");
39
+ function parsePoints(points) {
40
+ const numbers = points
41
+ .trim()
42
+ .split(/[\s,]+/)
43
+ .map(Number);
44
+ if (numbers.length < 6 || numbers.length % 2 !== 0 || numbers.some((n) => !Number.isFinite(n))) {
45
+ return undefined;
46
+ }
47
+ const result = [];
48
+ for (let i = 0; i < numbers.length; i += 2)
49
+ result.push([numbers[i], numbers[i + 1]]);
50
+ return result;
51
+ }
52
+ const round = (n) => Math.round(n * 100) / 100;
53
+ /**
54
+ * Path data for a closed polygon whose corners are eased with quadratic curves of `radius`
55
+ * (smaller where an edge is too short for it: at most a third of each adjacent edge).
56
+ */
57
+ export function roundedPolygonPath(points, radius = CORNER_RADIUS) {
58
+ const n = points.length;
59
+ const corners = points.map((p, i) => {
60
+ const prev = points[(i - 1 + n) % n];
61
+ const next = points[(i + 1) % n];
62
+ const toPrev = Math.hypot(prev[0] - p[0], prev[1] - p[1]);
63
+ const toNext = Math.hypot(next[0] - p[0], next[1] - p[1]);
64
+ const r = Math.min(radius, toPrev / 3, toNext / 3);
65
+ const along = (q, d) => d === 0 ? p : [p[0] + ((q[0] - p[0]) * r) / d, p[1] + ((q[1] - p[1]) * r) / d];
66
+ return { start: along(prev, toPrev), corner: p, end: along(next, toNext) };
67
+ });
68
+ const fmt = (p) => `${round(p[0])},${round(p[1])}`;
69
+ const parts = [`M${fmt(corners[0].end)}`];
70
+ for (let i = 1; i <= n; i++) {
71
+ const c = corners[i % n];
72
+ parts.push(`L${fmt(c.start)}`, `Q${fmt(c.corner)} ${fmt(c.end)}`);
73
+ }
74
+ return parts.join(" ") + " Z";
75
+ }
76
+ const near = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]) < 0.5;
77
+ /** Distance from q to the line through a and b. */
78
+ function offLine(a, b, q) {
79
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
80
+ if (length === 0)
81
+ return Math.hypot(q[0] - a[0], q[1] - a[1]);
82
+ return Math.abs((b[0] - a[0]) * (a[1] - q[1]) - (a[0] - q[0]) * (b[1] - a[1])) / length;
83
+ }
84
+ /**
85
+ * The corners of a closed shape drawn only with straight lines written as cubic segments, as
86
+ * Mermaid draws its flag and other hand-drawn polygons: the fill as one subpath, the outline as
87
+ * each side drawn twice ("M a C … b M a C … b M b C … c …"). Undefined for any path with a curve,
88
+ * another command, a gap or an open end.
89
+ */
90
+ export function straightPathCorners(d) {
91
+ const tokens = d.match(/[a-z]|-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi) ?? [];
92
+ const segments = [];
93
+ let current;
94
+ let i = 0;
95
+ const take = () => {
96
+ const x = Number(tokens[i]);
97
+ const y = Number(tokens[i + 1]);
98
+ i += 2;
99
+ return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : undefined;
100
+ };
101
+ while (i < tokens.length) {
102
+ const command = tokens[i++];
103
+ if (command === "M") {
104
+ current = take();
105
+ if (!current)
106
+ return undefined;
107
+ }
108
+ else if (command === "C" && current) {
109
+ // One or more segments after a single "C".
110
+ do {
111
+ const c1 = take();
112
+ const c2 = take();
113
+ const to = take();
114
+ if (!c1 || !c2 || !to)
115
+ return undefined;
116
+ if (offLine(current, to, c1) > 0.5 || offLine(current, to, c2) > 0.5)
117
+ return undefined;
118
+ segments.push([current, to]);
119
+ current = to;
120
+ } while (i < tokens.length && !/[a-z]/i.test(tokens[i]));
121
+ }
122
+ else if (command === "Z" || command === "z") {
123
+ continue;
124
+ }
125
+ else {
126
+ return undefined;
127
+ }
128
+ }
129
+ // Drop each side's repeated stroke, then the sides must chain into a closed shape.
130
+ const sides = segments.filter(([from, to], k) => k === 0 || !(near(from, segments[k - 1][0]) && near(to, segments[k - 1][1])));
131
+ if (sides.length < 3)
132
+ return undefined;
133
+ for (let k = 1; k < sides.length; k++)
134
+ if (!near(sides[k][0], sides[k - 1][1]))
135
+ return undefined;
136
+ if (!near(sides.at(-1)[1], sides[0][0]))
137
+ return undefined;
138
+ const corners = sides.map(([from]) => from);
139
+ // Drop points that do not turn (several segments along one side).
140
+ const turning = corners.filter((p, k) => {
141
+ const prev = corners[(k - 1 + corners.length) % corners.length];
142
+ const next = corners[(k + 1) % corners.length];
143
+ return offLine(prev, next, p) > 0.5;
144
+ });
145
+ return turning.length >= 3 ? turning : undefined;
146
+ }
147
+ const NODE_PATH = /<path\b([^>]*?)\sd="([^"]*)"/g;
148
+ const NODE_POLYGON = /<polygon\b([^>]*?)\spoints="([^"]*)"([^>]*?)(\/?)>(<\/polygon>)?/g;
149
+ /**
150
+ * Round the corners of node polygons: `<polygon class="label-container">` (decision, hexagon,
151
+ * parallelogram, trapezoid) and closed shapes drawn only with straight segments (the flag).
152
+ * Other polygons and paths (arrowheads, edges, curved shapes) are left alone.
153
+ */
154
+ export function roundNodePolygons(svg, radius = CORNER_RADIUS) {
155
+ return svg
156
+ .replace(NODE_POLYGON, (tag, before, points, after) => {
157
+ if (!/\bclass="[^"]*\blabel-container\b/.test(before + after))
158
+ return tag;
159
+ const parsed = parsePoints(points);
160
+ if (!parsed)
161
+ return tag;
162
+ return `<path${before} d="${roundedPolygonPath(parsed, radius)}"${after}></path>`;
163
+ })
164
+ .replace(NODE_PATH, (tag, before, d) => {
165
+ const corners = straightPathCorners(d);
166
+ return corners ? `<path${before} d="${roundedPolygonPath(corners, radius)}"` : tag;
167
+ });
168
+ }
169
+ //# sourceMappingURL=mermaid-style.js.map
@@ -1,6 +1,7 @@
1
1
  import { MERMAID_JS, readTextAsset } from "../assets.js";
2
2
  import { mermaidPlaceholder } from "../markdown/mermaid.js";
3
3
  import { FONT_FAMILIES, fontFaceCss, fontStack } from "./fonts.js";
4
+ import { MERMAID_THEME_CSS, roundNodePolygons } from "./mermaid-style.js";
4
5
  import { escapeHtml } from "../html.js";
5
6
  import { authorConfigured, candidate, chooseCandidate, flipWorthTrying, flippedDirection, flowchartHeader, withDirection, WRAPPING_WIDTHS, wrapsExhausted, } from "./mermaid-fit.js";
6
7
  export { MERMAID_THEMES } from "../options.js";
@@ -25,6 +26,8 @@ function mermaidConfig(theme, font, fontPx, wrap, themeVariables = {}) {
25
26
  fontSize: fontPx,
26
27
  themeVariables: { ...themeVariables, fontFamily, fontSize: `${fontPx}px` },
27
28
  htmlLabels: true,
29
+ // Rounded corners, bold subgraph titles, see-through fills (render/mermaid-style.ts).
30
+ themeCSS: MERMAID_THEME_CSS,
28
31
  // Actors only at the top of sequence diagrams: repeating them below costs height.
29
32
  sequence: { mirrorActors: false },
30
33
  ...(wrap === undefined ? {} : { flowchart: { wrappingWidth: wrap } }),
@@ -51,7 +54,8 @@ body { margin: 0; width: ${widthPx}px; font-family: ${fontStack(font)}; }</style
51
54
  // Load the font weights Mermaid uses before it measures any text.
52
55
  await Promise.all(["400", "600", "700"].map((w) => document.fonts.load(`${w} 16px "${family}"`)));
53
56
  }, family);
54
- const render = (source, id, wrap) => page.evaluate(async (source, id, config) => {
57
+ const render = (source, id, wrap) => page
58
+ .evaluate(async (source, id, config) => {
55
59
  const { mermaid } = globalThis;
56
60
  // Each render sets the whole configuration, so one layout never leaks into the next.
57
61
  mermaid.initialize(config);
@@ -61,7 +65,10 @@ body { margin: 0; width: ${widthPx}px; font-family: ${fontStack(font)}; }</style
61
65
  catch (error) {
62
66
  return { error: String(error?.message ?? error) };
63
67
  }
64
- }, source, id, mermaidConfig(theme, font, fontPx, wrap, options.themeVariables));
68
+ }, source, id, mermaidConfig(theme, font, fontPx, wrap, options.themeVariables))
69
+ .then((result) =>
70
+ // Rounding the polygons keeps each diagram's size, so autofit measures it the same.
71
+ "svg" in result ? { svg: roundNodePolygons(result.svg) } : result);
65
72
  const results = [];
66
73
  for (const [i, block] of blocks.entries()) {
67
74
  const id = `mermaid-${i + 1}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imontene/mdrender",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Command-line app for Windows and Linux that renders Markdown — with LaTeX equations and Mermaid diagrams — to PDF and HTML set in Manrope or Inter",
5
5
  "keywords": [
6
6
  "markdown",