@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 +12 -0
- package/README.md +6 -0
- package/dist/render/mermaid-style.d.ts +39 -0
- package/dist/render/mermaid-style.js +169 -0
- package/dist/render/mermaid.js +9 -2
- package/package.json +1 -1
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
|
package/dist/render/mermaid.js
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|